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