[pylontech] Refactor parser to support new firmware version and SysError (#12300)

This commit is contained in:
functionpointer
2026-02-04 17:06:59 +01:00
committed by GitHub
parent ba18a8b3e3
commit 36f2654fa6
2 changed files with 122 additions and 23 deletions

View File

@@ -2,6 +2,28 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// Helper macros
#define PARSE_INT(field, field_name) \
{ \
get_token(token_buf); \
auto val = parse_number<int>(token_buf); \
if (val.has_value()) { \
(field) = val.value(); \
} else { \
ESP_LOGD(TAG, "invalid " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \
return; \
} \
}
#define PARSE_STR(field, field_name) \
{ \
get_token(field); \
if (strlen(field) < 2) { \
ESP_LOGD(TAG, "too short " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \
return; \
} \
}
namespace esphome {
namespace pylontech {
@@ -64,33 +86,106 @@ void PylontechComponent::loop() {
void PylontechComponent::process_line_(std::string &buffer) {
ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str());
// clang-format off
// example line to parse:
// Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St
// 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal
// example lines to parse:
// Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St
// 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal
// 1 46012 1255 9100 5300 5500 3047 3091 SysError Low Normal Normal 4% 2025-11-28 17:56:33 Low Normal 7800 Normal
// newer firmware example:
// Power Volt Curr Tempr Tlow Tlow.Id Thigh Thigh.Id Vlow Vlow.Id Vhigh Vhigh.Id Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St SysAlarm.St
// 1 49405 0 17600 13700 8 14500 0 3293 2 3294 0 Idle Normal Normal Normal 60% 2025-12-05 00:53:41 Normal Normal 16600 Normal Normal
// clang-format on
PylontechListener::LineContents l{};
char mostempr_s[6];
const int parsed = sscanf( // NOLINT
buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %5s %*s", // NOLINT
&l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT
l.curr_st, l.temp_st, &l.coulomb, mostempr_s); // NOLINT
if (l.bat_num <= 0) {
ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str());
return;
const char *cursor = buffer.c_str();
char token_buf[TEXT_SENSOR_MAX_LEN] = {0};
// Helper Lambda to extract tokens
auto get_token = [&](char *token_buf) -> void {
// Skip leading whitespace
while (*cursor == ' ' || *cursor == '\t') {
cursor++;
}
if (*cursor == '\0') {
token_buf[0] = 0;
return;
}
const char *start = cursor;
// Find end of field
while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' && *cursor != '\r') {
cursor++;
}
size_t token_len = std::min(static_cast<size_t>(cursor - start), static_cast<size_t>(TEXT_SENSOR_MAX_LEN - 1));
memcpy(token_buf, start, token_len);
token_buf[token_len] = 0;
};
{
get_token(token_buf);
auto val = parse_number<int>(token_buf);
if (val.has_value() && val.value() > 0) {
l.bat_num = val.value();
} else if (strcmp(token_buf, "Power") == 0) {
// header line i.e. "Power Volt Curr" and so on
this->has_tlow_id_ = buffer.find("Tlow.Id") != std::string::npos;
ESP_LOGD(TAG, "header line %s Tlow.Id: %s", this->has_tlow_id_ ? "with" : "without",
buffer.substr(0, buffer.size() - 2).c_str());
return;
} else {
ESP_LOGD(TAG, "unknown line %s", buffer.substr(0, buffer.size() - 2).c_str());
return;
}
}
if (parsed != 14) {
ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str());
return;
PARSE_INT(l.volt, "Volt");
PARSE_INT(l.curr, "Curr");
PARSE_INT(l.tempr, "Tempr");
PARSE_INT(l.tlow, "Tlow");
if (this->has_tlow_id_) {
get_token(token_buf); // Skip Tlow.Id
}
auto mostempr_parsed = parse_number<int>(mostempr_s);
if (mostempr_parsed.has_value()) {
l.mostempr = mostempr_parsed.value();
} else {
l.mostempr = -300;
ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num);
PARSE_INT(l.thigh, "Thigh");
if (this->has_tlow_id_) {
get_token(token_buf); // Skip Thigh.Id
}
PARSE_INT(l.vlow, "Vlow");
if (this->has_tlow_id_) {
get_token(token_buf); // Skip Vlow.Id
}
PARSE_INT(l.vhigh, "Vhigh");
if (this->has_tlow_id_) {
get_token(token_buf); // Skip Vhigh.Id
}
PARSE_STR(l.base_st, "Base.St");
PARSE_STR(l.volt_st, "Volt.St");
PARSE_STR(l.curr_st, "Curr.St");
PARSE_STR(l.temp_st, "Temp.St");
{
get_token(token_buf);
for (char &i : token_buf) {
if (i == '%') {
i = 0;
break;
}
}
auto coul_val = parse_number<int>(token_buf);
if (coul_val.has_value()) {
l.coulomb = coul_val.value();
} else {
ESP_LOGD(TAG, "invalid Coulomb in line %s", buffer.substr(0, buffer.size() - 2).c_str());
return;
}
}
get_token(token_buf); // Skip Date
get_token(token_buf); // Skip Time
get_token(token_buf); // Skip B.V.St
get_token(token_buf); // Skip B.T.St
PARSE_INT(l.mostempr, "Mostempr");
ESP_LOGD(TAG, "successful line %s", buffer.substr(0, buffer.size() - 2).c_str());
for (PylontechListener *listener : this->listeners_) {
listener->on_line_read(&l);
@@ -101,3 +196,6 @@ float PylontechComponent::get_setup_priority() const { return setup_priority::DA
} // namespace pylontech
} // namespace esphome
#undef PARSE_INT
#undef PARSE_STR

View File

@@ -8,14 +8,14 @@ namespace esphome {
namespace pylontech {
static const uint8_t NUM_BUFFERS = 20;
static const uint8_t TEXT_SENSOR_MAX_LEN = 8;
static const uint8_t TEXT_SENSOR_MAX_LEN = 14;
class PylontechListener {
public:
struct LineContents {
int bat_num = 0, volt, curr, tempr, tlow, thigh, vlow, vhigh, coulomb, mostempr;
char base_st[TEXT_SENSOR_MAX_LEN], volt_st[TEXT_SENSOR_MAX_LEN], curr_st[TEXT_SENSOR_MAX_LEN],
temp_st[TEXT_SENSOR_MAX_LEN];
char base_st[TEXT_SENSOR_MAX_LEN] = {0}, volt_st[TEXT_SENSOR_MAX_LEN] = {0}, curr_st[TEXT_SENSOR_MAX_LEN] = {0},
temp_st[TEXT_SENSOR_MAX_LEN] = {0};
};
virtual void on_line_read(LineContents *line);
@@ -45,6 +45,7 @@ class PylontechComponent : public PollingComponent, public uart::UARTDevice {
std::string buffer_[NUM_BUFFERS];
int buffer_index_write_ = 0;
int buffer_index_read_ = 0;
bool has_tlow_id_ = false;
std::vector<PylontechListener *> listeners_{};
};