mirror of
https://github.com/esphome/esphome.git
synced 2026-02-10 17:51:53 +00:00
Compare commits
17 Commits
beta_preme
...
optimize_b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a932ea343d | ||
|
|
bab8d1e8b2 | ||
|
|
e3141211c3 | ||
|
|
e85a022c77 | ||
|
|
1c3af30299 | ||
|
|
5caed68cd9 | ||
|
|
b97a728cf1 | ||
|
|
dcbb020479 | ||
|
|
87ac263264 | ||
|
|
097901e9c8 | ||
|
|
01a90074ba | ||
|
|
57b85a8400 | ||
|
|
2edfcf278f | ||
|
|
bcd4a9fc39 | ||
|
|
78df8be31f | ||
|
|
dacc557a16 | ||
|
|
3767c5ec91 |
@@ -1155,9 +1155,11 @@ enum WaterHeaterCommandHasField {
|
||||
WATER_HEATER_COMMAND_HAS_NONE = 0;
|
||||
WATER_HEATER_COMMAND_HAS_MODE = 1;
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
|
||||
WATER_HEATER_COMMAND_HAS_STATE = 4;
|
||||
WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true];
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
|
||||
WATER_HEATER_COMMAND_HAS_ON_STATE = 32;
|
||||
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64;
|
||||
}
|
||||
|
||||
message WaterHeaterCommandRequest {
|
||||
|
||||
@@ -1343,8 +1343,12 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
|
||||
call.set_target_temperature_low(msg.target_temperature_low);
|
||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
|
||||
call.set_target_temperature_high(msg.target_temperature_high);
|
||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
|
||||
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) ||
|
||||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
|
||||
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
|
||||
}
|
||||
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) ||
|
||||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
|
||||
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
|
||||
}
|
||||
call.perform();
|
||||
@@ -1895,10 +1899,6 @@ bool APIConnection::schedule_batch_() {
|
||||
}
|
||||
|
||||
void APIConnection::process_batch_() {
|
||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
if (this->deferred_batch_.empty()) {
|
||||
this->flags_.batch_scheduled = false;
|
||||
return;
|
||||
@@ -1923,6 +1923,10 @@ void APIConnection::process_batch_() {
|
||||
for (size_t i = 0; i < num_items; i++) {
|
||||
total_estimated_size += this->deferred_batch_[i].estimated_size;
|
||||
}
|
||||
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
|
||||
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
|
||||
total_estimated_size = MAX_BATCH_PACKET_SIZE;
|
||||
}
|
||||
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
|
||||
|
||||
@@ -1946,7 +1950,20 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||
// Multi-message path — heavy stack frame isolated in separate noinline function
|
||||
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
|
||||
}
|
||||
|
||||
// Separated from process_batch_() so the single-message fast path gets a minimal
|
||||
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
|
||||
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||
uint8_t footer_size) {
|
||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||
const uint8_t frame_overhead = header_padding + footer_size;
|
||||
|
||||
// Stack-allocated array for message info
|
||||
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
||||
@@ -1973,7 +1990,7 @@ void APIConnection::process_batch_() {
|
||||
|
||||
// Message was encoded successfully
|
||||
// payload_size is header_padding + actual payload size + footer_size
|
||||
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
|
||||
uint16_t proto_payload_size = payload_size - frame_overhead;
|
||||
// Use placement new to construct MessageInfo in pre-allocated stack array
|
||||
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
||||
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
||||
@@ -1989,42 +2006,38 @@ void APIConnection::process_batch_() {
|
||||
current_offset = shared_buf.size() + footer_size;
|
||||
}
|
||||
|
||||
if (items_processed == 0) {
|
||||
this->deferred_batch_.clear();
|
||||
return;
|
||||
}
|
||||
if (items_processed > 0) {
|
||||
// Add footer space for the last message (for Noise protocol MAC)
|
||||
if (footer_size > 0) {
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Add footer space for the last message (for Noise protocol MAC)
|
||||
if (footer_size > 0) {
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Send all collected messages
|
||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const MessageInfo>(message_info, items_processed));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||
}
|
||||
// Send all collected messages
|
||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const MessageInfo>(message_info, items_processed));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||
}
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Log messages after send attempt for VV debugging
|
||||
// It's safe to use the buffer for logging at this point regardless of send result
|
||||
for (size_t i = 0; i < items_processed; i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
this->log_batch_item_(item);
|
||||
}
|
||||
// Log messages after send attempt for VV debugging
|
||||
// It's safe to use the buffer for logging at this point regardless of send result
|
||||
for (size_t i = 0; i < items_processed; i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
this->log_batch_item_(item);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Handle remaining items more efficiently
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
// Remove processed items from the beginning
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
// Reschedule for remaining items
|
||||
this->schedule_batch_();
|
||||
} else {
|
||||
// All items processed
|
||||
this->clear_batch_();
|
||||
// Partial batch — remove processed items and reschedule
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
this->schedule_batch_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// All items processed (or none could be processed)
|
||||
this->clear_batch_();
|
||||
}
|
||||
|
||||
// Dispatch message encoding based on message_type
|
||||
|
||||
@@ -549,8 +549,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
batch_start_time = 0;
|
||||
}
|
||||
|
||||
// Remove processed items from the front
|
||||
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
|
||||
// Remove processed items from the front — noinline to keep memmove out of warm callers
|
||||
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
|
||||
|
||||
bool empty() const { return items.empty(); }
|
||||
size_t size() const { return items.size(); }
|
||||
@@ -622,6 +622,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
bool schedule_batch_();
|
||||
void process_batch_();
|
||||
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||
uint8_t footer_size) __attribute__((noinline));
|
||||
void clear_batch_() {
|
||||
this->deferred_batch_.clear();
|
||||
this->flags_.batch_scheduled = false;
|
||||
|
||||
@@ -147,6 +147,8 @@ enum WaterHeaterCommandHasField : uint32_t {
|
||||
WATER_HEATER_COMMAND_HAS_STATE = 4,
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
|
||||
WATER_HEATER_COMMAND_HAS_ON_STATE = 32,
|
||||
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64,
|
||||
};
|
||||
#ifdef USE_NUMBER
|
||||
enum NumberMode : uint32_t {
|
||||
|
||||
@@ -385,6 +385,10 @@ const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::Water
|
||||
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
|
||||
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
|
||||
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
|
||||
case enums::WATER_HEATER_COMMAND_HAS_ON_STATE:
|
||||
return "WATER_HEATER_COMMAND_HAS_ON_STATE";
|
||||
case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE:
|
||||
return "WATER_HEATER_COMMAND_HAS_AWAY_STATE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
@@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -22,13 +27,27 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
|
||||
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f},
|
||||
{35.5f, 55.4f}, {55.5f, 125.4f},
|
||||
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 9.1f},
|
||||
{9.1f, 35.5f},
|
||||
{35.5f, 55.5f},
|
||||
{55.5f, 125.5f},
|
||||
{125.5f, 225.5f},
|
||||
{225.5f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
|
||||
{155.0f, 254.0f}, {255.0f, 354.0f},
|
||||
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 55.0f},
|
||||
{55.0f, 155.0f},
|
||||
{155.0f, 255.0f},
|
||||
{255.0f, 355.0f},
|
||||
{355.0f, 425.0f},
|
||||
{425.0f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
@@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
if (in_range) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
@@ -12,7 +13,11 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -21,10 +26,24 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
|
||||
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
|
||||
// clang-format off
|
||||
{0.0f, 15.1f},
|
||||
{15.1f, 30.1f},
|
||||
{30.1f, 55.1f},
|
||||
{55.1f, 110.1f},
|
||||
{110.1f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
|
||||
// clang-format off
|
||||
{0.0f, 25.1f},
|
||||
{25.1f, 50.1f},
|
||||
{50.1f, 90.1f},
|
||||
{90.1f, 180.1f},
|
||||
{180.1f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
@@ -42,7 +61,10 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
if (in_range) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ void CSE7766Component::loop() {
|
||||
}
|
||||
|
||||
// Early return prevents updating last_transmission_ when no data is available.
|
||||
int avail = this->available();
|
||||
if (avail <= 0) {
|
||||
size_t avail = this->available();
|
||||
if (avail == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ void CSE7766Component::loop() {
|
||||
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
|
||||
uint8_t buf[CSE7766_RAW_DATA_SIZE];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -133,10 +133,10 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
|
||||
|
||||
void DFPlayer::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -28,15 +28,28 @@ void DlmsMeterComponent::dump_config() {
|
||||
|
||||
void DlmsMeterComponent::loop() {
|
||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
||||
while (this->available()) {
|
||||
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
|
||||
size_t avail = this->available();
|
||||
if (avail > 0) {
|
||||
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
||||
if (remaining == 0) {
|
||||
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
|
||||
break;
|
||||
} else {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
// Cap reads to remaining buffer capacity.
|
||||
if (avail > remaining) {
|
||||
avail = remaining;
|
||||
}
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
}
|
||||
uint8_t c;
|
||||
this->read_byte(&c);
|
||||
this->receive_buffer_.push_back(c);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
|
||||
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
|
||||
|
||||
@@ -40,9 +40,7 @@ bool Dsmr::ready_to_request_data_() {
|
||||
this->start_requesting_data_();
|
||||
}
|
||||
if (!this->requesting_data_) {
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->drain_rx_buffer_();
|
||||
}
|
||||
}
|
||||
return this->requesting_data_;
|
||||
@@ -115,138 +113,169 @@ void Dsmr::stop_requesting_data_() {
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Stop reading data from P1 port");
|
||||
}
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->drain_rx_buffer_();
|
||||
this->requesting_data_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::drain_rx_buffer_() {
|
||||
uint8_t buf[64];
|
||||
size_t avail;
|
||||
while ((avail = this->available()) > 0) {
|
||||
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::reset_telegram_() {
|
||||
this->header_found_ = false;
|
||||
this->footer_found_ = false;
|
||||
this->bytes_read_ = 0;
|
||||
this->crypt_bytes_read_ = 0;
|
||||
this->crypt_telegram_len_ = 0;
|
||||
this->last_read_time_ = 0;
|
||||
}
|
||||
|
||||
void Dsmr::receive_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
const char c = this->read();
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
|
||||
// Find a new telegram header, i.e. forward slash.
|
||||
if (c == '/') {
|
||||
ESP_LOGV(TAG, "Header of telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
if (!this->header_found_)
|
||||
continue;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
const char c = static_cast<char>(buf[i]);
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
// Find a new telegram header, i.e. forward slash.
|
||||
if (c == '/') {
|
||||
ESP_LOGV(TAG, "Header of telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
if (!this->header_found_)
|
||||
continue;
|
||||
|
||||
// Some v2.2 or v3 meters will send a new value which starts with '('
|
||||
// in a new line, while the value belongs to the previous ObisId. For
|
||||
// proper parsing, remove these new line characters.
|
||||
if (c == '(') {
|
||||
while (true) {
|
||||
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
||||
if (previous_char == '\n' || previous_char == '\r') {
|
||||
this->bytes_read_--;
|
||||
} else {
|
||||
break;
|
||||
// Check for buffer overflow.
|
||||
if (this->bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Some v2.2 or v3 meters will send a new value which starts with '('
|
||||
// in a new line, while the value belongs to the previous ObisId. For
|
||||
// proper parsing, remove these new line characters.
|
||||
if (c == '(') {
|
||||
while (true) {
|
||||
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
||||
if (previous_char == '\n' || previous_char == '\r') {
|
||||
this->bytes_read_--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->telegram_[this->bytes_read_] = c;
|
||||
this->bytes_read_++;
|
||||
|
||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||
if (c == '!') {
|
||||
ESP_LOGV(TAG, "Footer of telegram found");
|
||||
this->footer_found_ = true;
|
||||
continue;
|
||||
}
|
||||
// Check for the end of the hex checksum, i.e. a newline.
|
||||
if (this->footer_found_ && c == '\n') {
|
||||
// Parse the telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->telegram_[this->bytes_read_] = c;
|
||||
this->bytes_read_++;
|
||||
|
||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||
if (c == '!') {
|
||||
ESP_LOGV(TAG, "Footer of telegram found");
|
||||
this->footer_found_ = true;
|
||||
continue;
|
||||
}
|
||||
// Check for the end of the hex checksum, i.e. a newline.
|
||||
if (this->footer_found_ && c == '\n') {
|
||||
// Parse the telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::receive_encrypted_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
const char c = this->read();
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
|
||||
// Find a new telegram start byte.
|
||||
if (!this->header_found_) {
|
||||
if ((uint8_t) c != 0xDB) {
|
||||
continue;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
const char c = static_cast<char>(buf[i]);
|
||||
|
||||
// Find a new telegram start byte.
|
||||
if (!this->header_found_) {
|
||||
if ((uint8_t) c != 0xDB) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
||||
this->crypt_bytes_read_++;
|
||||
|
||||
// Read the length of the incoming encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
||||
// Complete header + data bytes
|
||||
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
||||
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
||||
}
|
||||
|
||||
// Check for the end of the encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "End of encrypted telegram found");
|
||||
|
||||
// Decrypt the encrypted telegram.
|
||||
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
||||
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
||||
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
||||
// system title is at byte 2 and frame counter at byte 15
|
||||
for (int i = 10; i < 14; i++)
|
||||
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
||||
constexpr uint16_t iv_size{12};
|
||||
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
||||
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
||||
// the ciphertext start at byte 18
|
||||
&this->crypt_telegram_[18],
|
||||
// cipher size
|
||||
this->crypt_bytes_read_ - 17);
|
||||
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
|
||||
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
||||
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
||||
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
||||
|
||||
// Parse the decrypted telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
||||
this->crypt_bytes_read_++;
|
||||
|
||||
// Read the length of the incoming encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
||||
// Complete header + data bytes
|
||||
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
||||
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
||||
}
|
||||
|
||||
// Check for the end of the encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "End of encrypted telegram found");
|
||||
|
||||
// Decrypt the encrypted telegram.
|
||||
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
||||
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
||||
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
||||
// system title is at byte 2 and frame counter at byte 15
|
||||
for (int i = 10; i < 14; i++)
|
||||
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
||||
constexpr uint16_t iv_size{12};
|
||||
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
||||
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
||||
// the ciphertext start at byte 18
|
||||
&this->crypt_telegram_[18],
|
||||
// cipher size
|
||||
this->crypt_bytes_read_ - 17);
|
||||
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
|
||||
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
||||
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
||||
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
||||
|
||||
// Parse the decrypted telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ class Dsmr : public Component, public uart::UARTDevice {
|
||||
void receive_telegram_();
|
||||
void receive_encrypted_telegram_();
|
||||
void reset_telegram_();
|
||||
void drain_rx_buffer_();
|
||||
|
||||
/// Wait for UART data to become available within the read timeout.
|
||||
///
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
#include "hlk_fm22x.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const char *const TAG = "hlk_fm22x";
|
||||
|
||||
// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name)
|
||||
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
|
||||
|
||||
void HlkFm22xComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
|
||||
this->set_enrolling_(false);
|
||||
while (this->available()) {
|
||||
while (this->available() > 0) {
|
||||
this->read();
|
||||
}
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
|
||||
@@ -35,7 +31,7 @@ void HlkFm22xComponent::update() {
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
|
||||
if (name.length() > 31) {
|
||||
if (name.length() > HLK_FM22X_NAME_SIZE - 1) {
|
||||
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -88,7 +84,7 @@ void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *da
|
||||
}
|
||||
this->wait_cycles_ = 0;
|
||||
this->active_command_ = command;
|
||||
while (this->available())
|
||||
while (this->available() > 0)
|
||||
this->read();
|
||||
this->write((uint8_t) (START_CODE >> 8));
|
||||
this->write((uint8_t) (START_CODE & 0xFF));
|
||||
@@ -137,17 +133,24 @@ void HlkFm22xComponent::recv_command_() {
|
||||
checksum ^= byte;
|
||||
length |= byte;
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
data.reserve(length);
|
||||
if (length > HLK_FM22X_MAX_RESPONSE_SIZE) {
|
||||
ESP_LOGE(TAG, "Response too large: %u bytes", length);
|
||||
// Discard exactly the remaining payload and checksum for this frame
|
||||
for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i)
|
||||
this->read();
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint16_t idx = 0; idx < length; ++idx) {
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
data.push_back(byte);
|
||||
this->recv_buf_[idx] = byte;
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type,
|
||||
format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length));
|
||||
#endif
|
||||
|
||||
byte = this->read();
|
||||
@@ -157,10 +160,10 @@ void HlkFm22xComponent::recv_command_() {
|
||||
}
|
||||
switch (response_type) {
|
||||
case HlkFm22xResponseType::NOTE:
|
||||
this->handle_note_(data);
|
||||
this->handle_note_(this->recv_buf_.data(), length);
|
||||
break;
|
||||
case HlkFm22xResponseType::REPLY:
|
||||
this->handle_reply_(data);
|
||||
this->handle_reply_(this->recv_buf_.data(), length);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
|
||||
@@ -168,11 +171,15 @@ void HlkFm22xComponent::recv_command_() {
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
|
||||
void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
|
||||
if (length < 1) {
|
||||
ESP_LOGE(TAG, "Empty note data");
|
||||
return;
|
||||
}
|
||||
switch (data[0]) {
|
||||
case HlkFm22xNoteType::FACE_STATE:
|
||||
if (data.size() < 17) {
|
||||
ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
|
||||
if (length < 17) {
|
||||
ESP_LOGE(TAG, "Invalid face note data size: %zu", length);
|
||||
break;
|
||||
}
|
||||
{
|
||||
@@ -209,9 +216,13 @@ void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
||||
void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
|
||||
auto expected = this->active_command_;
|
||||
this->active_command_ = HlkFm22xCommand::NONE;
|
||||
if (length < 2) {
|
||||
ESP_LOGE(TAG, "Reply too short: %zu bytes", length);
|
||||
return;
|
||||
}
|
||||
if (data[0] != (uint8_t) expected) {
|
||||
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
|
||||
return;
|
||||
@@ -238,16 +249,20 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
switch (expected) {
|
||||
case HlkFm22xCommand::VERIFY: {
|
||||
if (length < 4 + HLK_FM22X_NAME_SIZE) {
|
||||
ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length);
|
||||
break;
|
||||
}
|
||||
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
|
||||
std::string name(data.begin() + 4, data.begin() + 36);
|
||||
ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
|
||||
const char *name_ptr = reinterpret_cast<const char *>(data + 4);
|
||||
ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr);
|
||||
if (this->last_face_id_sensor_ != nullptr) {
|
||||
this->last_face_id_sensor_->publish_state(face_id);
|
||||
}
|
||||
if (this->last_face_name_text_sensor_ != nullptr) {
|
||||
this->last_face_name_text_sensor_->publish_state(name);
|
||||
this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE);
|
||||
}
|
||||
this->face_scan_matched_callback_.call(face_id, name);
|
||||
this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE));
|
||||
break;
|
||||
}
|
||||
case HlkFm22xCommand::ENROLL: {
|
||||
@@ -266,9 +281,8 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
|
||||
break;
|
||||
case HlkFm22xCommand::GET_VERSION:
|
||||
if (this->version_text_sensor_ != nullptr) {
|
||||
std::string version(data.begin() + 2, data.end());
|
||||
this->version_text_sensor_->publish_state(version);
|
||||
if (this->version_text_sensor_ != nullptr && length > 2) {
|
||||
this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2);
|
||||
}
|
||||
this->defer([this]() { this->get_face_count_(); });
|
||||
break;
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const uint16_t START_CODE = 0xEFAA;
|
||||
static constexpr size_t HLK_FM22X_NAME_SIZE = 32;
|
||||
// Maximum response payload: command(1) + result(1) + face_id(2) + name(32) = 36
|
||||
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
|
||||
enum HlkFm22xCommand {
|
||||
NONE = 0x00,
|
||||
RESET = 0x10,
|
||||
@@ -118,10 +121,11 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
|
||||
void get_face_count_();
|
||||
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
|
||||
void recv_command_();
|
||||
void handle_note_(const std::vector<uint8_t> &data);
|
||||
void handle_reply_(const std::vector<uint8_t> &data);
|
||||
void handle_note_(const uint8_t *data, size_t length);
|
||||
void handle_reply_(const uint8_t *data, size_t length);
|
||||
void set_enrolling_(bool enrolling);
|
||||
|
||||
std::array<uint8_t, HLK_FM22X_MAX_RESPONSE_SIZE> recv_buf_;
|
||||
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
|
||||
uint16_t wait_cycles_ = 0;
|
||||
sensor::Sensor *face_count_sensor_{nullptr};
|
||||
|
||||
@@ -276,10 +276,10 @@ void LD2410Component::restart_and_read_all_info() {
|
||||
|
||||
void LD2410Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -311,10 +311,10 @@ void LD2412Component::restart_and_read_all_info() {
|
||||
|
||||
void LD2412Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -335,9 +335,10 @@ void LD2420Component::revert_config_action() {
|
||||
|
||||
void LD2420Component::loop() {
|
||||
// If there is a active send command do not process it here, the send command call will handle it.
|
||||
while (!this->cmd_active_ && this->available()) {
|
||||
this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
|
||||
if (this->cmd_active_) {
|
||||
return;
|
||||
}
|
||||
this->read_batch_(this->buffer_data_);
|
||||
}
|
||||
|
||||
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
|
||||
@@ -539,6 +540,23 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i], buffer.data(), buffer.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
|
||||
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <span>
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
@@ -165,6 +166,7 @@ class LD2420Component : public Component, public uart::UARTDevice {
|
||||
void handle_energy_mode_(uint8_t *buffer, int len);
|
||||
void handle_ack_data_(uint8_t *buffer, int len);
|
||||
void readline_(int rx_data, uint8_t *buffer, int len);
|
||||
void read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer);
|
||||
void set_calibration_(bool state) { this->calibration_ = state; };
|
||||
bool get_calibration_() { return this->calibration_; };
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_THROTTLE
|
||||
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID
|
||||
|
||||
AUTO_LOAD = ["ld24xx"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
@@ -11,6 +12,8 @@ MULTI_CONF = True
|
||||
ld2450_ns = cg.esphome_ns.namespace("ld2450")
|
||||
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
|
||||
|
||||
LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template())
|
||||
|
||||
CONF_LD2450_ID = "ld2450_id"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
@@ -20,6 +23,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_THROTTLE): cv.invalid(
|
||||
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
|
||||
),
|
||||
cv.Optional(CONF_ON_DATA): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
@@ -45,3 +53,6 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
for conf in config.get(CONF_ON_DATA, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -277,10 +277,10 @@ void LD2450Component::dump_config() {
|
||||
|
||||
void LD2450Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
@@ -413,6 +413,10 @@ void LD2450Component::restart_and_read_all_info() {
|
||||
this->set_timeout(1500, [this]() { this->read_all_info(); });
|
||||
}
|
||||
|
||||
void LD2450Component::add_on_data_callback(std::function<void()> &&callback) {
|
||||
this->data_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
// Send command with values to LD2450
|
||||
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
||||
ESP_LOGV(TAG, "Sending COMMAND %02X", command);
|
||||
@@ -613,6 +617,8 @@ void LD2450Component::handle_periodic_data_() {
|
||||
this->still_presence_millis_ = App.get_loop_component_start_time();
|
||||
}
|
||||
#endif
|
||||
|
||||
this->data_callback_.call();
|
||||
}
|
||||
|
||||
bool LD2450Component::handle_ack_data_() {
|
||||
|
||||
@@ -141,6 +141,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1,
|
||||
int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2);
|
||||
|
||||
/// Add a callback that will be called after each successfully processed periodic data frame.
|
||||
void add_on_data_callback(std::function<void()> &&callback);
|
||||
|
||||
protected:
|
||||
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
|
||||
void set_config_mode_(bool enable);
|
||||
@@ -190,6 +193,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
|
||||
#endif
|
||||
|
||||
LazyCallbackManager<void()> data_callback_;
|
||||
};
|
||||
|
||||
class LD2450DataTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit LD2450DataTrigger(LD2450Component *parent) {
|
||||
parent->add_on_data_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2450
|
||||
|
||||
@@ -36,8 +36,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
#endif
|
||||
|
||||
// Fast path: main thread, no recursion (99.9% of all logs)
|
||||
// Pass nullptr for thread_name since we already know this is the main task
|
||||
if (is_main_task && !this->main_task_recursion_guard_) [[likely]] {
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args);
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,21 +48,23 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
}
|
||||
|
||||
// Non-main thread handling (~0.1% of logs)
|
||||
// Resolve thread name once and pass it through the logging chain.
|
||||
// ESP32/LibreTiny: use TaskHandle_t overload to avoid redundant xTaskGetCurrentTaskHandle()
|
||||
// (we already have the handle from the main task check above).
|
||||
// Host: pass a stack buffer for pthread_getname_np to write into.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task);
|
||||
const char *thread_name = get_thread_name_(current_task);
|
||||
#else // USE_HOST
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args);
|
||||
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
||||
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
||||
#endif
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name);
|
||||
}
|
||||
|
||||
// Handles non-main thread logging only
|
||||
// Kept separate from hot path to improve instruction cache performance
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
TaskHandle_t current_task) {
|
||||
#else // USE_HOST
|
||||
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) {
|
||||
#endif
|
||||
const char *thread_name) {
|
||||
// Check if already in recursion for this non-main thread/task
|
||||
if (this->is_non_main_task_recursive_()) {
|
||||
return;
|
||||
@@ -73,12 +76,8 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
bool message_sent = false;
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// For non-main threads/tasks, queue the message for callbacks
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
message_sent =
|
||||
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args);
|
||||
#else // USE_HOST
|
||||
message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), format, args);
|
||||
#endif
|
||||
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, format, args);
|
||||
if (message_sent) {
|
||||
// Enable logger loop to process the buffered message
|
||||
// This is safe to call from any context including ISRs
|
||||
@@ -101,19 +100,27 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
#endif
|
||||
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
|
||||
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||
this->write_to_console_(buf);
|
||||
}
|
||||
|
||||
// RAII guard automatically resets on return
|
||||
}
|
||||
#else
|
||||
// Implementation for all other platforms (single-task, no threading)
|
||||
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
|
||||
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
|
||||
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
|
||||
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
|
||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
return;
|
||||
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
|
||||
#ifdef USE_ZEPHYR
|
||||
char tmp[MAX_POINTER_REPRESENTATION];
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
|
||||
this->get_thread_name_(tmp));
|
||||
#else // Other single-task platforms don't have thread names, so pass nullptr
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
#endif
|
||||
}
|
||||
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
|
||||
|
||||
@@ -129,7 +136,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
return;
|
||||
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
}
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdarg>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
#include <pthread.h>
|
||||
@@ -124,6 +125,10 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
// Stack buffer size for retrieving thread/task names from the OS
|
||||
// macOS allows up to 64 bytes, Linux up to 16
|
||||
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
|
||||
|
||||
// Buffer wrapper for log formatting functions
|
||||
struct LogBuffer {
|
||||
char *data;
|
||||
@@ -408,34 +413,24 @@ class Logger : public Component {
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
// Handles non-main thread logging only (~0.1% of calls)
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// ESP32/LibreTiny: Pass task handle to avoid calling xTaskGetCurrentTaskHandle() twice
|
||||
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
TaskHandle_t current_task);
|
||||
#else // USE_HOST
|
||||
// Host: No task handle parameter needed (not used in send_message_thread_safe)
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args);
|
||||
#endif
|
||||
const char *thread_name);
|
||||
#endif
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on)
|
||||
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args, LogBuffer &buf) {
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST)
|
||||
buf.write_header(level, tag, line, this->get_thread_name_());
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char tmp[MAX_POINTER_REPRESENTATION];
|
||||
buf.write_header(level, tag, line, this->get_thread_name_(tmp));
|
||||
#else
|
||||
buf.write_header(level, tag, line, nullptr);
|
||||
#endif
|
||||
va_list args, LogBuffer &buf, const char *thread_name) {
|
||||
buf.write_header(level, tag, line, thread_name);
|
||||
buf.format_body(format, args);
|
||||
}
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Format a log message with flash string format and write it to a buffer with header, footer, and null terminator
|
||||
// ESP8266-only (single-task), thread_name is always nullptr
|
||||
inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line,
|
||||
const __FlashStringHelper *format, va_list args,
|
||||
LogBuffer &buf) {
|
||||
@@ -466,9 +461,10 @@ class Logger : public Component {
|
||||
|
||||
// Helper to format and send a log message to both console and listeners
|
||||
// Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings
|
||||
// thread_name: name of the calling thread/task, or nullptr for main task
|
||||
template<typename FormatType>
|
||||
inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line,
|
||||
FormatType format, va_list args) {
|
||||
FormatType format, va_list args, const char *thread_name) {
|
||||
RecursionGuard guard(recursion_guard);
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
@@ -477,7 +473,7 @@ class Logger : public Component {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||
}
|
||||
this->notify_listeners_(level, tag, buf);
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
@@ -565,37 +561,57 @@ class Logger : public Component {
|
||||
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(
|
||||
#ifdef USE_ZEPHYR
|
||||
char *buff
|
||||
// --- get_thread_name_ overloads (per-platform) ---
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls
|
||||
// when the caller already has the handle (e.g. from the main task check in log_vprintf_)
|
||||
const char *get_thread_name_(TaskHandle_t task) {
|
||||
if (task == this->main_task_) {
|
||||
return nullptr; // Main task
|
||||
}
|
||||
#if defined(USE_ESP32)
|
||||
return pcTaskGetName(task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(task);
|
||||
#endif
|
||||
) {
|
||||
#ifdef USE_ZEPHYR
|
||||
}
|
||||
|
||||
// Convenience overload - gets the current task handle and delegates
|
||||
const char *HOT get_thread_name_() { return this->get_thread_name_(xTaskGetCurrentTaskHandle()); }
|
||||
|
||||
#elif defined(USE_HOST)
|
||||
// Takes a caller-provided buffer for the thread name (stack-allocated for thread safety)
|
||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
||||
pthread_t current_thread = pthread_self();
|
||||
if (pthread_equal(current_thread, main_thread_)) {
|
||||
return nullptr; // Main thread
|
||||
}
|
||||
// For non-main threads, get the thread name into the caller-provided buffer
|
||||
if (pthread_getname_np(current_thread, buff.data(), buff.size()) == 0) {
|
||||
return buff.data();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
||||
k_tid_t current_task = k_current_get();
|
||||
#else
|
||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||
#endif
|
||||
if (current_task == main_task_) {
|
||||
return nullptr; // Main task
|
||||
} else {
|
||||
#if defined(USE_ESP32)
|
||||
return pcTaskGetName(current_task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *name = k_thread_name_get(current_task);
|
||||
if (name) {
|
||||
// zephyr print task names only if debug component is present
|
||||
return name;
|
||||
}
|
||||
std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
|
||||
return buff;
|
||||
#endif
|
||||
}
|
||||
const char *name = k_thread_name_get(current_task);
|
||||
if (name) {
|
||||
// zephyr print task names only if debug component is present
|
||||
return name;
|
||||
}
|
||||
std::snprintf(buff.data(), buff.size(), "%p", current_task);
|
||||
return buff.data();
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Non-main task recursion guards (per-platform) ---
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
// RAII guard for non-main task recursion using pthread TLS
|
||||
class NonMainTaskRecursionGuard {
|
||||
@@ -635,22 +651,6 @@ class Logger : public Component {
|
||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_HOST
|
||||
const char *HOT get_thread_name_() {
|
||||
pthread_t current_thread = pthread_self();
|
||||
if (pthread_equal(current_thread, main_thread_)) {
|
||||
return nullptr; // Main thread
|
||||
}
|
||||
// For non-main threads, return the thread name
|
||||
// We store it in thread-local storage to avoid allocation
|
||||
static thread_local char thread_name_buf[32];
|
||||
if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) {
|
||||
return thread_name_buf;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
|
||||
inline void disable_loop_when_buffer_empty_() {
|
||||
|
||||
@@ -59,7 +59,7 @@ void TaskLogBuffer::release_message_main_loop(void *token) {
|
||||
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
@@ -95,7 +95,6 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
// Store the thread name now instead of waiting until main loop processing
|
||||
// This avoids crashes if the task completes or is deleted between when this message
|
||||
// is enqueued and when it's processed by the main loop
|
||||
const char *thread_name = pcTaskGetName(task_handle);
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
|
||||
@@ -58,7 +58,7 @@ class TaskLogBuffer {
|
||||
void release_message_main_loop(void *token);
|
||||
|
||||
// Thread-safe - send a message to the ring buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||
|
||||
@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format,
|
||||
va_list args) {
|
||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// Acquire a slot
|
||||
int slot_index = this->acquire_write_slot_();
|
||||
if (slot_index < 0) {
|
||||
@@ -85,11 +85,9 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
|
||||
msg.tag = tag;
|
||||
msg.line = line;
|
||||
|
||||
// Get thread name using pthread
|
||||
char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE];
|
||||
// pthread_getname_np works the same on Linux and macOS
|
||||
if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) {
|
||||
strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1);
|
||||
// Store the thread name now to avoid crashes if thread exits before processing
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
|
||||
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
|
||||
} else {
|
||||
msg.thread_name[0] = '\0';
|
||||
|
||||
@@ -86,7 +86,8 @@ class TaskLogBufferHost {
|
||||
|
||||
// Thread-safe - send a message to the buffer from any thread
|
||||
// Returns true if message was queued, false if buffer is full
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args);
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed
|
||||
inline bool HOT has_messages() const {
|
||||
|
||||
@@ -101,7 +101,7 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
|
||||
}
|
||||
|
||||
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
|
||||
TaskHandle_t task_handle, const char *format, va_list args) {
|
||||
const char *thread_name, const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
@@ -162,7 +162,6 @@ bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char
|
||||
msg->line = line;
|
||||
|
||||
// Store the thread name now to avoid crashes if task is deleted before processing
|
||||
const char *thread_name = pcTaskGetTaskName(task_handle);
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';
|
||||
|
||||
@@ -70,7 +70,7 @@ class TaskLogBufferLibreTiny {
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Fast check using volatile counter - no lock needed
|
||||
|
||||
@@ -20,10 +20,10 @@ void Modbus::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -398,10 +398,10 @@ bool Nextion::remove_from_q_(bool report_empty) {
|
||||
|
||||
void Nextion::process_serial_() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ void Pipsolar::setup() {
|
||||
|
||||
void Pipsolar::empty_uart_buffer_() {
|
||||
uint8_t buf[64];
|
||||
int avail;
|
||||
size_t avail;
|
||||
while ((avail = this->available()) > 0) {
|
||||
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
|
||||
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -97,10 +97,10 @@ void Pipsolar::loop() {
|
||||
}
|
||||
|
||||
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
uint8_t buf[64];
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -56,17 +56,23 @@ void PylontechComponent::setup() {
|
||||
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
||||
|
||||
void PylontechComponent::loop() {
|
||||
if (this->available() > 0) {
|
||||
size_t avail = this->available();
|
||||
if (avail > 0) {
|
||||
// pylontech sends a lot of data very suddenly
|
||||
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
|
||||
uint8_t data;
|
||||
int recv = 0;
|
||||
while (this->available() > 0) {
|
||||
if (this->read_byte(&data)) {
|
||||
buffer_[buffer_index_write_] += (char) data;
|
||||
recv++;
|
||||
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
|
||||
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
recv += to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
buffer_[buffer_index_write_] += (char) buf[i];
|
||||
if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
// complete line received
|
||||
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
|
||||
}
|
||||
|
||||
@@ -82,10 +82,10 @@ void RD03DComponent::dump_config() {
|
||||
|
||||
void RD03DComponent::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -136,10 +136,10 @@ void RFBridgeComponent::loop() {
|
||||
this->last_bridge_byte_ = now;
|
||||
}
|
||||
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
uint8_t buf[64];
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -107,10 +107,10 @@ void MR24HPC1Component::update_() {
|
||||
// main loop
|
||||
void MR24HPC1Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ void MR60BHA2Component::dump_config() {
|
||||
// main loop
|
||||
void MR60BHA2Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@ void MR60FDA2Component::setup() {
|
||||
// main loop
|
||||
void MR60FDA2Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import water_heater
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AWAY,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_OPTIMISTIC,
|
||||
@@ -18,6 +19,7 @@ from esphome.types import ConfigType
|
||||
from .. import template_ns
|
||||
|
||||
CONF_CURRENT_TEMPERATURE = "current_temperature"
|
||||
CONF_IS_ON = "is_on"
|
||||
|
||||
TemplateWaterHeater = template_ns.class_(
|
||||
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater
|
||||
@@ -51,6 +53,8 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
||||
water_heater.validate_water_heater_mode
|
||||
),
|
||||
cv.Optional(CONF_AWAY): cv.returning_lambda,
|
||||
cv.Optional(CONF_IS_ON): cv.returning_lambda,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -98,6 +102,22 @@ async def to_code(config: ConfigType) -> None:
|
||||
if CONF_SUPPORTED_MODES in config:
|
||||
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
|
||||
|
||||
if CONF_AWAY in config:
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_AWAY],
|
||||
[],
|
||||
return_type=cg.optional.template(bool),
|
||||
)
|
||||
cg.add(var.set_away_lambda(template_))
|
||||
|
||||
if CONF_IS_ON in config:
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_IS_ON],
|
||||
[],
|
||||
return_type=cg.optional.template(bool),
|
||||
)
|
||||
cg.add(var.set_is_on_lambda(template_))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"water_heater.template.publish",
|
||||
@@ -110,6 +130,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
cv.Optional(CONF_MODE): cv.templatable(
|
||||
water_heater.validate_water_heater_mode
|
||||
),
|
||||
cv.Optional(CONF_AWAY): cv.templatable(cv.boolean),
|
||||
cv.Optional(CONF_IS_ON): cv.templatable(cv.boolean),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -134,4 +156,12 @@ async def water_heater_template_publish_to_code(
|
||||
template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode)
|
||||
cg.add(var.set_mode(template_))
|
||||
|
||||
if CONF_AWAY in config:
|
||||
template_ = await cg.templatable(config[CONF_AWAY], args, bool)
|
||||
cg.add(var.set_away(template_))
|
||||
|
||||
if CONF_IS_ON in config:
|
||||
template_ = await cg.templatable(config[CONF_IS_ON], args, bool)
|
||||
cg.add(var.set_is_on(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -11,12 +11,15 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
|
||||
TEMPLATABLE_VALUE(float, current_temperature)
|
||||
TEMPLATABLE_VALUE(float, target_temperature)
|
||||
TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode)
|
||||
TEMPLATABLE_VALUE(bool, away)
|
||||
TEMPLATABLE_VALUE(bool, is_on)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->current_temperature_.has_value()) {
|
||||
this->parent_->set_current_temperature(this->current_temperature_.value(x...));
|
||||
}
|
||||
bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value();
|
||||
bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value() || this->away_.has_value() ||
|
||||
this->is_on_.has_value();
|
||||
if (needs_call) {
|
||||
auto call = this->parent_->make_call();
|
||||
if (this->target_temperature_.has_value()) {
|
||||
@@ -25,6 +28,12 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
|
||||
if (this->mode_.has_value()) {
|
||||
call.set_mode(this->mode_.value(x...));
|
||||
}
|
||||
if (this->away_.has_value()) {
|
||||
call.set_away(this->away_.value(x...));
|
||||
}
|
||||
if (this->is_on_.has_value()) {
|
||||
call.set_on(this->is_on_.value(x...));
|
||||
}
|
||||
call.perform();
|
||||
} else {
|
||||
this->parent_->publish_state();
|
||||
|
||||
@@ -17,7 +17,7 @@ void TemplateWaterHeater::setup() {
|
||||
}
|
||||
}
|
||||
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
|
||||
!this->mode_f_.has_value())
|
||||
!this->mode_f_.has_value() && !this->away_f_.has_value() && !this->is_on_f_.has_value())
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
|
||||
if (this->target_temperature_f_.has_value()) {
|
||||
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
|
||||
}
|
||||
if (this->away_f_.has_value()) {
|
||||
traits.set_supports_away_mode(true);
|
||||
}
|
||||
if (this->is_on_f_.has_value()) {
|
||||
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF);
|
||||
}
|
||||
return traits;
|
||||
}
|
||||
|
||||
@@ -62,6 +68,22 @@ void TemplateWaterHeater::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
auto away = this->away_f_.call();
|
||||
if (away.has_value()) {
|
||||
if (*away != this->is_away()) {
|
||||
this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *away);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
auto is_on = this->is_on_f_.call();
|
||||
if (is_on.has_value()) {
|
||||
if (*is_on != this->is_on()) {
|
||||
this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *is_on);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this->publish_state();
|
||||
}
|
||||
@@ -90,6 +112,17 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) {
|
||||
}
|
||||
}
|
||||
|
||||
if (call.get_away().has_value()) {
|
||||
if (this->optimistic_) {
|
||||
this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *call.get_away());
|
||||
}
|
||||
}
|
||||
if (call.get_on().has_value()) {
|
||||
if (this->optimistic_) {
|
||||
this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *call.get_on());
|
||||
}
|
||||
}
|
||||
|
||||
this->set_trigger_.trigger();
|
||||
|
||||
if (this->optimistic_) {
|
||||
|
||||
@@ -24,6 +24,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
||||
this->target_temperature_f_.set(std::forward<F>(f));
|
||||
}
|
||||
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
|
||||
template<typename F> void set_away_lambda(F &&f) { this->away_f_.set(std::forward<F>(f)); }
|
||||
template<typename F> void set_is_on_lambda(F &&f) { this->is_on_f_.set(std::forward<F>(f)); }
|
||||
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
@@ -49,6 +51,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
||||
TemplateLambda<float> current_temperature_f_;
|
||||
TemplateLambda<float> target_temperature_f_;
|
||||
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
|
||||
TemplateLambda<bool> away_f_;
|
||||
TemplateLambda<bool> is_on_f_;
|
||||
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
||||
water_heater::WaterHeaterModeMask supported_modes_;
|
||||
bool optimistic_{true};
|
||||
|
||||
@@ -32,10 +32,10 @@ void Tuya::setup() {
|
||||
|
||||
void Tuya::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
static const char *const TAG = "uart";
|
||||
|
||||
// UART parity strings indexed by UARTParityOptions enum (0-2): NONE, EVEN, ODD
|
||||
PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "UNKNOWN");
|
||||
|
||||
void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity,
|
||||
uint8_t data_bits) {
|
||||
if (this->parent_->get_baud_rate() != baud_rate) {
|
||||
@@ -30,16 +34,7 @@ void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UART
|
||||
}
|
||||
|
||||
const LogString *parity_to_str(UARTParityOptions parity) {
|
||||
switch (parity) {
|
||||
case UART_CONFIG_PARITY_NONE:
|
||||
return LOG_STR("NONE");
|
||||
case UART_CONFIG_PARITY_EVEN:
|
||||
return LOG_STR("EVEN");
|
||||
case UART_CONFIG_PARITY_ODD:
|
||||
return LOG_STR("ODD");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return UARTParityStrings::get_log_str(static_cast<uint8_t>(parity), UARTParityStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
} // namespace esphome::uart
|
||||
|
||||
@@ -11,8 +11,8 @@ pyserial==3.5
|
||||
platformio==6.1.19
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20260110.0
|
||||
aioesphomeapi==43.14.0
|
||||
esphome-dashboard==20260210.0
|
||||
aioesphomeapi==44.0.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
ld2450:
|
||||
- id: ld2450_radar
|
||||
on_data:
|
||||
then:
|
||||
- logger.log: "LD2450 Radar Data Received"
|
||||
|
||||
button:
|
||||
- platform: ld2450
|
||||
|
||||
@@ -13,6 +13,8 @@ esphome:
|
||||
id: template_water_heater
|
||||
target_temperature: 50.0
|
||||
mode: ECO
|
||||
away: false
|
||||
is_on: true
|
||||
|
||||
# Templated
|
||||
- water_heater.template.publish:
|
||||
@@ -20,6 +22,8 @@ esphome:
|
||||
current_temperature: !lambda "return 45.0;"
|
||||
target_temperature: !lambda "return 55.0;"
|
||||
mode: !lambda "return water_heater::WATER_HEATER_MODE_GAS;"
|
||||
away: !lambda "return true;"
|
||||
is_on: !lambda "return false;"
|
||||
|
||||
# Test C++ API: set_template() with stateless lambda (no captures)
|
||||
# NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break.
|
||||
@@ -414,6 +418,8 @@ water_heater:
|
||||
current_temperature: !lambda "return 42.0f;"
|
||||
target_temperature: !lambda "return 60.0f;"
|
||||
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
|
||||
away: !lambda "return false;"
|
||||
is_on: !lambda "return true;"
|
||||
supported_modes:
|
||||
- "OFF"
|
||||
- ECO
|
||||
|
||||
@@ -4,6 +4,14 @@ host:
|
||||
api:
|
||||
logger:
|
||||
|
||||
globals:
|
||||
- id: global_away
|
||||
type: bool
|
||||
initial_value: "false"
|
||||
- id: global_is_on
|
||||
type: bool
|
||||
initial_value: "true"
|
||||
|
||||
water_heater:
|
||||
- platform: template
|
||||
id: test_boiler
|
||||
@@ -11,6 +19,8 @@ water_heater:
|
||||
optimistic: true
|
||||
current_temperature: !lambda "return 45.0f;"
|
||||
target_temperature: !lambda "return 60.0f;"
|
||||
away: !lambda "return id(global_away);"
|
||||
is_on: !lambda "return id(global_is_on);"
|
||||
# Note: No mode lambda - we want optimistic mode changes to stick
|
||||
# A mode lambda would override mode changes in loop()
|
||||
supported_modes:
|
||||
@@ -22,3 +32,8 @@ water_heater:
|
||||
min_temperature: 30.0
|
||||
max_temperature: 85.0
|
||||
target_temperature_step: 0.5
|
||||
set_action:
|
||||
- lambda: |-
|
||||
// Sync optimistic state back to globals so lambdas reflect the change
|
||||
id(global_away) = id(test_boiler).is_away();
|
||||
id(global_is_on) = id(test_boiler).is_on();
|
||||
|
||||
@@ -5,7 +5,13 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import WaterHeaterInfo, WaterHeaterMode, WaterHeaterState
|
||||
from aioesphomeapi import (
|
||||
WaterHeaterFeature,
|
||||
WaterHeaterInfo,
|
||||
WaterHeaterMode,
|
||||
WaterHeaterState,
|
||||
WaterHeaterStateFlag,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
@@ -22,18 +28,25 @@ async def test_water_heater_template(
|
||||
loop = asyncio.get_running_loop()
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
states: dict[int, aioesphomeapi.EntityState] = {}
|
||||
gas_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future()
|
||||
eco_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future()
|
||||
state_future: asyncio.Future[WaterHeaterState] | None = None
|
||||
|
||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||
states[state.key] = state
|
||||
if isinstance(state, WaterHeaterState):
|
||||
# Wait for GAS mode
|
||||
if state.mode == WaterHeaterMode.GAS and not gas_mode_future.done():
|
||||
gas_mode_future.set_result(state)
|
||||
# Wait for ECO mode (we start at OFF, so test transitioning to ECO)
|
||||
elif state.mode == WaterHeaterMode.ECO and not eco_mode_future.done():
|
||||
eco_mode_future.set_result(state)
|
||||
if (
|
||||
isinstance(state, WaterHeaterState)
|
||||
and state_future is not None
|
||||
and not state_future.done()
|
||||
):
|
||||
state_future.set_result(state)
|
||||
|
||||
async def wait_for_state(timeout: float = 5.0) -> WaterHeaterState:
|
||||
"""Wait for next water heater state change."""
|
||||
nonlocal state_future
|
||||
state_future = loop.create_future()
|
||||
try:
|
||||
return await asyncio.wait_for(state_future, timeout)
|
||||
finally:
|
||||
state_future = None
|
||||
|
||||
# Get entities and set up state synchronization
|
||||
entities, services = await client.list_entities_services()
|
||||
@@ -89,24 +102,52 @@ async def test_water_heater_template(
|
||||
f"Expected target temp 60.0, got {initial_state.target_temperature}"
|
||||
)
|
||||
|
||||
# Verify supported features: away mode and on/off (fixture has away + is_on lambdas)
|
||||
assert (
|
||||
test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_AWAY_MODE
|
||||
) != 0, "Expected SUPPORTS_AWAY_MODE in supported_features"
|
||||
assert (
|
||||
test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_ON_OFF
|
||||
) != 0, "Expected SUPPORTS_ON_OFF in supported_features"
|
||||
|
||||
# Verify initial state: on (is_on lambda returns true), not away (away lambda returns false)
|
||||
assert (initial_state.state & WaterHeaterStateFlag.ON) != 0, (
|
||||
"Expected initial state to include ON flag"
|
||||
)
|
||||
assert (initial_state.state & WaterHeaterStateFlag.AWAY) == 0, (
|
||||
"Expected initial state to not include AWAY flag"
|
||||
)
|
||||
|
||||
# Test turning on away mode
|
||||
client.water_heater_command(test_water_heater.key, away=True)
|
||||
away_on_state = await wait_for_state()
|
||||
assert (away_on_state.state & WaterHeaterStateFlag.AWAY) != 0
|
||||
# ON flag should still be set (is_on lambda returns true)
|
||||
assert (away_on_state.state & WaterHeaterStateFlag.ON) != 0
|
||||
|
||||
# Test turning off away mode
|
||||
client.water_heater_command(test_water_heater.key, away=False)
|
||||
away_off_state = await wait_for_state()
|
||||
assert (away_off_state.state & WaterHeaterStateFlag.AWAY) == 0
|
||||
assert (away_off_state.state & WaterHeaterStateFlag.ON) != 0
|
||||
|
||||
# Test turning off (on=False)
|
||||
client.water_heater_command(test_water_heater.key, on=False)
|
||||
off_state = await wait_for_state()
|
||||
assert (off_state.state & WaterHeaterStateFlag.ON) == 0
|
||||
assert (off_state.state & WaterHeaterStateFlag.AWAY) == 0
|
||||
|
||||
# Test turning back on (on=True)
|
||||
client.water_heater_command(test_water_heater.key, on=True)
|
||||
on_state = await wait_for_state()
|
||||
assert (on_state.state & WaterHeaterStateFlag.ON) != 0
|
||||
|
||||
# Test changing to GAS mode
|
||||
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)
|
||||
|
||||
try:
|
||||
gas_state = await asyncio.wait_for(gas_mode_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("GAS mode change not received within 5 seconds")
|
||||
|
||||
assert isinstance(gas_state, WaterHeaterState)
|
||||
gas_state = await wait_for_state()
|
||||
assert gas_state.mode == WaterHeaterMode.GAS
|
||||
|
||||
# Test changing to ECO mode (from GAS)
|
||||
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.ECO)
|
||||
|
||||
try:
|
||||
eco_state = await asyncio.wait_for(eco_mode_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("ECO mode change not received within 5 seconds")
|
||||
|
||||
assert isinstance(eco_state, WaterHeaterState)
|
||||
eco_state = await wait_for_state()
|
||||
assert eco_state.mode == WaterHeaterMode.ECO
|
||||
|
||||
Reference in New Issue
Block a user