Skip to content

Commit

Permalink
Feature: parse additional Pylontech CAN protocol fields (#1213)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ranma authored Sep 25, 2024
1 parent 2265992 commit 191cc80
Show file tree
Hide file tree
Showing 4 changed files with 23 additions and 2 deletions.
3 changes: 3 additions & 0 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class PylontechBatteryStats : public BatteryStats {

float _chargeVoltage;
float _chargeCurrentLimitation;
float _dischargeVoltageLimitation;
uint16_t _stateOfHealth;
float _temperature;

Expand All @@ -140,6 +141,8 @@ class PylontechBatteryStats : public BatteryStats {
bool _chargeEnabled;
bool _dischargeEnabled;
bool _chargeImmediately;

uint8_t _moduleCount;
};

class SBSBatteryStats : public BatteryStats {
Expand Down
4 changes: 4 additions & 0 deletions src/BatteryStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ 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, "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"));
Expand Down Expand Up @@ -380,6 +382,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/stateOfHealth", String(_stateOfHealth));
MqttSettings.publish("battery/temperature", String(_temperature));
MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge));
Expand All @@ -399,6 +402,7 @@ void PylontechBatteryStats::mqttPublish() const
MqttSettings.publish("battery/charging/chargeEnabled", String(_chargeEnabled));
MqttSettings.publish("battery/charging/dischargeEnabled", String(_dischargeEnabled));
MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately));
MqttSettings.publish("battery/modulesTotal", String(_moduleCount));
}

void SBSBatteryStats::mqttPublish() const
Expand Down
2 changes: 2 additions & 0 deletions src/MqttHandleBatteryHass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ 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");
publishSensor("Module Count", "mdi:counter", "modulesTotal");

publishBinarySensor("Alarm Discharge current", "mdi:alert", "alarm/overCurrentDischarge", "1", "0");
publishBinarySensor("Warning Discharge current", "mdi:alert-outline", "warning/highCurrentDischarge", "1", "0");
Expand Down
16 changes: 14 additions & 2 deletions src/PylontechCanReceiver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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->setDischargeCurrentLimit(this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1), millis());
_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->getDischargeCurrentLimit());
MessageOutput.printf("[Pylontech] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f dischargeVoltageLimitation: %f\r\n",
_stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->getDischargeCurrentLimit(),
_stats->_dischargeVoltageLimitation);
}
break;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -155,6 +164,7 @@ void PylontechCanReceiver::dummyData()
_stats->_chargeVoltage = dummyFloat(50);
_stats->_chargeCurrentLimitation = dummyFloat(33);
_stats->setDischargeCurrentLimit(dummyFloat(12), millis());
_stats->_dischargeVoltageLimitation = dummyFloat(46);
_stats->_stateOfHealth = 99;
_stats->setVoltage(48.67, millis());
_stats->setCurrent(dummyFloat(-1), 1/*precision*/, millis());
Expand All @@ -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;
Expand Down

0 comments on commit 191cc80

Please sign in to comment.