Skip to content

Commit

Permalink
Feature: Support for SBS Unipower batteries (#1199)
Browse files Browse the repository at this point in the history
Allows to connect to SBS Unipower batteries using a CAN bus.
  • Loading branch information
Snoopy-HSS authored Sep 16, 2024
1 parent a6e7007 commit 3fc4309
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 0 deletions.
1 change: 1 addition & 0 deletions include/BatteryCanReceiver.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class BatteryCanReceiver : public BatteryProvider {
uint16_t readUnsignedInt16(uint8_t *data);
int16_t readSignedInt16(uint8_t *data);
uint32_t readUnsignedInt32(uint8_t *data);
int32_t readSignedInt24(uint8_t *data);
float scaleValue(int16_t value, float factor);
bool getBit(uint8_t value, uint8_t bit);

Expand Down
32 changes: 32 additions & 0 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,38 @@ class PylontechBatteryStats : public BatteryStats {
bool _chargeImmediately;
};

class SBSBatteryStats : public BatteryStats {
friend class SBSCanReceiver;

public:
void getLiveViewData(JsonVariant& root) const final;
void mqttPublish() const final;
float getChargeCurrent() const { return _current; } ;
float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ;

private:
void setLastUpdate(uint32_t ts) { _lastUpdate = ts; }

float _chargeVoltage;
float _chargeCurrentLimitation;
float _dischargeCurrentLimitation;
uint16_t _stateOfHealth;
float _current;
float _temperature;

bool _alarmUnderTemperature;
bool _alarmOverTemperature;
bool _alarmUnderVoltage;
bool _alarmOverVoltage;
bool _alarmBmsInternal;

bool _warningHighCurrentDischarge;
bool _warningHighCurrentCharge;

bool _chargeEnabled;
bool _dischargeEnabled;
};

class PytesBatteryStats : public BatteryStats {
friend class PytesCanReceiver;

Expand Down
19 changes: 19 additions & 0 deletions include/SBSCanReceiver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include "BatteryCanReceiver.h"
#include <driver/twai.h>
#include <Arduino.h>

class SBSCanReceiver : public BatteryCanReceiver {
public:
bool init(bool verboseLogging) final;
void onMessage(twai_message_t rx_message) final;

std::shared_ptr<BatteryStats> getStats() const final { return _stats; }

private:
void dummyData();
std::shared_ptr<SBSBatteryStats> _stats =
std::make_shared<SBSBatteryStats>();
};
4 changes: 4 additions & 0 deletions src/Battery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "Battery.h"
#include "MessageOutput.h"
#include "PylontechCanReceiver.h"
#include "SBSCanReceiver.h"
#include "JkBmsController.h"
#include "VictronSmartShunt.h"
#include "MqttBattery.h"
Expand Down Expand Up @@ -61,6 +62,9 @@ void BatteryClass::updateSettings()
case 4:
_upProvider = std::make_unique<PytesCanReceiver>();
break;
case 5:
_upProvider = std::make_unique<SBSCanReceiver>();
break;
default:
MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider);
return;
Expand Down
5 changes: 5 additions & 0 deletions src/BatteryCanReceiver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ int16_t BatteryCanReceiver::readSignedInt16(uint8_t *data)
return this->readUnsignedInt16(data);
}

int32_t BatteryCanReceiver::readSignedInt24(uint8_t *data)
{
return (data[2] << 16) | (data[1] << 8) | data[0];
}

uint32_t BatteryCanReceiver::readUnsignedInt32(uint8_t *data)
{
return (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0];
Expand Down
43 changes: 43 additions & 0 deletions src/BatteryStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,30 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
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, "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"));

// 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);
Expand Down Expand Up @@ -377,6 +401,25 @@ void PylontechBatteryStats::mqttPublish() const
MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately));
}

void SBSBatteryStats::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/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();
Expand Down
28 changes: 28 additions & 0 deletions src/MqttHandleBatteryHass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,34 @@ void MqttHandleBatteryHassClass::loop()
publishBinarySensor("Warning BMS internal", "mdi:alert-outline", "warning/bmsInternal", "1", "0");
publishBinarySensor("Warning Cell Imbalance", "mdi:alert-outline", "warning/cellImbalance", "1", "0");
break;

