mirror of
https://github.com/esphome/esphome.git
synced 2026-02-06 01:22:47 +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/helpers.h"
|
||||||
#include "esphome/core/log.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 esphome {
|
||||||
namespace pylontech {
|
namespace pylontech {
|
||||||
|
|
||||||
@@ -64,33 +86,106 @@ void PylontechComponent::loop() {
|
|||||||
void PylontechComponent::process_line_(std::string &buffer) {
|
void PylontechComponent::process_line_(std::string &buffer) {
|
||||||
ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str());
|
ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str());
|
||||||
// clang-format off
|
// 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
|
// 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 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
|
// clang-format on
|
||||||
|
|
||||||
PylontechListener::LineContents l{};
|
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) {
|
const char *cursor = buffer.c_str();
|
||||||
ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str());
|
char token_buf[TEXT_SENSOR_MAX_LEN] = {0};
|
||||||
return;
|
|
||||||
|
// 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) {
|
PARSE_INT(l.volt, "Volt");
|
||||||
ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str());
|
PARSE_INT(l.curr, "Curr");
|
||||||
return;
|
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);
|
PARSE_INT(l.thigh, "Thigh");
|
||||||
if (mostempr_parsed.has_value()) {
|
if (this->has_tlow_id_) {
|
||||||
l.mostempr = mostempr_parsed.value();
|
get_token(token_buf); // Skip Thigh.Id
|
||||||
} else {
|
|
||||||
l.mostempr = -300;
|
|
||||||
ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num);
|
|
||||||
}
|
}
|
||||||
|
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_) {
|
for (PylontechListener *listener : this->listeners_) {
|
||||||
listener->on_line_read(&l);
|
listener->on_line_read(&l);
|
||||||
@@ -101,3 +196,6 @@ float PylontechComponent::get_setup_priority() const { return setup_priority::DA
|
|||||||
|
|
||||||
} // namespace pylontech
|
} // namespace pylontech
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
|
#undef PARSE_INT
|
||||||
|
#undef PARSE_STR
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ namespace esphome {
|
|||||||
namespace pylontech {
|
namespace pylontech {
|
||||||
|
|
||||||
static const uint8_t NUM_BUFFERS = 20;
|
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 {
|
class PylontechListener {
|
||||||
public:
|
public:
|
||||||
struct LineContents {
|
struct LineContents {
|
||||||
int bat_num = 0, volt, curr, tempr, tlow, thigh, vlow, vhigh, coulomb, mostempr;
|
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],
|
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];
|
temp_st[TEXT_SENSOR_MAX_LEN] = {0};
|
||||||
};
|
};
|
||||||
|
|
||||||
virtual void on_line_read(LineContents *line);
|
virtual void on_line_read(LineContents *line);
|
||||||
@@ -45,6 +45,7 @@ class PylontechComponent : public PollingComponent, public uart::UARTDevice {
|
|||||||
std::string buffer_[NUM_BUFFERS];
|
std::string buffer_[NUM_BUFFERS];
|
||||||
int buffer_index_write_ = 0;
|
int buffer_index_write_ = 0;
|
||||||
int buffer_index_read_ = 0;
|
int buffer_index_read_ = 0;
|
||||||
|
bool has_tlow_id_ = false;
|
||||||
|
|
||||||
std::vector<PylontechListener *> listeners_{};
|
std::vector<PylontechListener *> listeners_{};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user