mirror of
https://github.com/esphome/esphome.git
synced 2026-02-05 10:20:11 +08:00
[pylontech] Refactor parser to support new firmware version and SysError (#12300)
This commit is contained in:
@@ -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:
|
||||
// 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());
|
||||
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;
|
||||
}
|
||||
if (parsed != 14) {
|
||||
ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str());
|
||||
return;
|
||||
|
||||
const char *start = cursor;
|
||||
|
||||
// Find end of field
|
||||
while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' && *cursor != '\r') {
|
||||
cursor++;
|
||||
}
|
||||
auto mostempr_parsed = parse_number<int>(mostempr_s);
|
||||
if (mostempr_parsed.has_value()) {
|
||||
l.mostempr = mostempr_parsed.value();
|
||||
|
||||
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 {
|
||||
l.mostempr = -300;
|
||||
ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num);
|
||||
ESP_LOGD(TAG, "unknown line %s", 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
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user