diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 63ddbbac05a..37f82e2755e 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced +8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695 diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d25934c60b4..18dac6a2d17 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -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 { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 09eb17110f7..e51689fd077 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1344,8 +1344,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(); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 15819da1721..d001f869c50 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -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 { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index f1e3bdcafe1..73690610ede 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -385,6 +385,10 @@ const char *proto_enum_to_string(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"; } diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 45abd3ca3df..7ffdf757a0f 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 48c06be5581..79f8fd03c3e 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 20fbd20cd63..baf7f593147 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -120,9 +120,9 @@ void Dsmr::stop_requesting_data_() { void Dsmr::drain_rx_buffer_() { uint8_t buf[64]; - int avail; + size_t avail; while ((avail = this->available()) > 0) { - if (!this->read_array(buf, std::min(static_cast(avail), sizeof(buf)))) { + if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { break; } } @@ -140,9 +140,9 @@ void Dsmr::receive_telegram_() { while (this->available_within_timeout_()) { // Read all available bytes in batches to reduce UART call overhead. uint8_t buf[64]; - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) return; avail -= to_read; @@ -206,9 +206,9 @@ void Dsmr::receive_encrypted_telegram_() { while (this->available_within_timeout_()) { // Read all available bytes in batches to reduce UART call overhead. uint8_t buf[64]; - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) return; avail -= to_read; diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6f5011246cb..8c052acc87a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -135,6 +135,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_driver_dac", # DAC driver - only needed by esp32_dac component "esp_driver_i2s", # I2S driver - only needed by i2s_audio component "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM + "esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 170f436f026..287c780769d 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -95,9 +95,9 @@ async def to_code(config): framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" if framework_ver >= cv.Version(5, 5, 0): - esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4") + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 4ca0b998b18..8bb5cbb62ed 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -7,22 +7,25 @@ #include "esphome/core/log.h" #include +#include namespace esphome { namespace esp32_rmt_led_strip { static const char *const TAG = "esp32_rmt_led_strip"; -#ifdef USE_ESP32_VARIANT_ESP32H2 -static const uint32_t RMT_CLK_FREQ = 32000000; -static const uint8_t RMT_CLK_DIV = 1; -#else -static const uint32_t RMT_CLK_FREQ = 80000000; -static const uint8_t RMT_CLK_DIV = 2; -#endif - static const size_t RMT_SYMBOLS_PER_BYTE = 8; +// Query the RMT default clock source frequency. This varies by variant: +// APB (80MHz) on ESP32/S2/S3/C3, PLL_F80M (80MHz) on C6/P4, XTAL (32MHz) on H2. +// Worst-case reset time is WS2811 at 300µs = 24000 ticks at 80MHz, well within +// the 15-bit rmt_symbol_word_t duration field max of 32767. +static uint32_t rmt_resolution_hz() { + uint32_t freq; + esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); + return freq; +} + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free, rmt_symbol_word_t *symbols, bool *done, void *arg) { @@ -92,7 +95,7 @@ void ESP32RMTLEDStripLightOutput::setup() { rmt_tx_channel_config_t channel; memset(&channel, 0, sizeof(channel)); channel.clk_src = RMT_CLK_SRC_DEFAULT; - channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV; + channel.resolution_hz = rmt_resolution_hz(); channel.gpio_num = gpio_num_t(this->pin_); channel.mem_block_symbols = this->rmt_symbols_; channel.trans_queue_depth = 1; @@ -137,7 +140,7 @@ void ESP32RMTLEDStripLightOutput::setup() { void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) { - float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; + float ratio = (float) rmt_resolution_hz() / 1e09f; // 0-bit this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high); diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 4727877633c..1d793ac6b17 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -94,10 +94,7 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): if CORE.is_esp32: - # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) - # HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h - # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) - include_builtin_idf_component("driver") + include_builtin_idf_component("esp_driver_pcnt") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index c88360ca786..a427cc4a052 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -103,6 +103,42 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st * - ESP-IDF: blocking reads, 0 only returned when all content read * - Arduino: non-blocking, 0 means "no data yet" or "all content read" * + * Chunked responses that complete in a reasonable time work correctly on both + * platforms. The limitation below applies only to *streaming* chunked + * responses where data arrives slowly over a long period. + * + * Streaming chunked responses are NOT supported (all platforms): + * The read helpers (http_read_loop_result, http_read_fully) block the main + * event loop until all response data is received. For streaming responses + * where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy), + * this starves the event loop on both ESP-IDF and Arduino. If data arrives + * just often enough to avoid the caller's timeout, the loop runs + * indefinitely. If data stops entirely, ESP-IDF fails with + * -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with + * delay(1) until the caller's timeout fires. Supporting streaming requires + * a non-blocking incremental read pattern that yields back to the event + * loop between chunks. Components that need streaming should use + * esp_http_client directly on a separate FreeRTOS task with + * esp_http_client_is_complete_data_received() for completion detection + * (see audio_reader.cpp for an example). + * + * Chunked transfer encoding - platform differences: + * - ESP-IDF HttpContainer: + * HttpContainerIDF overrides is_read_complete() to call + * esp_http_client_is_complete_data_received(), which is the + * authoritative completion check for both chunked and non-chunked + * transfers. When esp_http_client_read() returns 0 for a completed + * chunked response, read() returns 0 and is_read_complete() returns + * true, so callers get COMPLETE from http_read_loop_result(). + * + * - Arduino HttpContainer: + * Chunked responses are decoded internally (see + * HttpContainerArduino::read_chunked_()). When the final chunk arrives, + * is_chunked_ is cleared and content_length is set to bytes_read_. + * Completion is then detected via is_read_complete(), and a subsequent + * read() returns 0 to indicate "all content read" (not + * HTTP_ERROR_CONNECTION_CLOSED). + * * Use the helper functions below instead of checking return values directly: * - http_read_loop_result(): for manual loops with per-chunk processing * - http_read_fully(): for simple "read N bytes into buffer" operations @@ -204,9 +240,13 @@ class HttpContainer : public Parented { size_t get_bytes_read() const { return this->bytes_read_; } - /// Check if all expected content has been read - /// For chunked responses, returns false (completion detected via read() returning error/EOF) - bool is_read_complete() const { + /// Check if all expected content has been read. + /// Base implementation handles non-chunked responses and status-code-based no-body checks. + /// Platform implementations may override for chunked completion detection: + /// - ESP-IDF: overrides to call esp_http_client_is_complete_data_received() for chunked. + /// - Arduino: read_chunked_() clears is_chunked_ and sets content_length on the final + /// chunk, after which the base implementation detects completion. + virtual bool is_read_complete() const { // Per RFC 9112, these responses have no body: // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index bd12b7d1231..486984a694b 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -218,32 +218,50 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c return container; } +bool HttpContainerIDF::is_read_complete() const { + // Base class handles no-body status codes and non-chunked content_length completion + if (HttpContainer::is_read_complete()) { + return true; + } + // For chunked responses, use the authoritative ESP-IDF completion check + return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_); +} + // ESP-IDF HTTP read implementation (blocking mode) // // WARNING: Return values differ from BSD sockets! See http_request.h for full documentation. // // esp_http_client_read() in blocking mode returns: // > 0: bytes read -// 0: connection closed (end of stream) +// 0: all chunked data received (is_chunk_complete true) or connection closed +// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet // < 0: error // // We normalize to HttpContainer::read() contract: // > 0: bytes read -// 0: all content read (only returned when content_length is known and fully read) +// 0: all content read (for both content_length-based and chunked completion) // < 0: error/connection closed // // Note on chunked transfer encoding: // esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header). -// We handle this by skipping the content_length check when content_length is 0, -// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF -// by returning 0. +// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls +// esp_http_client_is_complete_data_received() to distinguish successful completion from +// connection errors. Callers use http_read_loop_result() which checks is_read_complete() +// to return COMPLETE for successful chunked EOF. +// +// Streaming chunked responses are not supported (see http_request.h for details). +// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN +// after its internal transport timeout (configured via timeout_ms) expires. +// This is passed through as a negative return value, which callers treat as an error. int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - // Check if we've already read all expected content (non-chunked only) - // For chunked responses (content_length == 0), esp_http_client_read() handles EOF - if (this->is_read_complete()) { + // Check if we've already read all expected content (non-chunked and no-body only). + // Use the base class check here, NOT the override: esp_http_client_is_complete_data_received() + // returns true as soon as all data arrives from the network, but data may still be in + // the client's internal buffer waiting to be consumed by esp_http_client_read(). + if (HttpContainer::is_read_complete()) { return 0; // All content read successfully } @@ -258,15 +276,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { return read_len_or_error; } - // esp_http_client_read() returns 0 in two cases: - // 1. Known content_length: connection closed before all data received (error) - // 2. Chunked encoding (content_length == 0): end of stream reached (EOF) - // For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct. - // For case 2, 0 indicates that all chunked data has already been delivered - // in previous successful read() calls, so treating this as a closed - // connection does not cause any loss of response data. + // esp_http_client_read() returns 0 when: + // - Known content_length: connection closed before all data received (error) + // - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF) + // + // Return 0 in both cases. Callers use http_read_loop_result() which calls + // is_read_complete() to distinguish these: + // - Chunked complete: is_read_complete() returns true (via + // esp_http_client_is_complete_data_received()), caller gets COMPLETE + // - Non-chunked incomplete: is_read_complete() returns false, caller + // eventually gets TIMEOUT (since no more data arrives) if (read_len_or_error == 0) { - return HTTP_ERROR_CONNECTION_CLOSED; + return 0; } // Negative value - error, return the actual error code for debugging diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index ad11811a8fa..2a130eae587 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -16,6 +16,7 @@ class HttpContainerIDF : public HttpContainer { HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {} int read(uint8_t *buf, size_t max_len) override; void end() override; + bool is_read_complete() const override; /// @brief Feeds the watchdog timer if the executing task has one attached void feed_wdt(); diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index b57b1d99788..95a04f768ae 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index f8ceee78ebb..95e19e0d5fe 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index bd6d697c90d..5854a5794c4 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -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) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 38ba0d7f96b..1ea5c18271a 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -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(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 &&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_() { diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index b94c3cac37c..fe69cd81d05 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -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 &&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 direction_text_sensors_{}; #endif + + LazyCallbackManager data_callback_; +}; + +class LD2450DataTrigger : public Trigger<> { + public: + explicit LD2450DataTrigger(LD2450Component *parent) { + parent->add_on_data_callback([this]() { this->trigger(); }); + } }; } // namespace esphome::ld2450 diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index c1f5635028c..d40343db33d 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 56bbc840fba..9f1ce47837c 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index d7b37f61300..e6831ad19e0 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -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(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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index c0d74cef4a8..8ac5a28d8f5 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -1,6 +1,11 @@ #include "pulse_counter_sensor.h" #include "esphome/core/log.h" +#ifdef HAS_PCNT +#include +#include +#endif + namespace esphome { namespace pulse_counter { @@ -56,103 +61,109 @@ pulse_counter_t BasicPulseCounterStorage::read_raw_value() { #ifdef HAS_PCNT bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { - static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0; - static pcnt_channel_t next_pcnt_channel = PCNT_CHANNEL_0; this->pin = pin; this->pin->setup(); - this->pcnt_unit = next_pcnt_unit; - this->pcnt_channel = next_pcnt_channel; - next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1); - if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) { - next_pcnt_unit = PCNT_UNIT_0; - next_pcnt_channel = pcnt_channel_t(int(next_pcnt_channel) + 1); + + pcnt_unit_config_t unit_config = { + .low_limit = INT16_MIN, + .high_limit = INT16_MAX, + .flags = {.accum_count = true}, + }; + esp_err_t error = pcnt_new_unit(&unit_config, &this->pcnt_unit); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Creating PCNT unit failed: %s", esp_err_to_name(error)); + return false; } - ESP_LOGCONFIG(TAG, - " PCNT Unit Number: %u\n" - " PCNT Channel Number: %u", - this->pcnt_unit, this->pcnt_channel); + pcnt_chan_config_t chan_config = { + .edge_gpio_num = this->pin->get_pin(), + .level_gpio_num = -1, + }; + error = pcnt_new_channel(this->pcnt_unit, &chan_config, &this->pcnt_channel); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Creating PCNT channel failed: %s", esp_err_to_name(error)); + return false; + } - pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS; + pcnt_channel_edge_action_t rising = PCNT_CHANNEL_EDGE_ACTION_HOLD; + pcnt_channel_edge_action_t falling = PCNT_CHANNEL_EDGE_ACTION_HOLD; switch (this->rising_edge_mode) { case PULSE_COUNTER_DISABLE: - rising = PCNT_COUNT_DIS; + rising = PCNT_CHANNEL_EDGE_ACTION_HOLD; break; case PULSE_COUNTER_INCREMENT: - rising = PCNT_COUNT_INC; + rising = PCNT_CHANNEL_EDGE_ACTION_INCREASE; break; case PULSE_COUNTER_DECREMENT: - rising = PCNT_COUNT_DEC; + rising = PCNT_CHANNEL_EDGE_ACTION_DECREASE; break; } switch (this->falling_edge_mode) { case PULSE_COUNTER_DISABLE: - falling = PCNT_COUNT_DIS; + falling = PCNT_CHANNEL_EDGE_ACTION_HOLD; break; case PULSE_COUNTER_INCREMENT: - falling = PCNT_COUNT_INC; + falling = PCNT_CHANNEL_EDGE_ACTION_INCREASE; break; case PULSE_COUNTER_DECREMENT: - falling = PCNT_COUNT_DEC; + falling = PCNT_CHANNEL_EDGE_ACTION_DECREASE; break; } - pcnt_config_t pcnt_config = { - .pulse_gpio_num = this->pin->get_pin(), - .ctrl_gpio_num = PCNT_PIN_NOT_USED, - .lctrl_mode = PCNT_MODE_KEEP, - .hctrl_mode = PCNT_MODE_KEEP, - .pos_mode = rising, - .neg_mode = falling, - .counter_h_lim = 0, - .counter_l_lim = 0, - .unit = this->pcnt_unit, - .channel = this->pcnt_channel, - }; - esp_err_t error = pcnt_unit_config(&pcnt_config); + error = pcnt_channel_set_edge_action(this->pcnt_channel, rising, falling); if (error != ESP_OK) { - ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Setting PCNT edge action failed: %s", esp_err_to_name(error)); return false; } if (this->filter_us != 0) { - uint16_t filter_val = std::min(static_cast(this->filter_us * 80u), 1023u); - ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val); - error = pcnt_set_filter_value(this->pcnt_unit, filter_val); + uint32_t apb_freq; + esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq); + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq; + pcnt_glitch_filter_config_t filter_config = { + .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), + }; + error = pcnt_unit_set_glitch_filter(this->pcnt_unit, &filter_config); if (error != ESP_OK) { - ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error)); - return false; - } - error = pcnt_filter_enable(this->pcnt_unit); - if (error != ESP_OK) { - ESP_LOGE(TAG, "Enabling filter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Setting PCNT glitch filter failed: %s", esp_err_to_name(error)); return false; } } - error = pcnt_counter_pause(this->pcnt_unit); + error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MIN); if (error != ESP_OK) { - ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Adding PCNT low limit watch point failed: %s", esp_err_to_name(error)); return false; } - error = pcnt_counter_clear(this->pcnt_unit); + error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MAX); if (error != ESP_OK) { - ESP_LOGE(TAG, "Clearing pulse counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Adding PCNT high limit watch point failed: %s", esp_err_to_name(error)); return false; } - error = pcnt_counter_resume(this->pcnt_unit); + + error = pcnt_unit_enable(this->pcnt_unit); if (error != ESP_OK) { - ESP_LOGE(TAG, "Resuming pulse counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Enabling PCNT unit failed: %s", esp_err_to_name(error)); + return false; + } + error = pcnt_unit_clear_count(this->pcnt_unit); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Clearing PCNT unit failed: %s", esp_err_to_name(error)); + return false; + } + error = pcnt_unit_start(this->pcnt_unit); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Starting PCNT unit failed: %s", esp_err_to_name(error)); return false; } return true; } pulse_counter_t HwPulseCounterStorage::read_raw_value() { - pulse_counter_t counter; - pcnt_get_counter_value(this->pcnt_unit, &counter); - pulse_counter_t ret = counter - this->last_value; - this->last_value = counter; + int count; + pcnt_unit_get_count(this->pcnt_unit, &count); + pulse_counter_t ret = count - this->last_value; + this->last_value = count; return ret; } #endif // HAS_PCNT diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index f906e9e5cb3..a7913d5d66b 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -6,14 +6,13 @@ #include -// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h) -// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the -// "driver" IDF component dependency. See: -// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6 -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) -#include +#if defined(USE_ESP32) +#include +#ifdef SOC_PCNT_SUPPORTED +#include #define HAS_PCNT -#endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) +#endif // SOC_PCNT_SUPPORTED +#endif // USE_ESP32 namespace esphome { namespace pulse_counter { @@ -24,11 +23,7 @@ enum PulseCounterCountMode { PULSE_COUNTER_DECREMENT, }; -#ifdef HAS_PCNT -using pulse_counter_t = int16_t; -#else // HAS_PCNT using pulse_counter_t = int32_t; -#endif // HAS_PCNT struct PulseCounterStorageBase { virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0; @@ -58,8 +53,8 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase { bool pulse_counter_setup(InternalGPIOPin *pin) override; pulse_counter_t read_raw_value() override; - pcnt_unit_t pcnt_unit; - pcnt_channel_t pcnt_channel; + pcnt_unit_handle_t pcnt_unit{nullptr}; + pcnt_channel_handle_t pcnt_channel{nullptr}; }; #endif // HAS_PCNT diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 65be5ee7936..01244635679 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -129,10 +129,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): use_pcnt = config.get(CONF_USE_PCNT) if CORE.is_esp32 and use_pcnt: - # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) - # Provides driver/pcnt.h header for hardware pulse counter API - # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) - include_builtin_idf_component("driver") + include_builtin_idf_component("esp_driver_pcnt") var = await sensor.new_sensor(config, use_pcnt) await cg.register_component(var, config) diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index d7242532564..7eb89d5b321 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -56,14 +56,14 @@ void PylontechComponent::setup() { void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::loop() { - int avail = this->available(); + 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 int recv = 0; uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index e4dbdf41cb6..d47347fcfa6 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index eda8365169a..f95244ea458 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -3,15 +3,11 @@ #ifdef USE_ESP32 #include +#include namespace esphome::remote_receiver { static const char *const TAG = "remote_receiver.esp32"; -#ifdef USE_ESP32_VARIANT_ESP32H2 -static const uint32_t RMT_CLK_FREQ = 32000000; -#else -static const uint32_t RMT_CLK_FREQ = 80000000; -#endif static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) { RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg; @@ -98,7 +94,10 @@ void RemoteReceiverComponent::setup() { } uint32_t event_size = sizeof(rmt_rx_done_event_data_t); - uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000); + uint32_t rmt_freq; + esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, + &rmt_freq); + uint32_t max_filter_ns = UINT8_MAX * 1000u / (rmt_freq / 1000000); memset(&this->store_.config, 0, sizeof(this->store_.config)); this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns); this->store_.config.signal_range_max_ns = this->idle_us_ * 1000; diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index 7036862d148..4e4705a8892 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import audio, esp32, speaker +from esphome.components import audio, esp32, socket, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -34,7 +34,7 @@ def _set_stream_limits(config): return config -def _validate_audio_compatability(config): +def _validate_audio_compatibility(config): inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER)(config) inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config) inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config) @@ -73,10 +73,13 @@ CONFIG_SCHEMA = cv.All( ) -FINAL_VALIDATE_SCHEMA = _validate_audio_compatability +FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility async def to_code(config): + # Enable wake_loop_threadsafe for immediate command processing from other tasks + socket.require_wake_loop_threadsafe() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await speaker.register_speaker(var, config) @@ -86,12 +89,11 @@ async def to_code(config): cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION])) - if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM): - cg.add(var.set_task_stack_in_psram(task_stack_in_psram)) - if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]: - esp32.add_idf_sdkconfig_option( - "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True - ) + if config.get(CONF_TASK_STACK_IN_PSRAM): + cg.add(var.set_task_stack_in_psram(True)) + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index ad61aca0841..74420f906a1 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -4,6 +4,8 @@ #include "esphome/components/audio/audio_resampler.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -17,13 +19,17 @@ static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1; static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50; -static const uint32_t TASK_DELAY_MS = 20; static const uint32_t TASK_STACK_SIZE = 3072; +static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000; + static const char *const TAG = "resampler_speaker"; enum ResamplingEventGroupBits : uint32_t { - COMMAND_STOP = (1 << 0), // stops the resampler task + COMMAND_STOP = (1 << 0), // signals stop request + COMMAND_START = (1 << 1), // signals start request + COMMAND_FINISH = (1 << 2), // signals finish request (graceful stop) + TASK_COMMAND_STOP = (1 << 5), // signals the task to stop STATE_STARTING = (1 << 10), STATE_RUNNING = (1 << 11), STATE_STOPPING = (1 << 12), @@ -34,9 +40,16 @@ enum ResamplingEventGroupBits : uint32_t { ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits }; +void ResamplerSpeaker::dump_config() { + ESP_LOGCONFIG(TAG, + "Resampler Speaker:\n" + " Target Bits Per Sample: %u\n" + " Target Sample Rate: %" PRIu32 " Hz", + this->target_bits_per_sample_, this->target_sample_rate_); +} + void ResamplerSpeaker::setup() { this->event_group_ = xEventGroupCreate(); - if (this->event_group_ == nullptr) { ESP_LOGE(TAG, "Failed to create event group"); this->mark_failed(); @@ -55,81 +68,155 @@ void ResamplerSpeaker::setup() { this->audio_output_callback_(new_frames, write_timestamp); } }); + + // Start with loop disabled since no task is running and no commands are pending + this->disable_loop(); } void ResamplerSpeaker::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); + // Process commands with priority: STOP > FINISH > START + // This ensures stop commands take precedence over conflicting start commands + if (event_group_bits & ResamplingEventGroupBits::COMMAND_STOP) { + if (this->state_ == speaker::STATE_RUNNING || this->state_ == speaker::STATE_STARTING) { + // Clear STOP, START, and FINISH bits - stop takes precedence + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP | + ResamplingEventGroupBits::COMMAND_START | + ResamplingEventGroupBits::COMMAND_FINISH); + this->waiting_for_output_ = false; + this->enter_stopping_state_(); + } else if (this->state_ == speaker::STATE_STOPPED) { + // Already stopped, just clear the command bits + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP | + ResamplingEventGroupBits::COMMAND_START | + ResamplingEventGroupBits::COMMAND_FINISH); + } + // Leave bits set if STATE_STOPPING - will be processed once stopped + } else if (event_group_bits & ResamplingEventGroupBits::COMMAND_FINISH) { + if (this->state_ == speaker::STATE_RUNNING) { + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH); + this->output_speaker_->finish(); + } else if (this->state_ == speaker::STATE_STOPPED) { + // Already stopped, just clear the command bit + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH); + } + // Leave bit set if transitioning states - will be processed once state allows + } else if (event_group_bits & ResamplingEventGroupBits::COMMAND_START) { + if (this->state_ == speaker::STATE_STOPPED) { + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START); + this->state_ = speaker::STATE_STARTING; + } else if (this->state_ == speaker::STATE_RUNNING) { + // Already running, just clear the command bit + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START); + } + // Leave bit set if transitioning states - will be processed once state allows + } + + // Re-read bits after command processing (enter_stopping_state_ may have set task bits) + event_group_bits = xEventGroupGetBits(this->event_group_); + if (event_group_bits & ResamplingEventGroupBits::STATE_STARTING) { - ESP_LOGD(TAG, "Starting resampler task"); + ESP_LOGD(TAG, "Starting"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STARTING); } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers")); + this->status_set_error(LOG_STR("Not enough memory")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); - this->state_ = speaker::STATE_STOPPING; + this->enter_stopping_state_(); } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream")); + this->status_set_error(LOG_STR("Unsupported stream")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); - this->state_ = speaker::STATE_STOPPING; + this->enter_stopping_state_(); } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) { - this->status_set_error(LOG_STR("Resampler task failed")); + this->status_set_error(LOG_STR("Resampler failure")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); - this->state_ = speaker::STATE_STOPPING; + this->enter_stopping_state_(); } if (event_group_bits & ResamplingEventGroupBits::STATE_RUNNING) { - ESP_LOGD(TAG, "Started resampler task"); + ESP_LOGV(TAG, "Started"); this->status_clear_error(); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_RUNNING); } if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPING) { - ESP_LOGD(TAG, "Stopping resampler task"); + ESP_LOGV(TAG, "Stopping"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING); } if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) { - if (this->delete_task_() == ESP_OK) { - ESP_LOGD(TAG, "Stopped resampler task"); - xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); - } + this->delete_task_(); + ESP_LOGD(TAG, "Stopped"); + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); } switch (this->state_) { case speaker::STATE_STARTING: { - esp_err_t err = this->start_(); - if (err == ESP_OK) { - this->status_clear_error(); - this->state_ = speaker::STATE_RUNNING; + if (!this->waiting_for_output_) { + esp_err_t err = this->start_(); + if (err == ESP_OK) { + this->callback_remainder_ = 0; // reset callback remainder + this->status_clear_error(); + this->waiting_for_output_ = true; + this->state_start_ms_ = App.get_loop_component_start_time(); + } else { + this->set_start_error_(err); + this->waiting_for_output_ = false; + this->enter_stopping_state_(); + } } else { - switch (err) { - case ESP_ERR_INVALID_STATE: - this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start")); - break; - case ESP_ERR_NO_MEM: - this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack")); - default: - this->status_set_error(LOG_STR("Failed to start resampler")); - break; + if (this->output_speaker_->is_running()) { + this->state_ = speaker::STATE_RUNNING; + this->waiting_for_output_ = false; + } else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) { + // Timed out waiting for the output speaker to start + this->waiting_for_output_ = false; + this->enter_stopping_state_(); } - - this->state_ = speaker::STATE_STOPPING; } break; } case speaker::STATE_RUNNING: if (this->output_speaker_->is_stopped()) { - this->state_ = speaker::STATE_STOPPING; + this->enter_stopping_state_(); + } + break; + case speaker::STATE_STOPPING: { + if ((this->output_speaker_->get_pause_state()) || + ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS)) { + // If output speaker is paused or stopping timeout exceeded, force stop + this->output_speaker_->stop(); } + if (this->output_speaker_->is_stopped() && (this->task_handle_ == nullptr)) { + // Only transition to stopped state once the output speaker and resampler task are fully stopped + this->waiting_for_output_ = false; + this->state_ = speaker::STATE_STOPPED; + } break; - case speaker::STATE_STOPPING: - this->stop_(); - this->state_ = speaker::STATE_STOPPED; - break; + } case speaker::STATE_STOPPED: + event_group_bits = xEventGroupGetBits(this->event_group_); + if (event_group_bits == 0) { + // No pending events, disable loop to save CPU cycles + this->disable_loop(); + } + break; + } +} + +void ResamplerSpeaker::set_start_error_(esp_err_t err) { + switch (err) { + case ESP_ERR_INVALID_STATE: + this->status_set_error(LOG_STR("Task failed to start")); + break; + case ESP_ERR_NO_MEM: + this->status_set_error(LOG_STR("Not enough memory")); + break; + default: + this->status_set_error(LOG_STR("Failed to start")); break; } } @@ -143,16 +230,33 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) { bytes_written = this->output_speaker_->play(data, length, ticks_to_wait); } else { - if (this->ring_buffer_.use_count() == 1) { - std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); + if (temp_ring_buffer) { + // Only write to the ring buffer if the reference is valid bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); + } else { + // Delay to avoid repeatedly hammering while waiting for the speaker to start + vTaskDelay(ticks_to_wait); } } return bytes_written; } -void ResamplerSpeaker::start() { this->state_ = speaker::STATE_STARTING; } +void ResamplerSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { + this->enable_loop_soon_any_context(); + uint32_t event_bits = xEventGroupGetBits(this->event_group_); + if (!(event_bits & command_bit)) { + xEventGroupSetBits(this->event_group_, command_bit); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + if (wake_loop) { + App.wake_loop_threadsafe(); + } +#endif + } +} + +void ResamplerSpeaker::start() { this->send_command_(ResamplingEventGroupBits::COMMAND_START, true); } esp_err_t ResamplerSpeaker::start_() { this->target_stream_info_ = audio::AudioStreamInfo( @@ -185,7 +289,7 @@ esp_err_t ResamplerSpeaker::start_task_() { } if (this->task_handle_ == nullptr) { - this->task_handle_ = xTaskCreateStatic(resample_task, "sample", TASK_STACK_SIZE, (void *) this, + this->task_handle_ = xTaskCreateStatic(resample_task, "resampler", TASK_STACK_SIZE, (void *) this, RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_); } @@ -196,43 +300,47 @@ esp_err_t ResamplerSpeaker::start_task_() { return ESP_OK; } -void ResamplerSpeaker::stop() { this->state_ = speaker::STATE_STOPPING; } +void ResamplerSpeaker::stop() { this->send_command_(ResamplingEventGroupBits::COMMAND_STOP); } -void ResamplerSpeaker::stop_() { +void ResamplerSpeaker::enter_stopping_state_() { + this->state_ = speaker::STATE_STOPPING; + this->state_start_ms_ = App.get_loop_component_start_time(); if (this->task_handle_ != nullptr) { - xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP); + xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::TASK_COMMAND_STOP); } this->output_speaker_->stop(); } -esp_err_t ResamplerSpeaker::delete_task_() { - if (!this->task_created_) { +void ResamplerSpeaker::delete_task_() { + if (this->task_handle_ != nullptr) { + // Delete the suspended task + vTaskDelete(this->task_handle_); this->task_handle_ = nullptr; - - if (this->task_stack_buffer_ != nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } - - this->task_stack_buffer_ = nullptr; - } - - return ESP_OK; } - return ESP_ERR_INVALID_STATE; + if (this->task_stack_buffer_ != nullptr) { + // Deallocate the task stack buffer + if (this->task_stack_in_psram_) { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); + stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); + } else { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); + stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); + } + + this->task_stack_buffer_ = nullptr; + } } -void ResamplerSpeaker::finish() { this->output_speaker_->finish(); } +void ResamplerSpeaker::finish() { this->send_command_(ResamplingEventGroupBits::COMMAND_FINISH); } bool ResamplerSpeaker::has_buffered_data() const { bool has_ring_buffer_data = false; - if (this->requires_resampling_() && (this->ring_buffer_.use_count() > 0)) { - has_ring_buffer_data = (this->ring_buffer_.lock()->available() > 0); + if (this->requires_resampling_()) { + std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); + if (temp_ring_buffer) { + has_ring_buffer_data = (temp_ring_buffer->available() > 0); + } } return (has_ring_buffer_data || this->output_speaker_->has_buffered_data()); } @@ -253,9 +361,8 @@ bool ResamplerSpeaker::requires_resampling_() const { } void ResamplerSpeaker::resample_task(void *params) { - ResamplerSpeaker *this_resampler = (ResamplerSpeaker *) params; + ResamplerSpeaker *this_resampler = static_cast(params); - this_resampler->task_created_ = true; xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING); std::unique_ptr resampler = @@ -269,7 +376,7 @@ void ResamplerSpeaker::resample_task(void *params) { std::shared_ptr temp_ring_buffer = RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); - if (temp_ring_buffer.use_count() == 0) { + if (!temp_ring_buffer) { err = ESP_ERR_NO_MEM; } else { this_resampler->ring_buffer_ = temp_ring_buffer; @@ -291,7 +398,7 @@ void ResamplerSpeaker::resample_task(void *params) { while (err == ESP_OK) { uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_); - if (event_bits & ResamplingEventGroupBits::COMMAND_STOP) { + if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) { break; } @@ -310,8 +417,8 @@ void ResamplerSpeaker::resample_task(void *params) { xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING); resampler.reset(); xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED); - this_resampler->task_created_ = false; - vTaskDelete(nullptr); + + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } } // namespace resampler diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index 810087ab7f4..c1ebd7e7b5b 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -8,14 +8,16 @@ #include "esphome/core/component.h" -#include #include +#include namespace esphome { namespace resampler { class ResamplerSpeaker : public Component, public speaker::Speaker { public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + void dump_config() override; void setup() override; void loop() override; @@ -65,13 +67,18 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { /// ESP_ERR_INVALID_STATE if the task wasn't created esp_err_t start_task_(); - /// @brief Stops the output speaker. If the resampling task is running, it sends the stop command. - void stop_(); + /// @brief Transitions to STATE_STOPPING, records the stopping timestamp, sends the task stop command if the task is + /// running, and stops the output speaker. + void enter_stopping_state_(); - /// @brief Deallocates the task stack and resets the pointers. - /// @return ESP_OK if successful - /// ESP_ERR_INVALID_STATE if the task hasn't stopped itself - esp_err_t delete_task_(); + /// @brief Sets the appropriate status error based on the start failure reason. + void set_start_error_(esp_err_t err); + + /// @brief Deletes the resampler task if suspended, deallocates the task stack, and resets the related pointers. + void delete_task_(); + + /// @brief Sends a command via event group bits, enables the loop, and optionally wakes the main loop. + void send_command_(uint32_t command_bit, bool wake_loop = false); inline bool requires_resampling_() const; static void resample_task(void *params); @@ -83,7 +90,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { speaker::Speaker *output_speaker_{nullptr}; bool task_stack_in_psram_{false}; - bool task_created_{false}; + bool waiting_for_output_{false}; TaskHandle_t task_handle_{nullptr}; StaticTask_t task_stack_; @@ -98,6 +105,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { uint32_t target_sample_rate_; uint32_t buffer_duration_ms_; + uint32_t state_start_ms_{0}; uint64_t callback_remainder_{0}; }; diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index e33c13aafe8..d8c148145ce 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 3f2103b4013..99d519b434d 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index d95e13241d3..12f188fe03c 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 441ee2b5c27..5d571618d33 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 5f96155fbf0..71f98c826a7 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -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 diff --git a/esphome/components/template/water_heater/automation.h b/esphome/components/template/water_heater/automation.h index 3dad2b85ae9..d19542db41c 100644 --- a/esphome/components/template/water_heater/automation.h +++ b/esphome/components/template/water_heater/automation.h @@ -11,12 +11,15 @@ class TemplateWaterHeaterPublishAction : public Action, public Parentedcurrent_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, public Parentedmode_.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(); diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index c354deee0e3..57c76286a0d 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -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_) { diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h index 22173209aaa..045a142e406 100644 --- a/esphome/components/template/water_heater/template_water_heater.h +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -24,6 +24,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { this->target_temperature_f_.set(std::forward(f)); } template void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward(f)); } + template void set_away_lambda(F &&f) { this->away_f_.set(std::forward(f)); } + template void set_is_on_lambda(F &&f) { this->is_on_f_.set(std::forward(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 current_temperature_f_; TemplateLambda target_temperature_f_; TemplateLambda mode_f_; + TemplateLambda away_f_; + TemplateLambda is_on_f_; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; water_heater::WaterHeaterModeMask supported_modes_; bool optimistic_{true}; diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 9ee4c09b860..a1acbf2f56c 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -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(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index b4a2c9b9097..f39ea9b3ae4 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: espressif/mdns: version: 1.9.1 espressif/esp_wifi_remote: - version: 1.2.4 + version: 1.3.2 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: @@ -18,7 +18,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.9.3 + version: 2.11.5 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: diff --git a/platformio.ini b/platformio.ini index d198862a255..94b1f1a727e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,6 +136,7 @@ extends = common:arduino platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = diff --git a/requirements.txt b/requirements.txt index 1771867535a..a0a29ad30a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tests/components/ld2450/common.yaml b/tests/components/ld2450/common.yaml index cfa3c922fc9..617228ca34a 100644 --- a/tests/components/ld2450/common.yaml +++ b/tests/components/ld2450/common.yaml @@ -1,5 +1,8 @@ ld2450: - id: ld2450_radar + on_data: + then: + - logger.log: "LD2450 Radar Data Received" button: - platform: ld2450 diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index b8742f8c7b1..e9ddfcf43e4 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -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 diff --git a/tests/integration/fixtures/water_heater_template.yaml b/tests/integration/fixtures/water_heater_template.yaml index 1aaded19913..0c82ff68cea 100644 --- a/tests/integration/fixtures/water_heater_template.yaml +++ b/tests/integration/fixtures/water_heater_template.yaml @@ -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(); diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index 6b4a685d0d5..096d4c84615 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -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