ESPHome 2026.5.0b1
Loading...
Searching...
No Matches
pylontech.cpp
Go to the documentation of this file.
1#include "pylontech.h"
3#include "esphome/core/log.h"
4
5// Helper macros
6#define PARSE_INT(field, field_name) \
7 { \
8 get_token(token_buf); \
9 auto val = parse_number<int>(token_buf); \
10 if (val.has_value()) { \
11 (field) = val.value(); \
12 } else { \
13 ESP_LOGD(TAG, "invalid " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \
14 return; \
15 } \
16 }
17
18#define PARSE_STR(field, field_name) \
19 { \
20 get_token(field); \
21 if (strlen(field) < 2) { \
22 ESP_LOGD(TAG, "too short " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \
23 return; \
24 } \
25 }
26
28
29static const char *const TAG = "pylontech";
30static const int MAX_DATA_LENGTH_BYTES = 256;
31static const uint8_t ASCII_LF = 0x0A;
32
34
37 ESP_LOGCONFIG(TAG, "pylontech:");
38 if (this->is_failed()) {
39 ESP_LOGE(TAG, "Connection with pylontech failed!");
40 }
41
42 for (PylontechListener *listener : this->listeners_) {
43 listener->dump_config();
44 }
45
46 LOG_UPDATE_INTERVAL(this);
47}
48
50 while (this->available() != 0) {
51 this->read();
52 }
53}
54
55void PylontechComponent::update() { this->write_str("pwr\n"); }
56
58 size_t avail = this->available();
59 if (avail > 0) {
60 // pylontech sends a lot of data very suddenly
61 // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
62 int recv = 0;
63 uint8_t buf[64];
64 while (avail > 0) {
65 size_t to_read = std::min(avail, sizeof(buf));
66 if (!this->read_array(buf, to_read)) {
67 break;
68 }
69 avail -= to_read;
70 recv += to_read;
71
72 for (size_t i = 0; i < to_read; i++) {
73 buffer_[buffer_index_write_] += (char) buf[i];
74 if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
75 // complete line received
76 buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
77 }
78 }
79 }
80 ESP_LOGV(TAG, "received %d bytes", recv);
81 } else {
82 // only process one line per call of loop() to not block esphome for too long
86 buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS;
87 }
88 }
89}
90
91void PylontechComponent::process_line_(std::string &buffer) {
92 ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str());
93 // clang-format off
94 // example lines to parse:
95 // 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
96 // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal
97 // 1 46012 1255 9100 5300 5500 3047 3091 SysError Low Normal Normal 4% 2025-11-28 17:56:33 Low Normal 7800 Normal
98 // newer firmware example:
99 // 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
100 // 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
101 // clang-format on
102
104
105 const char *cursor = buffer.c_str();
106 char token_buf[TEXT_SENSOR_MAX_LEN] = {0};
107
108 // Helper Lambda to extract tokens
109 auto get_token = [&](char *token_buf) -> void {
110 // Skip leading whitespace
111 while (*cursor == ' ' || *cursor == '\t') {
112 cursor++;
113 }
114
115 if (*cursor == '\0') {
116 token_buf[0] = 0;
117 return;
118 }
119
120 const char *start = cursor;
121
122 // Find end of field
123 while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' && *cursor != '\r') {
124 cursor++;
125 }
126
127 size_t token_len = std::min(static_cast<size_t>(cursor - start), static_cast<size_t>(TEXT_SENSOR_MAX_LEN - 1));
128 memcpy(token_buf, start, token_len);
129 token_buf[token_len] = 0;
130 };
131
132 {
133 get_token(token_buf);
134 auto val = parse_number<int>(token_buf);
135 if (val.has_value() && val.value() > 0) {
136 l.bat_num = val.value();
137 } else if (strcmp(token_buf, "Power") == 0) {
138 // header line i.e. "Power Volt Curr" and so on
139 this->has_tlow_id_ = buffer.find("Tlow.Id") != std::string::npos;
140 ESP_LOGD(TAG, "header line %s Tlow.Id: %s", this->has_tlow_id_ ? "with" : "without",
141 buffer.substr(0, buffer.size() - 2).c_str());
142 return;
143 } else {
144 ESP_LOGD(TAG, "unknown line %s", buffer.substr(0, buffer.size() - 2).c_str());
145 return;
146 }
147 }
148 PARSE_INT(l.volt, "Volt");
149 PARSE_INT(l.curr, "Curr");
150 PARSE_INT(l.tempr, "Tempr");
151 PARSE_INT(l.tlow, "Tlow");
152 if (this->has_tlow_id_) {
153 get_token(token_buf); // Skip Tlow.Id
154 }
155 PARSE_INT(l.thigh, "Thigh");
156 if (this->has_tlow_id_) {
157 get_token(token_buf); // Skip Thigh.Id
158 }
159 PARSE_INT(l.vlow, "Vlow");
160 if (this->has_tlow_id_) {
161 get_token(token_buf); // Skip Vlow.Id
162 }
163 PARSE_INT(l.vhigh, "Vhigh");
164 if (this->has_tlow_id_) {
165 get_token(token_buf); // Skip Vhigh.Id
166 }
167 PARSE_STR(l.base_st, "Base.St");
168 PARSE_STR(l.volt_st, "Volt.St");
169 PARSE_STR(l.curr_st, "Curr.St");
170 PARSE_STR(l.temp_st, "Temp.St");
171 {
172 get_token(token_buf);
173 for (char &i : token_buf) {
174 if (i == '%') {
175 i = 0;
176 break;
177 }
178 }
179 auto coul_val = parse_number<int>(token_buf);
180 if (coul_val.has_value()) {
181 l.coulomb = coul_val.value();
182 } else {
183 ESP_LOGD(TAG, "invalid Coulomb in line %s", buffer.substr(0, buffer.size() - 2).c_str());
184 return;
185 }
186 }
187 get_token(token_buf); // Skip Date
188 get_token(token_buf); // Skip Time
189 get_token(token_buf); // Skip B.V.St
190 get_token(token_buf); // Skip B.T.St
191 PARSE_INT(l.mostempr, "Mostempr");
192
193 ESP_LOGD(TAG, "successful line %s", buffer.substr(0, buffer.size() - 2).c_str());
194
195 for (PylontechListener *listener : this->listeners_) {
196 listener->on_line_read(&l);
197 }
198}
199
200} // namespace esphome::pylontech
201
202#undef PARSE_INT
203#undef PARSE_STR
uint8_t l
Definition bl0906.h:0
bool is_failed() const
Definition component.h:284
void loop() override
Read data once available.
Definition pylontech.cpp:57
std::vector< PylontechListener * > listeners_
Definition pylontech.h:47
void process_line_(std::string &buffer)
Definition pylontech.cpp:91
void update() override
Schedule data readings.
Definition pylontech.cpp:55
void setup() override
Setup the sensor and test for a connection.
Definition pylontech.cpp:49
std::string buffer_[NUM_BUFFERS]
Definition pylontech.h:42
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:38
void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits=1, UARTParityOptions parity=UART_CONFIG_PARITY_NONE, uint8_t data_bits=8)
Check that the configuration of the UART bus matches the provided values and otherwise print a warnin...
Definition uart.cpp:16
void write_str(const char *str)
Definition uart.h:32
mopeka_std_values val[3]
optional< T > parse_number(const char *str)
Parse an unsigned decimal number from a null-terminated string.
Definition helpers.h:1143
uint16_t length
Definition tt21100.cpp:0