Skip to content

Commit

Permalink
Add additional Pylontech CAN protocol fields
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 committed Sep 4, 2024
1 parent dcc8313 commit 3d52a4b
Show file tree
Hide file tree
Showing 4 changed files with 21 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 @@ -104,6 +104,7 @@ class PylontechBatteryStats : public BatteryStats {
float _chargeVoltage;
float _chargeCurrentLimitation;
float _dischargeCurrentLimitation;
float _dischargeVoltageLimitation;
uint16_t _stateOfHealth;
float _temperature;

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

uint8_t _moduleCount;
};

class PytesBatteryStats : public BatteryStats {
Expand Down
3 changes: 3 additions & 0 deletions src/BatteryStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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));
Expand Down
1 change: 1 addition & 0 deletions src/MqttHandleBatteryHass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
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->_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;
}
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->_dischargeCurrentLimitation = dummyFloat(12);
_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 3d52a4b

Please sign in to comment.