From 3d52a4bd590f1d0b39805450a74e4677e86a7927 Mon Sep 17 00:00:00 2001 From: Tobias Diedrich Date: Sat, 31 Aug 2024 15:42:24 +0200 Subject: [PATCH] Add additional Pylontech CAN protocol fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I noticed that these are missing while looking at dissassembly of the Pytes implementation of the protocol. I also found Pylontech sample CAN messages] which match the Pytes implementation [1]: ``` CAN ID – followed by 2 to 8 bytes of data: 0x351 – 14 02 74 0E 74 0E CC 01 – Battery voltage + current limits ^^^^^ discharge cutoff voltage 46.0V 0x355 – 1A 00 64 00 – State of Health (SOH) / State of Charge (SOC) 0x356 – 4e 13 02 03 04 05 – Voltage / Current / Temp 0x359 – 00 00 00 00 0A 50 4E – Protection & Alarm flags ^^^^^ always 0x50 0x59 in Pytes implementation ^^ module count (matches the blog article image) 0x35C – C0 00 – Battery charge request flags ^^ two possible additional flags (bit 3 and bit 4) 0x35E – 50 59 4C 4F 4E 20 20 20 – Manufacturer name (“PYLON “) ^^^^^^^^^^^^^^ Note: Pytes sends a 5-byte message "PYTES" instead padding with spaces ``` The extra charge request flag is "bit4: SOC low" (Seems to be SoC < 10% threshold for Pytes), I haven't bothered adding that as it provides little value. [1] https://www.setfirelabs.com/green-energy/pylontech-can-reading-can-replication --- include/BatteryStats.h | 3 +++ src/BatteryStats.cpp | 3 +++ src/MqttHandleBatteryHass.cpp | 1 + src/PylontechCanReceiver.cpp | 16 ++++++++++++++-- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 94da35d78..94f6470eb 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -104,6 +104,7 @@ class PylontechBatteryStats : public BatteryStats { float _chargeVoltage; float _chargeCurrentLimitation; float _dischargeCurrentLimitation; + float _dischargeVoltageLimitation; uint16_t _stateOfHealth; float _temperature; @@ -126,6 +127,8 @@ class PylontechBatteryStats : public BatteryStats { bool _chargeEnabled; bool _dischargeEnabled; bool _chargeImmediately; + + uint8_t _moduleCount; }; class PytesBatteryStats : public BatteryStats { diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 9f32931b7..83b2667c9 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -88,9 +88,11 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const // values go into the "Status" card of the web application addLiveViewValue(root, "chargeVoltage", _chargeVoltage, "V", 1); addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1); + addLiveViewValue(root, "dischargeVoltageLimitation", _dischargeVoltageLimitation, "V", 1); addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1); addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); addLiveViewValue(root, "temperature", _temperature, "°C", 1); + addLiveViewValue(root, "modules", _moduleCount, "", 0); addLiveViewTextValue(root, "chargeEnabled", (_chargeEnabled?"yes":"no")); addLiveViewTextValue(root, "dischargeEnabled", (_dischargeEnabled?"yes":"no")); @@ -315,6 +317,7 @@ void PylontechBatteryStats::mqttPublish() const MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage)); MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation)); + MqttSettings.publish("battery/settings/dischargeVoltageLimitation", String(_dischargeVoltageLimitation)); MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimitation)); MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); MqttSettings.publish("battery/temperature", String(_temperature)); diff --git a/src/MqttHandleBatteryHass.cpp b/src/MqttHandleBatteryHass.cpp index 9f24abe42..f2fa27343 100644 --- a/src/MqttHandleBatteryHass.cpp +++ b/src/MqttHandleBatteryHass.cpp @@ -55,6 +55,7 @@ void MqttHandleBatteryHassClass::loop() 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 voltage limit", NULL, "settings/dischargeVoltageLimitation", "voltage", "measurement", "V"); publishSensor("Discharge current limit", NULL, "settings/dischargeCurrentLimitation", "current", "measurement", "A"); publishBinarySensor("Alarm Discharge current", "mdi:alert", "alarm/overCurrentDischarge", "1", "0"); diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index 517a6a230..7cc7831da 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -18,10 +18,12 @@ void PylontechCanReceiver::onMessage(twai_message_t rx_message) _stats->_chargeVoltage = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); _stats->_chargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1); _stats->_dischargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); + _stats->_dischargeVoltageLimitation = this->scaleValue(this->readUnsignedInt16(rx_message.data + 6), 0.1); if (_verboseLogging) { - MessageOutput.printf("[Pylontech] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f\r\n", - _stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->_dischargeCurrentLimitation); + MessageOutput.printf("[Pylontech] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f dischargeVoltageLimitation: %f\r\n", + _stats->_chargeVoltage, _stats->_chargeCurrentLimitation, + _stats->_dischargeCurrentLimitation, _stats->_dischargeVoltageLimitation); } break; } @@ -93,6 +95,13 @@ void PylontechCanReceiver::onMessage(twai_message_t rx_message) _stats->_warningBmsInternal, _stats->_warningHighCurrentCharge); } + + _stats->_moduleCount = rx_message.data[4]; + if (_verboseLogging) { + MessageOutput.printf("[Pylontech] Modules: %d\r\n", + _stats->_moduleCount); + } + break; } @@ -155,6 +164,7 @@ void PylontechCanReceiver::dummyData() _stats->_chargeVoltage = dummyFloat(50); _stats->_chargeCurrentLimitation = dummyFloat(33); _stats->_dischargeCurrentLimitation = dummyFloat(12); + _stats->_dischargeVoltageLimitation = dummyFloat(46); _stats->_stateOfHealth = 99; _stats->setVoltage(48.67, millis()); _stats->setCurrent(dummyFloat(-1), 1/*precision*/, millis()); @@ -164,6 +174,8 @@ void PylontechCanReceiver::dummyData() _stats->_dischargeEnabled = true; _stats->_chargeImmediately = false; + _stats->_moduleCount = 1; + _stats->_warningHighCurrentDischarge = false; _stats->_warningHighCurrentCharge = false; _stats->_warningLowTemperature = false;