Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Neuer Batterietyp SBS Unipower XL #1199

Merged
merged 12 commits into from
Sep 16, 2024
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;
Snoopy-HSS marked this conversation as resolved.
Show resolved Hide resolved
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:
Snoopy-HSS marked this conversation as resolved.
Show resolved Hide resolved
// 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;
Snoopy-HSS marked this conversation as resolved.
Show resolved Hide resolved
_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
Loading