ESPHome 2026.10.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
28
29static const char *const TAG = "pylontech";
30static const int MAX_DATA_LENGTH_BYTES = 256;
31static const uint8_t ASCII_LF = 0x0A;
32
34
36 ESP_LOGCONFIG(TAG, "pylontech:");
37 if (this->is_failed()) {
38 ESP_LOGE(TAG, "Connection with pylontech failed!");
39 }
40
41 for (PylontechListener *listener : this->listeners_) {
42 listener->dump_config();
43 }
44
45 LOG_UPDATE_INTERVAL(this);
46}
47
49 while (this->available() != 0) {
50 this->read();
51 }
52}
53
54void PylontechComponent::update() { this->write_str("pwr\n"); }
55
57 size_t avail = this->available();
58 if (avail > 0) {
59 // pylontech sends a lot of data very suddenly
60 // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
61 int recv = 0;
62 uint8_t buf[64];
63 while (avail > 0) {
64 size_t to_read = std::min(avail, sizeof(buf));
65 if (!this->read_array(buf, to_read)) {
66 break;
67 }
68 avail -= to_read;
69 recv += to_read;
70
71 for (size_t i = 0; i < to_read; i++) {
72 buffer_[buffer_index_write_] += (char) buf[i];
73 if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
74 // complete line received
75 buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
76 }
77 }
78 }
79 ESP_LOGV(TAG, "received %d bytes", recv);
80 } else {
81 // only process one line per call of loop() to not block esphome for too long
85 buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS;
86 }
87 }
88}
89
90void PylontechComponent::process_line_(std::string &buffer) {
91 ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str());
92 // clang-format off
93 // example lines to parse:
94 // 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
95 // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal
96 // 1 46012 1255 9100 5300 5500 3047 3091 SysError Low Normal Normal 4% 2025-11-28 17:56:33 Low Normal 7800 Normal
97 // newer firmware example:
98 // 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
99 // 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
100 // clang-format on
101
103
104 const char *cursor = buffer.c_str();
105 char token_buf[TEXT_SENSOR_MAX_LEN] = {0};
106
107 // Helper Lambda to extract tokens
108 auto get_token = [&](char *token_buf) -> void {
109 // Skip leading whitespace
110 while (*cursor == ' ' || *cursor == '\t') {
111 cursor++;
112 }
113
114 if (*cursor == '\0') {
115 token_buf[0] = 0;
116 return;
117 }
118
119 const char *start = cursor;
120
121 // Find end of field
122 while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' && *cursor != '\r') {
123 cursor++;
124 }
125
126 size_t token_len = std::min(static_cast<size_t>(cursor - start), static_cast<size_t>(TEXT_SENSOR_MAX_LEN - 1));
127 memcpy(token_buf, start, token_len);
128 token_buf[token_len] = 0;
129 };
130
131 {
132 get_token(token_buf);
133 auto val = parse_number<int>(token_buf);
134 if (val.has_value() && val.value() > 0) {
135 l.bat_num = val.value();
136 } else if (strcmp(token_buf, "Power") == 0) {
137 // header line i.e. "Power Volt Curr" and so on
138 this->has_tlow_id_ = buffer.find("Tlow.Id") != std::string::npos;
139 ESP_LOGD(TAG, "header line %s Tlow.Id: %s",
140 this->has_tlow_id_ ? LOG_STR_LITERAL("with") : LOG_STR_LITERAL("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:272
void loop() override
Read data once available.
Definition pylontech.cpp:56
std::vector< PylontechListener * > listeners_
Definition pylontech.h:47
void process_line_(std::string &buffer)
Definition pylontech.cpp:90
void update() override
Schedule data readings.
Definition pylontech.cpp:54
void setup() override
Setup the sensor and test for a connection.
Definition pylontech.cpp:48
std::string buffer_[NUM_BUFFERS]
Definition pylontech.h:42
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:39
void write_str(const char *str)
Definition uart.h:33
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:1179
uint16_t length
Definition tt21100.cpp:0