Feature: parse additional Pylontech CAN protocol fields (#1213)

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
This commit is contained in:
ranma 2024-09-25 14:45:52 +02:00 committed by GitHub
parent 2265992836
commit 191cc8007d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 23 additions and 2 deletions

View File

@ -118,6 +118,7 @@ class PylontechBatteryStats : public BatteryStats {
float _chargeVoltage;
float _chargeCurrentLimitation;
float _dischargeVoltageLimitation;
uint16_t _stateOfHealth;
float _temperature;
@ -140,6 +141,8 @@ class PylontechBatteryStats : public BatteryStats {
bool _chargeEnabled;
bool _dischargeEnabled;
bool _chargeImmediately;
uint8_t _moduleCount;
};
class SBSBatteryStats : public BatteryStats {

View File

@ -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"));
@ -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));
@ -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

View File

@ -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");

View File

@ -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;
}
@ -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->setDischargeCurrentLimit(dummyFloat(12), millis());
_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;