case 5: // SBS Unipower
publishSensor("Battery voltage", NULL, "voltage", "voltage", "measurement", "V");
publishSensor("Battery current", NULL, "current", "current", "measurement", "A");
publishSensor("Temperature", NULL, "temperature", "temperature", "measurement", "°C");
publishSensor("State of Health (SOH)", "mdi:heart-plus", "stateOfHealth", NULL, "measurement", "%");
publishSensor("Charge voltage (BMS)", NULL, "settings/chargeVoltage", "voltage", "measurement", "V");
publishSensor("Charge current limit", NULL, "settings/chargeCurrentLimitation", "current", "measurement", "A");
publishSensor("Discharge current limit", NULL, "settings/dischargeCurrentLimitation", "current", "measurement", "A");

publishBinarySensor("Warning Discharge current", "mdi:alert-outline", "warning/highCurrentDischarge", "1", "0");

publishBinarySensor("Alarm Temperature low", "mdi:thermometer-low", "alarm/underTemperature", "1", "0");

publishBinarySensor("Alarm Temperature high", "mdi:thermometer-high", "alarm/overTemperature", "1", "0");

publishBinarySensor("Alarm Voltage low", "mdi:alert", "alarm/underVoltage", "1", "0");

publishBinarySensor("Alarm Voltage high", "mdi:alert", "alarm/overVoltage", "1", "0");

publishBinarySensor("Alarm BMS internal", "mdi:alert", "alarm/bmsInternal", "1", "0");

publishBinarySensor("Warning High charge current", "mdi:alert-outline", "warning/highCurrentCharge", "1", "0");

publishBinarySensor("Charge enabled", "mdi:battery-arrow-up", "charging/chargeEnabled", "1", "0");
publishBinarySensor("Discharge enabled", "mdi:battery-arrow-down", "charging/dischargeEnabled", "1", "0");

break;
}

_doPublish = false;
Expand Down
185 changes: 185 additions & 0 deletions src/SBSCanReceiver.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SBSCanReceiver.h"
#include "MessageOutput.h"
#include "PinMapping.h"
#include <driver/twai.h>
#include <ctime>

bool SBSCanReceiver::init(bool verboseLogging)
{
_stats->_chargeVoltage =58.4;
return BatteryCanReceiver::init(verboseLogging, "SBS");
}


void SBSCanReceiver::onMessage(twai_message_t rx_message)
{
switch (rx_message.identifier) {
case 0x610: {
_stats->setVoltage(this->readUnsignedInt16(rx_message.data)* 0.001, millis());
_stats->_current =(this->readSignedInt16(rx_message.data + 3)) * 0.001;
_stats->setSoC(static_cast<float>(this->readUnsignedInt16(rx_message.data + 6)), 1, millis());

if (_verboseLogging) {
MessageOutput.printf("[SBS Unipower] 1552 SoC: %f Voltage: %f Current: %f\r\n", _stats->getSoC(), _stats->getVoltage(), _stats->_current);
}
break;
}

case 0x630: {
int clusterstate = rx_message.data[0];
switch (clusterstate) {
case 0:
// Battery inactive
_stats->_dischargeEnabled = 0;
_stats->_chargeEnabled = 0;
break;

case 1:
// Battery Discharge mode (recuperation enabled)
_stats->_chargeEnabled = 1;
_stats->_dischargeEnabled = 1;
break;

case 2:
// Battery in charge Mode (discharge with half current possible (45A))
_stats->_chargeEnabled = 1;
_stats->_dischargeEnabled = 1;
break;

case 4:
// Battery Fault
_stats->_chargeEnabled = 0;
_stats->_dischargeEnabled = 0;
break;

case 8:
// Battery Deepsleep
_stats->_chargeEnabled = 0;
_stats->_dischargeEnabled = 0;
break;

default:
_stats->_dischargeEnabled = 0;
_stats->_chargeEnabled = 0;
break;
}
_stats->setManufacturer("SBS UniPower ");

if (_verboseLogging) {
MessageOutput.printf("[SBS Unipower] 1584 chargeStatusBits: %d %d\r\n", _stats->_chargeEnabled, _stats->_dischargeEnabled);
}
break;
}

case 0x640: {
_stats->_chargeCurrentLimitation = (this->readSignedInt24(rx_message.data + 3) * 0.001);
_stats->_dischargeCurrentLimitation = (this->readSignedInt24(rx_message.data)) * 0.001;

if (_verboseLogging) {
MessageOutput.printf("[SBS Unipower] 1600 Currents %f, %f \r\n", _stats->_chargeCurrentLimitation, _stats->_dischargeCurrentLimitation);
}
break;
}

case 0x650: {
byte temp = rx_message.data[0];
_stats->_temperature = (static_cast<float>(temp)-32) /1.8;

if (_verboseLogging) {
MessageOutput.printf("[SBS Unipower] 1616 Temp %f \r\n",_stats->_temperature);
}
break;
}

case 0x660: {
uint16_t alarmBits = rx_message.data[0];
_stats->_alarmUnderTemperature = this->getBit(alarmBits, 1);
_stats->_alarmOverTemperature = this->getBit(alarmBits, 0);
_stats->_alarmUnderVoltage = this->getBit(alarmBits, 3);
_stats->_alarmOverVoltage= this->getBit(alarmBits, 2);
_stats->_alarmBmsInternal= this->getBit(rx_message.data[1], 2);

if (_verboseLogging) {
MessageOutput.printf("[SBS Unipower] 1632 Alarms: %d %d %d %d \r\n ", _stats->_alarmUnderTemperature, _stats->_alarmOverTemperature, _stats->_alarmUnderVoltage, _stats->_alarmOverVoltage);
}
break;
}

case 0x670: {
uint16_t warningBits = rx_message.data[1];
_stats->_warningHighCurrentDischarge = this->getBit(warningBits, 1);
_stats->_warningHighCurrentCharge = this->getBit(warningBits, 0);

if (_verboseLogging) {
MessageOutput.printf("[SBS Unipower] 1648 Warnings: %d %d \r\n", _stats->_warningHighCurrentDischarge, _stats->_warningHighCurrentCharge);
}
break;
}

default:
return; // do not update last update timestamp
break;
}

_stats->setLastUpdate(millis());
}

