diff --git a/include/BatteryCanReceiver.h b/include/BatteryCanReceiver.h index d9565ee39..5f35c09aa 100644 --- a/include/BatteryCanReceiver.h +++ b/include/BatteryCanReceiver.h @@ -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); diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 380b2dec4..0c50eecdb 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -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; diff --git a/include/SBSCanReceiver.h b/include/SBSCanReceiver.h new file mode 100644 index 000000000..0cec94196 --- /dev/null +++ b/include/SBSCanReceiver.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "BatteryCanReceiver.h" +#include +#include + +class SBSCanReceiver : public BatteryCanReceiver { +public: + bool init(bool verboseLogging) final; + void onMessage(twai_message_t rx_message) final; + + std::shared_ptr getStats() const final { return _stats; } + +private: + void dummyData(); + std::shared_ptr _stats = + std::make_shared(); +}; diff --git a/src/Battery.cpp b/src/Battery.cpp index 029f8ab0a..e10abb1fa 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -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" @@ -61,6 +62,9 @@ void BatteryClass::updateSettings() case 4: _upProvider = std::make_unique(); break; + case 5: + _upProvider = std::make_unique(); + break; default: MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider); return; diff --git a/src/BatteryCanReceiver.cpp b/src/BatteryCanReceiver.cpp index 90ea7b33d..ff16fd043 100644 --- a/src/BatteryCanReceiver.cpp +++ b/src/BatteryCanReceiver.cpp @@ -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]; diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 48c869eac..2c0366a9f 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -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); @@ -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(); diff --git a/src/MqttHandleBatteryHass.cpp b/src/MqttHandleBatteryHass.cpp index 9f24abe42..a17a10b14 100644 --- a/src/MqttHandleBatteryHass.cpp +++ b/src/MqttHandleBatteryHass.cpp @@ -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; diff --git a/src/SBSCanReceiver.cpp b/src/SBSCanReceiver.cpp new file mode 100644 index 000000000..9150075c1 --- /dev/null +++ b/src/SBSCanReceiver.cpp @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SBSCanReceiver.h" +#include "MessageOutput.h" +#include "PinMapping.h" +#include +#include + +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(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(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((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 diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 91045f22a..91f4fa9fc 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -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", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index a851f7a27..51f12daba 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -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", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 43bfbc4a1..fa0b2bb09 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -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", diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue index 84852f7aa..4b7b7aad7 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -228,6 +228,7 @@ export default defineComponent({ { key: 2, value: 'Mqtt' }, { key: 3, value: 'Victron' }, { key: 4, value: 'PytesCan' }, + { key: 5, value: 'SBSCan' }, ], jkBmsInterfaceTypeList: [ { key: 0, value: 'Uart' },