11#define PARSE_TOKEN_INT(field, field_name) \
13 auto val = parse_number<int>(token_buf); \
14 if (val.has_value() && in_range<decltype(field)>(*val)) { \
17 ESP_LOGV(TAG, "invalid " field_name " in line %s", buffer); \
18 this->reject_line_(); \
23#define PARSE_INT(field, field_name) \
25 get_token(cursor, token_buf); \
26 PARSE_TOKEN_INT(field, field_name) \
29#define PARSE_STR(field, field_name) \
31 get_token(cursor, field); \
32 if (strlen(field) < 2) { \
33 ESP_LOGV(TAG, "too short " field_name " in line %s", buffer); \
34 this->reject_line_(); \
43#ifdef PYLONTECH_CELL_BATTERY_COUNT
45static constexpr uint32_t BAT_COMMAND_DELAY_MS = 20;
49template<
typename T>
static bool in_range(
int value) {
50 if constexpr (
sizeof(T) >=
sizeof(int)) {
53 return value >= std::numeric_limits<T>::min() && value <= std::numeric_limits<T>::max();
57#ifdef PYLONTECH_LISTENER_COUNT
59static void get_token(
const char *&cursor,
char *token_buf) {
61 while (*cursor ==
' ' || *cursor ==
'\t') {
65 if (*cursor ==
'\0') {
70 const char *start = cursor;
73 while (*cursor !=
'\0' && *cursor !=
' ' && *cursor !=
'\t') {
77 size_t token_len = std::min(
static_cast<size_t>(cursor - start),
static_cast<size_t>(TEXT_SENSOR_MAX_LEN - 1));
78 memcpy(token_buf, start, token_len);
79 token_buf[token_len] = 0;
83static uint8_t slot_index(
battery_mask_t batteries, uint8_t bat_index) {
84 return __builtin_popcount(batteries & ((1u << bat_index) - 1u));
91 ESP_LOGCONFIG(TAG,
"pylontech:");
93#ifdef PYLONTECH_LISTENER_COUNT
95 listener->dump_config();
99 LOG_UPDATE_INTERVAL(
this);
105#ifdef PYLONTECH_CELL_BATTERY_COUNT
117#ifdef PYLONTECH_LISTENER_COUNT
119#ifdef PYLONTECH_CELL_BATTERY_COUNT
125 snprintf(cmd,
sizeof(cmd),
"bat %u\n", this->
bat_reply_);
138 size_t to_read = std::min(avail,
sizeof(buf));
143#ifdef PYLONTECH_LISTENER_COUNT
144 for (
size_t i = 0; i < to_read; i++) {
146 if (line !=
nullptr) {
152 if (strncmp(partial,
"Power", 5) == 0) {
161#ifdef PYLONTECH_LISTENER_COUNT
163 for (uint8_t i = 0; this->
pending_ != 0 && i < PYLONTECH_BATTERY_COUNT; i++) {
170 ESP_LOGV(TAG,
"Publishing battery %u",
l->bat_num);
172 if (listener->get_battery() ==
l->bat_num) {
173 listener->on_line_read(
l);
178#ifdef PYLONTECH_CELL_BATTERY_COUNT
183 ESP_LOGV(TAG,
"Publishing cells of battery %u", bat_index + 1);
185 if (listener->get_battery() == bat_index + 1) {
193 ESP_LOGW(TAG,
"Rejected %u malformed lines", this->
rejected_);
200#ifdef PYLONTECH_CELL_BATTERY_COUNT
206 ESP_LOGV(TAG,
"header line %s Tlow.Id: %s", this->
has_tlow_id_ ? LOG_STR_LITERAL(
"with") : LOG_STR_LITERAL(
"without"),
212#ifdef PYLONTECH_CELL_BATTERY_COUNT
213 if (strcmp(
buffer,
"$$") == 0) {
241 const char *cursor =
buffer;
242 char token_buf[TEXT_SENSOR_MAX_LEN] = {0};
244 get_token(cursor, token_buf);
246 if (!bat.has_value()) {
247 if (strcmp(token_buf,
"Power") == 0) {
251 ESP_LOGV(TAG,
"unknown line %s",
buffer);
255#ifdef PYLONTECH_CELL_BATTERY_COUNT
257 ESP_LOGV(TAG,
"dropped late line %s",
buffer);
261 const unsigned bat_index =
static_cast<unsigned>(*bat) - 1u;
262 if (bat_index >= MAX_BATTERIES || (this->
batteries_ & (1u << bat_index)) == 0) {
271 PARSE_INT(
l.volt,
"Volt");
272 PARSE_INT(
l.curr,
"Curr");
273 PARSE_INT(
l.tempr,
"Tempr");
274 PARSE_INT(
l.tlow,
"Tlow");
276 get_token(cursor, token_buf);
278 PARSE_INT(
l.thigh,
"Thigh");
280 get_token(cursor, token_buf);
282 PARSE_INT(
l.vlow,
"Vlow");
284 get_token(cursor, token_buf);
286 PARSE_INT(
l.vhigh,
"Vhigh");
288 get_token(cursor, token_buf);
290 PARSE_STR(
l.base_st,
"Base.St");
291 PARSE_STR(
l.volt_st,
"Volt.St");
292 PARSE_STR(
l.curr_st,
"Curr.St");
293 PARSE_STR(
l.temp_st,
"Temp.St");
294 get_token(cursor, token_buf);
295 if (
char *pct = strchr(token_buf,
'%')) {
298 PARSE_TOKEN_INT(
l.coulomb,
"Coulomb");
299 get_token(cursor, token_buf);
300 get_token(cursor, token_buf);
301 get_token(cursor, token_buf);
302 get_token(cursor, token_buf);
303 PARSE_INT(
l.mostempr,
"Mostempr");
308#ifdef PYLONTECH_CELL_BATTERY_COUNT
319 const uint8_t bat_index = __builtin_ctz(remaining);
337 const char *cursor =
buffer;
338 char token_buf[TEXT_SENSOR_MAX_LEN] = {0};
340 get_token(cursor, token_buf);
342 if (!cell.has_value() || *cell < 0 || *cell >= MAX_CELLS) {
344 ESP_LOGV(TAG,
"unknown line %s",
buffer);
348 PARSE_INT(c.volt,
"Volt");
349 get_token(cursor, token_buf);
351 PARSE_INT(tempr,
"Tempr");
353 if (!in_range<int16_t>(tempr)) {
354 ESP_LOGV(TAG,
"invalid Tempr in line %s",
buffer);
369#undef PARSE_TOKEN_INT
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
const char * partial()
The partial line, NUL terminated, for prompts with no line end; the first N - 1 characters on the byt...
bool overflowed() const
True only right after the feed() that found the line too long; the rest of that line is then dropped.
char * feed(uint8_t c)
Feed one byte; returns the completed line (empty lines included), valid until the next call,...
void log_if_overflowed(const char *tag) const
Log the shared "line dropped" warning when the last feed() overflowed.
StaticVector< PylontechListener *, PYLONTECH_LISTENER_COUNT > listeners_
void process_line_(const char *buffer)
void loop() override
Read data once available.
bool cell_polling_enabled_
battery_mask_t cells_pending_
void parse_header_(const char *buffer)
battery_mask_t batteries_
std::array< PylontechListener::CellArray, PYLONTECH_CELL_BATTERY_COUNT > cell_slots_
battery_mask_t cell_batteries_
void update() override
Schedule data readings.
LineReader< 200 > line_reader_
void setup() override
Setup the sensor and test for a connection.
std::array< cell_mask_t, PYLONTECH_CELL_BATTERY_COUNT > cells_read_
std::array< PylontechListener::LineContents, PYLONTECH_BATTERY_COUNT > slots_
void dump_config() override
void parse_cell_line_(const char *buffer)
optional< std::array< uint8_t, N > > read_array()
void discard_available()
Read and drop every byte waiting in the receive buffer.
void write_str(const char *str)
ESPHOME_LOG_TAG(TAG, "pylontech")
optional< T > parse_number(const char *str)
Parse an unsigned decimal number from a null-terminated string.
Application App
Global storage of Application pointer - only one Application can exist.