#ifdef SBSCanReceiver_DUMMY
void SBSCanReceiver::dummyData()
{
static uint32_t lastUpdate = millis();
static uint8_t issues = 0;

if (millis() < (lastUpdate + 5 * 1000)) { return; }

lastUpdate = millis();
_stats->setLastUpdate(lastUpdate);

auto dummyFloat = [](int offset) -> float {
return offset + (static_cast<float>((lastUpdate + offset) % 10) / 10);
};

_stats->setManufacturer("SBS Unipower XL");
_stats->setSoC(42, 0/*precision*/, millis());
_stats->_chargeVoltage = dummyFloat(50);
_stats->_chargeCurrentLimitation = dummyFloat(33);
_stats->_dischargeCurrentLimitation = dummyFloat(12);
_stats->_stateOfHealth = 99;
_stats->setVoltage(48.67, millis());
_stats->_current = dummyFloat(-1);
_stats->_temperature = dummyFloat(20);

_stats->_chargeEnabled = true;
_stats->_dischargeEnabled = true;

_stats->_warningHighCurrentDischarge = false;
_stats->_warningHighCurrentCharge = false;

_stats->_alarmOverCurrentDischarge = false;
_stats->_alarmOverCurrentCharge = false;
_stats->_alarmUnderVoltage = false;
_stats->_alarmOverVoltage = false;


if (issues == 1 || issues == 3) {
_stats->_warningHighCurrentDischarge = true;
_stats->_warningHighCurrentCharge = true;
}

if (issues == 2 || issues == 3) {
_stats->_alarmOverCurrentDischarge = true;
_stats->_alarmOverCurrentCharge = true;
_stats->_alarmUnderVoltage = true;
_stats->_alarmOverVoltage = true;
}

if (issues == 4) {
_stats->_warningHighCurrentCharge = true;
_stats->_alarmUnderVoltage = true;
_stats->_dischargeEnabled = false;
}

issues = (issues + 1) % 5;
}
#endif
1 change: 1 addition & 0 deletions webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,7 @@
"VerboseLogging": "@:base.VerboseLogging",
"Provider": "Datenanbieter",
"ProviderPylontechCan": "Pylontech per CAN-Bus",
"ProviderSBSCan": "SBS Unipower per CAN-Bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
"ProviderMqtt": "Batteriewerte aus MQTT Broker",
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
Expand Down
1 change: 1 addition & 0 deletions webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@
"VerboseLogging": "@:base.VerboseLogging",
"Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderSBSCan": "SBS Unipower using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "Battery data from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
Expand Down
1 change: 1 addition & 0 deletions webapp/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@
"VerboseLogging": "@:base.VerboseLogging",
"Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderSBSCan": "SBS Unipower using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "Battery data from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
Expand Down
Loading

0 comments on commit 3fc4309

Please sign in to comment.