diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index a77e17afce1..1e9ca66ed49 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import defaultdict from collections.abc import Callable +import json import sys from typing import TYPE_CHECKING @@ -438,6 +439,28 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): return "\n".join(lines) + def to_json(self) -> str: + """Export analysis results as JSON.""" + data = { + "components": { + name: { + "text": mem.text_size, + "rodata": mem.rodata_size, + "data": mem.data_size, + "bss": mem.bss_size, + "flash_total": mem.flash_total, + "ram_total": mem.ram_total, + "symbol_count": mem.symbol_count, + } + for name, mem in self.components.items() + }, + "totals": { + "flash": sum(c.flash_total for c in self.components.values()), + "ram": sum(c.ram_total for c in self.components.values()), + }, + } + return json.dumps(data, indent=2) + def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: """Dump uncategorized symbols for analysis.""" # Sort by size descending diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index fec34c5b2ab..0d75bcb58ed 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -3,6 +3,7 @@ #include "bedjet_hub.h" #include "bedjet_child.h" #include "bedjet_const.h" +#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/core/application.h" #include diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index bf65ae67c02..8d88a10b27a 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -96,10 +96,16 @@ void CaptivePortal::start() { } void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { - if (req->url() == ESPHOME_F("/config.json")) { +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + StringRef url = req->url_to(url_buf); +#else + const auto &url = req->url(); +#endif + if (url == ESPHOME_F("/config.json")) { this->handle_config(req); return; - } else if (req->url() == ESPHOME_F("/wifisave")) { + } else if (url == ESPHOME_F("/wifisave")) { this->handle_wifisave(req); return; } diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index ccbeedcd2fd..701cbd8a6d1 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -63,11 +63,13 @@ def validate_auto_clear(value): return cv.boolean(value) -BASIC_DISPLAY_SCHEMA = cv.Schema( - { - cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_, - } -).extend(cv.polling_component_schema("1s")) +def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema: + """Create a basic display schema with configurable default update interval.""" + return cv.Schema( + { + cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_, + } + ).extend(cv.polling_component_schema(default_update_interval)) def _validate_test_card(config): @@ -81,34 +83,41 @@ def _validate_test_card(config): return config -FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( - { - cv.Optional(CONF_ROTATION): validate_rotation, - cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All( - cv.ensure_list( +def full_display_schema(default_update_interval: str = "1s") -> cv.Schema: + """Create a full display schema with configurable default update interval.""" + schema = basic_display_schema(default_update_interval).extend( + { + cv.Optional(CONF_ROTATION): validate_rotation, + cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All( + cv.ensure_list( + { + cv.GenerateID(): cv.declare_id(DisplayPage), + cv.Required(CONF_LAMBDA): cv.lambda_, + } + ), + cv.Length(min=1), + ), + cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation( { - cv.GenerateID(): cv.declare_id(DisplayPage), - cv.Required(CONF_LAMBDA): cv.lambda_, + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayOnPageChangeTrigger + ), + cv.Optional(CONF_FROM): cv.use_id(DisplayPage), + cv.Optional(CONF_TO): cv.use_id(DisplayPage), } ), - cv.Length(min=1), - ), - cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DisplayOnPageChangeTrigger - ), - cv.Optional(CONF_FROM): cv.use_id(DisplayPage), - cv.Optional(CONF_TO): cv.use_id(DisplayPage), - } - ), - cv.Optional( - CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED - ): validate_auto_clear, - cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, - } -) -FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card) + cv.Optional( + CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED + ): validate_auto_clear, + cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, + } + ) + schema.add_extra(_validate_test_card) + return schema + + +BASIC_DISPLAY_SCHEMA = basic_display_schema("1s") +FULL_DISPLAY_SCHEMA = full_display_schema("1s") async def setup_display_core_(var, config): diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 8cc7b2663c6..8f7edc69a8e 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -31,6 +31,7 @@ from esphome.const import ( CONF_TRANSFORM, CONF_UPDATE_INTERVAL, CONF_WIDTH, + SCHEDULER_DONT_RUN, ) from esphome.cpp_generator import RawExpression from esphome.final_validate import full_config @@ -72,12 +73,10 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} def model_schema(config): model = MODELS[config[CONF_MODEL]] class_name = epaper_spi_ns.class_(model.class_name, EPaperBase) - minimum_update_interval = update_interval( - model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s") - ) cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required return ( - display.FULL_DISPLAY_SCHEMA.extend( + display.full_display_schema("60s") + .extend( spi.spi_device_schema( cs_pin_required=False, default_mode="MODE0", @@ -94,9 +93,6 @@ def model_schema(config): { cv.Optional(CONF_ROTATION, default=0): validate_rotation, cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), - cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( - update_interval, cv.Range(min=minimum_update_interval) - ), cv.Optional(CONF_TRANSFORM): cv.Schema( { cv.Required(CONF_MIRROR_X): cv.boolean, @@ -150,15 +146,22 @@ def _final_validate(config): global_config = full_config.get() from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN - if CONF_LAMBDA not in config and CONF_PAGES not in config: - if LVGL_DOMAIN in global_config: - if CONF_UPDATE_INTERVAL not in config: - config[CONF_UPDATE_INTERVAL] = update_interval("never") - else: - # If no drawing methods are configured, and LVGL is not enabled, show a test card - config[CONF_SHOW_TEST_CARD] = True - elif CONF_UPDATE_INTERVAL not in config: - config[CONF_UPDATE_INTERVAL] = update_interval("1min") + # If no drawing methods are configured, and LVGL is not enabled, show a test card + if ( + CONF_LAMBDA not in config + and CONF_PAGES not in config + and LVGL_DOMAIN not in global_config + ): + config[CONF_SHOW_TEST_CARD] = True + + interval = config[CONF_UPDATE_INTERVAL] + if interval != SCHEDULER_DONT_RUN: + model = MODELS[config[CONF_MODEL]] + minimum = update_interval(model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")) + if interval < minimum: + raise cv.Invalid( + f"update_interval must be at least {minimum} for {model.name}, got {interval}" + ) return config diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index da49fdc0700..f5b58e5592f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -676,6 +676,9 @@ CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" KEY_VFS_SELECT_REQUIRED = "vfs_select_required" KEY_VFS_DIR_REQUIRED = "vfs_dir_required" +# Ring buffer IRAM requirement tracking +KEY_RINGBUF_IN_IRAM = "ringbuf_in_iram" + def require_vfs_select() -> None: """Mark that VFS select support is required by a component. @@ -695,6 +698,17 @@ def require_vfs_dir() -> None: CORE.data[KEY_VFS_DIR_REQUIRED] = True +def enable_ringbuf_in_iram() -> None: + """Keep ring buffer functions in IRAM instead of moving them to flash. + + Call this from components that use esphome/core/ring_buffer.cpp and need + the ring buffer functions to remain in IRAM for performance reasons + (e.g., voice assistants, audio components). + This prevents CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH from being enabled. + """ + CORE.data[KEY_RINGBUF_IN_IRAM] = True + + def _parse_idf_component(value: str) -> ConfigType: """Parse IDF component shorthand syntax like 'owner/component^version'""" # Match operator followed by version-like string (digit or *) @@ -1118,14 +1132,18 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True) # Place ring buffer functions into flash instead of IRAM by default - # This saves IRAM. In ESP-IDF 6.0 flash placement becomes the default. - # Users can set ringbuf_in_iram: true as an escape hatch if they encounter issues. - if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM]: - # User requests ring buffer in IRAM + # This saves IRAM but may impact performance for audio/voice components. + # Components that need ring buffer in IRAM call enable_ringbuf_in_iram(). + # Users can also set ringbuf_in_iram: true to force IRAM placement. + # In ESP-IDF 6.0 flash placement becomes the default. + if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM] or CORE.data.get( + KEY_RINGBUF_IN_IRAM, False + ): + # User config or component requires ring buffer in IRAM for performance # IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False) else: - # Place in flash to save IRAM (default) + # No component needs it - place in flash to save IRAM add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True) # Place heap functions into flash to save IRAM (~4-6KB savings) diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 4b53d3a1721..84f0cd80424 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -85,7 +85,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi break; } gpio_set_intr_type(this->get_pin_num(), idf_type); - gpio_intr_enable(this->get_pin_num()); if (!isr_service_installed) { auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3); if (res != ESP_OK) { @@ -95,6 +94,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi isr_service_installed = true; } gpio_isr_handler_add(this->get_pin_num(), func, arg); + gpio_intr_enable(this->get_pin_num()); } size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const { diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 4e0bb68133f..3d21ec25b85 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -19,16 +19,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; - std::unique_ptr data; - size_t len; - - void set_data(const uint8_t *src, size_t size) { - if (!this->data || this->len != size) { - this->data = std::make_unique(size); - this->len = size; - } - memcpy(this->data.get(), src, size); - } + SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -41,14 +32,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - obj.set_data(data, len); + obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; - save.set_data(data, len); - s_pending_save.emplace_back(std::move(save)); + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } @@ -56,11 +47,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - if (obj.len != len) { + if (obj.data.size() != len) { // size mismatch return false; } - memcpy(data, obj.data.get(), len); + memcpy(data, obj.data.data(), len); return true; } } @@ -136,10 +127,10 @@ class ESP32Preferences : public ESPPreferences { snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); if (this->is_changed_(this->nvs_handle, save, key_str)) { - esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len); - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); + esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size()); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err)); failed++; last_err = err; last_key = save.key; @@ -147,7 +138,7 @@ class ESP32Preferences : public ESPPreferences { } written++; } else { - ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); + ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -178,7 +169,7 @@ class ESP32Preferences : public ESPPreferences { return true; } // Check size first before allocating memory - if (actual_len != to_save.len) { + if (actual_len != to_save.data.size()) { return true; } // Most preferences are small, use stack buffer with heap fallback for large ones @@ -188,7 +179,7 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return true; } - return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0; + return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0; } bool reset() override { diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 87b5e2b7383..29d79a0ae31 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -98,6 +98,10 @@ void ESP32BLE::advertising_set_service_data(const std::vector &data) { } void ESP32BLE::advertising_set_manufacturer_data(const std::vector &data) { + this->advertising_set_manufacturer_data(std::span(data)); +} + +void ESP32BLE::advertising_set_manufacturer_data(std::span data) { this->advertising_init_(); this->advertising_->set_manufacturer_data(data); this->advertising_start(); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 1999c870f8b..a56d28b11fe 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -118,6 +118,7 @@ class ESP32BLE : public Component { void advertising_start(); void advertising_set_service_data(const std::vector &data); void advertising_set_manufacturer_data(const std::vector &data); + void advertising_set_manufacturer_data(std::span data); void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } void advertising_set_service_data_and_name(std::span data, bool include_name); void advertising_add_service_uuid(ESPBTUUID uuid); diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 68704e49e2e..da105051ebe 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -59,6 +59,10 @@ void BLEAdvertising::set_service_data(const std::vector &data) { } void BLEAdvertising::set_manufacturer_data(const std::vector &data) { + this->set_manufacturer_data(std::span(data)); +} + +void BLEAdvertising::set_manufacturer_data(std::span data) { delete[] this->advertising_data_.p_manufacturer_data; this->advertising_data_.p_manufacturer_data = nullptr; this->advertising_data_.manufacturer_len = data.size(); diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index d7f1eeac9dd..66ff7984d36 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -37,6 +37,7 @@ class BLEAdvertising { void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } void set_manufacturer_data(const std::vector &data); + void set_manufacturer_data(std::span data); void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; } void set_service_data(const std::vector &data); void set_service_data(std::span data); diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index f2aa7e762ef..6f7c0fa389e 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -1,5 +1,6 @@ #include "esp32_ble_beacon.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #ifdef USE_ESP32 diff --git a/esphome/components/esp32_ble_server/ble_server_automations.cpp b/esphome/components/esp32_ble_server/ble_server_automations.cpp index 0761de994a2..74cf56a7650 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.cpp +++ b/esphome/components/esp32_ble_server/ble_server_automations.cpp @@ -15,7 +15,10 @@ Trigger, uint16_t> *BLETriggers::create_characteristic_on_w Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) new Trigger, uint16_t>(); characteristic->on_write([on_write_trigger](std::span data, uint16_t id) { - // Convert span to vector for trigger + // Convert span to vector for trigger - copy is necessary because: + // 1. Trigger stores the data for use in automation actions that execute later + // 2. The span is only valid during this callback (points to temporary BLE stack data) + // 3. User lambdas in automations need persistent data they can access asynchronously on_write_trigger->trigger(std::vector(data.begin(), data.end()), id); }); return on_write_trigger; @@ -27,7 +30,10 @@ Trigger, uint16_t> *BLETriggers::create_descriptor_on_write Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) new Trigger, uint16_t>(); descriptor->on_write([on_write_trigger](std::span data, uint16_t id) { - // Convert span to vector for trigger + // Convert span to vector for trigger - copy is necessary because: + // 1. Trigger stores the data for use in automation actions that execute later + // 2. The span is only valid during this callback (points to temporary BLE stack data) + // 3. User lambdas in automations need persistent data they can access asynchronously on_write_trigger->trigger(std::vector(data.begin(), data.end()), id); }); return on_write_trigger; diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 70f8ce12049..9002a088e5c 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -802,8 +802,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)]; -#endif ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); +#endif /* * Bit 7 is `RMII Reference Clock Select`. Default is `0`. @@ -820,8 +820,10 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); +#endif } } #endif // USE_ETHERNET_KSZ8081 diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 02fde730eb5..799e9ba233a 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -71,7 +71,7 @@ void FanCall::validate_() { auto traits = this->parent_.get_traits(); if (this->speed_.has_value()) { - this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); + this->speed_ = clamp(*this->speed_, 1, static_cast(traits.supported_speed_count())); // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes // "Manually setting a speed must disable any set preset mode" diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index c0c5f34c504..4014c8cd3bc 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -11,7 +11,7 @@ namespace fan { class FanTraits { public: FanTraits() = default; - FanTraits(bool oscillation, bool speed, bool direction, int speed_count) + FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count) : oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {} /// Return if this fan supports oscillation. @@ -23,9 +23,9 @@ class FanTraits { /// Set whether this fan supports speed levels. void set_speed(bool speed) { this->speed_ = speed; } /// Return how many speed levels the fan has - int supported_speed_count() const { return this->speed_count_; } + uint8_t supported_speed_count() const { return this->speed_count_; } /// Set how many speed levels this fan has. - void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; } + void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; } /// Return if this fan supports changing direction bool supports_direction() const { return this->direction_; } /// Set whether this fan supports changing direction @@ -64,7 +64,7 @@ class FanTraits { bool oscillation_{false}; bool speed_{false}; bool direction_{false}; - int speed_count_{}; + uint8_t speed_count_{}; std::vector preset_modes_{}; }; diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index 633ccea66b5..fc400c5dd19 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -9,30 +9,56 @@ from esphome.const import ( CONF_VALUE, ) from esphome.core import CoroPriority, coroutine_with_priority +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] globals_ns = cg.esphome_ns.namespace("globals") GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component) -RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component) +RestoringGlobalsComponent = globals_ns.class_( + "RestoringGlobalsComponent", cg.PollingComponent +) RestoringGlobalStringComponent = globals_ns.class_( - "RestoringGlobalStringComponent", cg.Component + "RestoringGlobalStringComponent", cg.PollingComponent ) GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action) CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length" +# Base schema fields shared by both variants +_BASE_SCHEMA = { + cv.Required(CONF_ID): cv.declare_id(GlobalsComponent), + cv.Required(CONF_TYPE): cv.string_strict, + cv.Optional(CONF_INITIAL_VALUE): cv.string_strict, + cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254), +} -MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( +# Non-restoring globals: regular Component (no polling needed) +_NON_RESTORING_SCHEMA = cv.Schema( { - cv.Required(CONF_ID): cv.declare_id(GlobalsComponent), - cv.Required(CONF_TYPE): cv.string_strict, - cv.Optional(CONF_INITIAL_VALUE): cv.string_strict, + **_BASE_SCHEMA, cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, - cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254), } ).extend(cv.COMPONENT_SCHEMA) +# Restoring globals: PollingComponent with configurable update_interval +_RESTORING_SCHEMA = cv.Schema( + { + **_BASE_SCHEMA, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + } +).extend(cv.polling_component_schema("1s")) + + +def _globals_schema(config: ConfigType) -> ConfigType: + """Select schema based on restore_value setting.""" + if config.get(CONF_RESTORE_VALUE, False): + return _RESTORING_SCHEMA(config) + return _NON_RESTORING_SCHEMA(config) + + +MULTI_CONF = True +CONFIG_SCHEMA = _globals_schema + # Run with low priority so that namespaces are registered first @coroutine_with_priority(CoroPriority.LATE) diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 1d2a08937ee..3db29bea356 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -5,8 +5,7 @@ #include "esphome/core/helpers.h" #include -namespace esphome { -namespace globals { +namespace esphome::globals { template class GlobalsComponent : public Component { public: @@ -24,13 +23,14 @@ template class GlobalsComponent : public Component { T value_{}; }; -template class RestoringGlobalsComponent : public Component { +template class RestoringGlobalsComponent : public PollingComponent { public: using value_type = T; - explicit RestoringGlobalsComponent() = default; - explicit RestoringGlobalsComponent(T initial_value) : value_(initial_value) {} + explicit RestoringGlobalsComponent() : PollingComponent(1000) {} + explicit RestoringGlobalsComponent(T initial_value) : PollingComponent(1000), value_(initial_value) {} explicit RestoringGlobalsComponent( - std::array::type, std::extent::value> initial_value) { + std::array::type, std::extent::value> initial_value) + : PollingComponent(1000) { memcpy(this->value_, initial_value.data(), sizeof(T)); } @@ -44,7 +44,7 @@ template class RestoringGlobalsComponent : public Component { float get_setup_priority() const override { return setup_priority::HARDWARE; } - void loop() override { store_value_(); } + void update() override { store_value_(); } void on_shutdown() override { store_value_(); } @@ -66,13 +66,14 @@ template class RestoringGlobalsComponent : public Component { }; // Use with string or subclasses of strings -template class RestoringGlobalStringComponent : public Component { +template class RestoringGlobalStringComponent : public PollingComponent { public: using value_type = T; - explicit RestoringGlobalStringComponent() = default; - explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; } + explicit RestoringGlobalStringComponent() : PollingComponent(1000) {} + explicit RestoringGlobalStringComponent(T initial_value) : PollingComponent(1000) { this->value_ = initial_value; } explicit RestoringGlobalStringComponent( - std::array::type, std::extent::value> initial_value) { + std::array::type, std::extent::value> initial_value) + : PollingComponent(1000) { memcpy(this->value_, initial_value.data(), sizeof(T)); } @@ -90,7 +91,7 @@ template class RestoringGlobalStringComponent : public C float get_setup_priority() const override { return setup_priority::HARDWARE; } - void loop() override { store_value_(); } + void update() override { store_value_(); } void on_shutdown() override { store_value_(); } @@ -144,5 +145,4 @@ template T &id(GlobalsComponent *value) { return value->value(); template T &id(RestoringGlobalsComponent *value) { return value->value(); } template T &id(RestoringGlobalStringComponent *value) { return value->value(); } -} // namespace globals -} // namespace esphome +} // namespace esphome::globals diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 31a20a8981f..7a86c1b6b7a 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -39,7 +39,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( DECAY_MODE_OPTIONS, upper=True ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255), cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index ec1e8ada0e1..f6ffabfd308 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -15,7 +15,7 @@ enum DecayMode { class HBridgeFan : public Component, public fan::Fan { public: - HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {} + HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {} void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } @@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan { output::FloatOutput *pin_b_; output::FloatOutput *enable_{nullptr}; output::BinaryOutput *oscillating_{nullptr}; - int speed_count_{}; + uint8_t speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; std::vector preset_modes_{}; diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 191c849aa38..da4e3215b31 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -119,7 +119,7 @@ void IDFI2CBus::dump_config() { if (s.second) { ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first); } else { - ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); + ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first); } } } diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index d3128c5f4ca..734e7d28330 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,6 +1,11 @@ from esphome import pins import esphome.codegen as cg from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + enable_ringbuf_in_iram, + get_esp32_variant, +) +from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32C5, @@ -10,8 +15,6 @@ from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, - add_idf_sdkconfig_option, - get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE @@ -278,6 +281,9 @@ async def to_code(config): # Helps avoid callbacks being skipped due to processor load add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) + # Keep ring buffer functions in IRAM for audio performance + enable_ringbuf_in_iram() + cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) if CONF_I2S_BCLK_PIN in config: cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp index 1b772d062ca..b2fbeb829b3 100644 --- a/esphome/components/kuntze/kuntze.cpp +++ b/esphome/components/kuntze/kuntze.cpp @@ -11,7 +11,7 @@ static const char *const TAG = "kuntze"; static const uint8_t CMD_READ_REG = 0x03; static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832}; -// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5) +// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes) static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8; void Kuntze::on_modbus_data(const std::vector &data) { diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 978dcce3fa9..fbe354a09b4 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -18,16 +18,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; - std::unique_ptr data; - size_t len; - - void set_data(const uint8_t *src, size_t size) { - if (!this->data || this->len != size) { - this->data = std::make_unique(size); - this->len = size; - } - memcpy(this->data.get(), src, size); - } + SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -42,14 +33,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - obj.set_data(data, len); + obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; - save.set_data(data, len); - s_pending_save.emplace_back(std::move(save)); + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } @@ -58,11 +49,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - if (obj.len != len) { + if (obj.data.size() != len) { // size mismatch return false; } - memcpy(data, obj.data.get(), len); + memcpy(data, obj.data.data(), len); return true; } } @@ -126,11 +117,11 @@ class LibreTinyPreferences : public ESPPreferences { snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); if (this->is_changed_(&this->db, save, key_str)) { - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); - fdb_blob_make(&this->blob, save.data.get(), save.len); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); + fdb_blob_make(&this->blob, save.data.data(), save.data.size()); fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob); if (err != FDB_NO_ERR) { - ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err); + ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err); failed++; last_err = err; last_key = save.key; @@ -138,7 +129,7 @@ class LibreTinyPreferences : public ESPPreferences { } written++; } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); + ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -162,7 +153,7 @@ class LibreTinyPreferences : public ESPPreferences { } // Check size first - if different, data has changed - if (kv.value_len != to_save.len) { + if (kv.value_len != to_save.data.size()) { return true; } @@ -176,7 +167,7 @@ class LibreTinyPreferences : public ESPPreferences { } // Compare the actual data - return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0; + return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0; } bool reset() override { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index fe9cab4993f..d37a545cfb4 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -234,6 +234,7 @@ class Logger : public Component { #endif protected: + void write_msg_(const char *msg, size_t len); // RAII guard for recursion flags - sets flag on construction, clears on destruction class RecursionGuard { public: @@ -260,7 +261,6 @@ class Logger : public Component { #endif #endif void process_messages_(); - void write_msg_(const char *msg, size_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) diff --git a/esphome/components/logger/logger_esp8266.cpp.bak b/esphome/components/logger/logger_esp8266.cpp.bak new file mode 100644 index 00000000000..5063d88b927 --- /dev/null +++ b/esphome/components/logger/logger_esp8266.cpp.bak @@ -0,0 +1,51 @@ +#ifdef USE_ESP8266 +#include "logger.h" +#include "esphome/core/log.h" + +namespace esphome::logger { + +static const char *const TAG = "logger"; + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + switch (this->uart_) { + case UART_SELECTION_UART0: + case UART_SELECTION_UART0_SWAP: + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + if (this->uart_ == UART_SELECTION_UART0_SWAP) { + Serial.swap(); + } + Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); + break; + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); + break; + } + } else { + uart_set_debug(UART_NO); + } + + global_logger = this; + + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } + +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); + case UART_SELECTION_UART0_SWAP: + default: + return LOG_STR("UART0_SWAP"); + } +} + +} // namespace esphome::logger +#endif diff --git a/esphome/components/logger/logger_host.cpp.bak b/esphome/components/logger/logger_host.cpp.bak new file mode 100644 index 00000000000..4abe92286a4 --- /dev/null +++ b/esphome/components/logger/logger_host.cpp.bak @@ -0,0 +1,22 @@ +#if defined(USE_HOST) +#include "logger.h" + +namespace esphome::logger { + +void HOT Logger::write_msg_(const char *msg) { + time_t rawtime; + struct tm *timeinfo; + char buffer[80]; + + time(&rawtime); + timeinfo = localtime(&rawtime); + strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo); + fputs(buffer, stdout); + puts(msg); +} + +void Logger::pre_setup() { global_logger = this; } + +} // namespace esphome::logger + +#endif diff --git a/esphome/components/logger/logger_libretiny.cpp.bak b/esphome/components/logger/logger_libretiny.cpp.bak new file mode 100644 index 00000000000..3edfa744800 --- /dev/null +++ b/esphome/components/logger/logger_libretiny.cpp.bak @@ -0,0 +1,70 @@ +#ifdef USE_LIBRETINY +#include "logger.h" + +namespace esphome::logger { + +static const char *const TAG = "logger"; + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + switch (this->uart_) { +#if LT_HW_UART0 + case UART_SELECTION_UART0: + this->hw_serial_ = &Serial0; + Serial0.begin(this->baud_rate_); + break; +#endif +#if LT_HW_UART1 + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + break; +#endif +#if LT_HW_UART2 + case UART_SELECTION_UART2: + this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); + break; +#endif + default: + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + if (this->uart_ != UART_SELECTION_DEFAULT) { + ESP_LOGW(TAG, " The chosen logger UART port is not available on this board." + "The default port was used instead."); + } + break; + } + + // change lt_log() port to match default Serial + if (this->uart_ == UART_SELECTION_DEFAULT) { + this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1); + lt_log_set_port(LT_UART_DEFAULT_SERIAL); + } else { + lt_log_set_port(this->uart_ - 1); + } + } + + global_logger = this; + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } + +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_DEFAULT: + return LOG_STR("DEFAULT"); + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); + case UART_SELECTION_UART2: + default: + return LOG_STR("UART2"); + } +} + +} // namespace esphome::logger + +#endif // USE_LIBRETINY diff --git a/esphome/components/logger/logger_rp2040.cpp.bak b/esphome/components/logger/logger_rp2040.cpp.bak new file mode 100644 index 00000000000..63727c2cda9 --- /dev/null +++ b/esphome/components/logger/logger_rp2040.cpp.bak @@ -0,0 +1,48 @@ +#ifdef USE_RP2040 +#include "logger.h" +#include "esphome/core/log.h" + +namespace esphome::logger { + +static const char *const TAG = "logger"; + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + switch (this->uart_) { + case UART_SELECTION_UART0: + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + break; + case UART_SELECTION_UART1: + this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); + break; + case UART_SELECTION_USB_CDC: + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + break; + } + } + global_logger = this; + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } + +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); +#endif + default: + return LOG_STR("UNKNOWN"); + } +} + +} // namespace esphome::logger +#endif // USE_RP2040 diff --git a/esphome/components/logger/logger_zephyr.cpp.bak b/esphome/components/logger/logger_zephyr.cpp.bak new file mode 100644 index 00000000000..fb0c7dcca37 --- /dev/null +++ b/esphome/components/logger/logger_zephyr.cpp.bak @@ -0,0 +1,96 @@ +#ifdef USE_ZEPHYR + +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "logger.h" + +#include +#include +#include + +namespace esphome::logger { + +static const char *const TAG = "logger"; + +#ifdef USE_LOGGER_USB_CDC +void Logger::loop() { + if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) { + return; + } + static bool opened = false; + uint32_t dtr = 0; + uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); + + /* Poll if the DTR flag was set, optional */ + if (opened == dtr) { + return; + } + + if (!opened) { + App.schedule_dump_config(); + } + opened = !opened; +} +#endif + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + static const struct device *uart_dev = nullptr; + switch (this->uart_) { + case UART_SELECTION_UART0: + uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0)); + break; + case UART_SELECTION_UART1: + uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1)); + break; +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0)); + if (device_is_ready(uart_dev)) { + usb_enable(nullptr); + } + break; +#endif + } + if (!device_is_ready(uart_dev)) { + ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_())); + } else { + this->uart_dev_ = uart_dev; + } + } + global_logger = this; + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { +#ifdef CONFIG_PRINTK + printk("%s\n", msg); +#endif + if (nullptr == this->uart_dev_) { + return; + } + while (*msg) { + uart_poll_out(this->uart_dev_, *msg); + ++msg; + } + uart_poll_out(this->uart_dev_, '\n'); +} + +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); +#endif + default: + return LOG_STR("UNKNOWN"); + } +} + +} // namespace esphome::logger + +#endif diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 56b2ecf9f4e..249c92e68c9 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) { this->update_reg_(pin, false, iodir); } } -float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; } +float MCP23016::get_setup_priority() const { return setup_priority::IO; } bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) { if (this->is_failed()) return false; diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 74696584da9..52c4a219ea6 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -448,6 +448,9 @@ async def to_code(config): # The inference task queues detection events that need immediate processing socket.require_wake_loop_threadsafe() + # Keep ring buffer functions in IRAM for audio performance + esp32.enable_ringbuf_in_iram() + mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 715e6feed8e..fbdc6dce237 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -119,7 +119,8 @@ bool MQTTAlarmControlPanelComponent::publish_state() { default: state_s = "unknown"; } - return this->publish(this->get_state_topic_(), state_s); + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish(this->get_state_topic_to_(topic_buf), state_s); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 7cbb5dcc0e8..75995f61e06 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -52,8 +52,9 @@ bool MQTTBinarySensorComponent::publish_state(bool state) { if (this->binary_sensor_->is_status_binary_sensor()) return true; + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; const char *state_s = state ? "ON" : "OFF"; - return this->publish(this->get_state_topic_(), state_s); + return this->publish(this->get_state_topic_to_(topic_buf), state_s); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 7607a4e817e..aec6140e3f1 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -132,17 +132,29 @@ std::string MQTTComponent::get_command_topic_() const { } bool MQTTComponent::publish(const std::string &topic, const std::string &payload) { - return this->publish(topic, payload.data(), payload.size()); + return this->publish(topic.c_str(), payload.data(), payload.size()); } bool MQTTComponent::publish(const std::string &topic, const char *payload, size_t payload_length) { - if (topic.empty()) + return this->publish(topic.c_str(), payload, payload_length); +} + +bool MQTTComponent::publish(const char *topic, const char *payload, size_t payload_length) { + if (topic[0] == '\0') return false; return global_mqtt_client->publish(topic, payload, payload_length, this->qos_, this->retain_); } +bool MQTTComponent::publish(const char *topic, const char *payload) { + return this->publish(topic, payload, strlen(payload)); +} + bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) { - if (topic.empty()) + return this->publish_json(topic.c_str(), f); +} + +bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f) { + if (topic[0] == '\0') return false; return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 1a5e6db3afe..304a2c0d0e4 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -157,6 +157,38 @@ class MQTTComponent : public Component { */ bool publish(const std::string &topic, const char *payload, size_t payload_length); + /** Send a MQTT message (no heap allocation for topic). + * + * @param topic The topic as C string. + * @param payload The payload buffer. + * @param payload_length The length of the payload. + */ + bool publish(const char *topic, const char *payload, size_t payload_length); + + /** Send a MQTT message (no heap allocation for topic). + * + * @param topic The topic as StringRef (for use with get_state_topic_to_()). + * @param payload The payload buffer. + * @param payload_length The length of the payload. + */ + bool publish(StringRef topic, const char *payload, size_t payload_length) { + return this->publish(topic.c_str(), payload, payload_length); + } + + /** Send a MQTT message (no heap allocation for topic). + * + * @param topic The topic as C string. + * @param payload The null-terminated payload. + */ + bool publish(const char *topic, const char *payload); + + /** Send a MQTT message (no heap allocation for topic). + * + * @param topic The topic as StringRef (for use with get_state_topic_to_()). + * @param payload The null-terminated payload. + */ + bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); } + /** Construct and send a JSON MQTT message. * * @param topic The topic. @@ -164,6 +196,20 @@ class MQTTComponent : public Component { */ bool publish_json(const std::string &topic, const json::json_build_t &f); + /** Construct and send a JSON MQTT message (no heap allocation for topic). + * + * @param topic The topic as C string. + * @param f The Json Message builder. + */ + bool publish_json(const char *topic, const json::json_build_t &f); + + /** Construct and send a JSON MQTT message (no heap allocation for topic). + * + * @param topic The topic as StringRef (for use with get_state_topic_to_()). + * @param f The Json Message builder. + */ + bool publish_json(StringRef topic, const json::json_build_t &f) { return this->publish_json(topic.c_str(), f); } + /** Subscribe to a MQTT topic. * * @param topic The topic. Wildcards are currently not supported. diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 493514c8fb5..d5bd13869a6 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -115,7 +115,8 @@ bool MQTTCoverComponent::publish_state() { : this->cover_->position == COVER_OPEN ? "open" : traits.get_supports_position() ? "open" : "unknown"; - if (!this->publish(this->get_state_topic_(), state_s)) + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + if (!this->publish(this->get_state_topic_to_(topic_buf), state_s)) success = false; return success; } diff --git a/esphome/components/mqtt/mqtt_date.cpp b/esphome/components/mqtt/mqtt_date.cpp index cbe4045486c..c422bb30586 100644 --- a/esphome/components/mqtt/mqtt_date.cpp +++ b/esphome/components/mqtt/mqtt_date.cpp @@ -53,7 +53,8 @@ bool MQTTDateComponent::send_initial_state() { } } bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { - return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish_json(this->get_state_topic_to_(topic_buf), [year, month, day](JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[ESPHOME_F("year")] = year; root[ESPHOME_F("month")] = month; diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp index f7b4ef06853..1492abd011a 100644 --- a/esphome/components/mqtt/mqtt_datetime.cpp +++ b/esphome/components/mqtt/mqtt_datetime.cpp @@ -66,15 +66,17 @@ bool MQTTDateTimeComponent::send_initial_state() { } bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second) { - return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - root[ESPHOME_F("year")] = year; - root[ESPHOME_F("month")] = month; - root[ESPHOME_F("day")] = day; - root[ESPHOME_F("hour")] = hour; - root[ESPHOME_F("minute")] = minute; - root[ESPHOME_F("second")] = second; - }); + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish_json(this->get_state_topic_to_(topic_buf), + [year, month, day, hour, minute, second](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + root[ESPHOME_F("year")] = year; + root[ESPHOME_F("month")] = month; + root[ESPHOME_F("day")] = day; + root[ESPHOME_F("hour")] = hour; + root[ESPHOME_F("minute")] = minute; + root[ESPHOME_F("second")] = second; + }); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index 42fbc1eabd8..37d5c2551a9 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -44,7 +44,8 @@ void MQTTEventComponent::dump_config() { } bool MQTTEventComponent::publish_event_(const std::string &event_type) { - return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish_json(this->get_state_topic_to_(topic_buf), [event_type](JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_EVENT_TYPE] = event_type; }); diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 0909090023b..c9791fb0f1e 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -158,9 +158,10 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig } } bool MQTTFanComponent::publish_state() { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; const char *state_s = this->state_->state ? "ON" : "OFF"; ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s); - this->publish(this->get_state_topic_(), state_s); + this->publish(this->get_state_topic_to_(topic_buf), state_s); bool failed = false; if (this->state_->get_traits().supports_direction()) { bool success = this->publish(this->get_direction_state_topic(), diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index e43cb63f4fc..3e3537fa5cb 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -34,7 +34,8 @@ void MQTTJSONLightComponent::on_light_remote_values_update() { MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} bool MQTTJSONLightComponent::publish_state_() { - return this->publish_json(this->get_state_topic_(), [this](JsonObject root) { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson LightJSONSchema::dump_json(*this->state_, root); }); diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 43ef60bdf43..96c9397da8e 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -47,13 +47,14 @@ void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfi bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } bool MQTTLockComponent::publish_state() { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; #ifdef USE_STORE_LOG_STR_IN_FLASH char buf[LOCK_STATE_STR_SIZE]; strncpy_P(buf, (PGM_P) lock_state_to_string(this->lock_->state), sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; - return this->publish(this->get_state_topic_(), buf); + return this->publish(this->get_state_topic_to_(topic_buf), buf); #else - return this->publish(this->get_state_topic_(), LOG_STR_ARG(lock_state_to_string(this->lock_->state))); + return this->publish(this->get_state_topic_to_(topic_buf), LOG_STR_ARG(lock_state_to_string(this->lock_->state))); #endif } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index a014096c5ff..7dc93eee0c0 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -74,9 +74,10 @@ bool MQTTNumberComponent::send_initial_state() { } } bool MQTTNumberComponent::publish_state(float value) { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; char buffer[64]; - buf_append_printf(buffer, sizeof(buffer), 0, "%f", value); - return this->publish(this->get_state_topic_(), buffer); + size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%f", value); + return this->publish(this->get_state_topic_to_(topic_buf), buffer, len); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index 2d830998ec8..25fd813496c 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -50,7 +50,8 @@ bool MQTTSelectComponent::send_initial_state() { } } bool MQTTSelectComponent::publish_state(const std::string &value) { - return this->publish(this->get_state_topic_(), value); + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size()); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index f136b823558..e83eab6732f 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -79,12 +79,13 @@ bool MQTTSensorComponent::send_initial_state() { } } bool MQTTSensorComponent::publish_state(float value) { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (mqtt::global_mqtt_client->is_publish_nan_as_none() && std::isnan(value)) - return this->publish(this->get_state_topic_(), "None", 4); + return this->publish(this->get_state_topic_to_(topic_buf), "None", 4); int8_t accuracy = this->sensor_->get_accuracy_decimals(); char buf[VALUE_ACCURACY_MAX_LEN]; size_t len = value_accuracy_to_buf(buf, value, accuracy); - return this->publish(this->get_state_topic_(), buf, len); + return this->publish(this->get_state_topic_to_(topic_buf), buf, len); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index a985ec66be3..70cd03a4eb5 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -52,8 +52,9 @@ void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } bool MQTTSwitchComponent::publish_state(bool state) { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; const char *state_s = state ? "ON" : "OFF"; - return this->publish(this->get_state_topic_(), state_s); + return this->publish(this->get_state_topic_to_(topic_buf), state_s); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp index fed9224b424..16293c06034 100644 --- a/esphome/components/mqtt/mqtt_text.cpp +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -53,7 +53,8 @@ bool MQTTTextComponent::send_initial_state() { } } bool MQTTTextComponent::publish_state(const std::string &value) { - return this->publish(this->get_state_topic_(), value); + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size()); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 5346923b41e..a6b9f90b683 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -31,7 +31,10 @@ void MQTTTextSensor::dump_config() { LOG_MQTT_COMPONENT(true, false); } -bool MQTTTextSensor::publish_state(const std::string &value) { return this->publish(this->get_state_topic_(), value); } +bool MQTTTextSensor::publish_state(const std::string &value) { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size()); +} bool MQTTTextSensor::send_initial_state() { if (this->sensor_->has_state()) { return this->publish_state(this->sensor_->state); diff --git a/esphome/components/mqtt/mqtt_time.cpp b/esphome/components/mqtt/mqtt_time.cpp index 8749c3b59ee..be391ce88c5 100644 --- a/esphome/components/mqtt/mqtt_time.cpp +++ b/esphome/components/mqtt/mqtt_time.cpp @@ -53,7 +53,8 @@ bool MQTTTimeComponent::send_initial_state() { } } bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { - return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish_json(this->get_state_topic_to_(topic_buf), [hour, minute, second](JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[ESPHOME_F("hour")] = hour; root[ESPHOME_F("minute")] = minute; diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index 99e0c85509c..c01fb9e52e0 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -28,7 +28,8 @@ void MQTTUpdateComponent::setup() { } bool MQTTUpdateComponent::publish_state() { - return this->publish_json(this->get_state_topic_(), [this](JsonObject root) { + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) { root[ESPHOME_F("installed_version")] = this->update_->update_info.current_version; root[ESPHOME_F("latest_version")] = this->update_->update_info.latest_version; root[ESPHOME_F("title")] = this->update_->update_info.title; diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 8e66a69c6f5..2e100823bfd 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -84,7 +84,8 @@ bool MQTTValveComponent::publish_state() { : this->valve_->position == VALVE_OPEN ? "open" : traits.get_supports_position() ? "open" : "unknown"; - if (!this->publish(this->get_state_topic_(), state_s)) + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + if (!this->publish(this->get_state_topic_to_(topic_buf), state_s)) success = false; return success; } diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index fc48ad67e3c..7aecab99d1b 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -41,12 +41,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component { void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); } bool canHandle(AsyncWebServerRequest *request) const override { - if (request->method() == HTTP_GET) { - if (request->url() == "/metrics") - return true; - } - - return false; + if (request->method() != HTTP_GET) + return false; +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + return request->url_to(url_buf) == "/metrics"; +#else + return request->url() == ESPHOME_F("/metrics"); +#endif } void handleRequest(AsyncWebServerRequest *req) override; diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 8b054877043..685dfc1c5e9 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -80,23 +80,21 @@ class Select : public EntityBase { void add_on_state_callback(std::function &&callback); - protected: - friend class SelectCall; - - size_t active_index_{0}; - /** Set the value of the select by index, this is an optional virtual method. - * - * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes. - * Overriding this index-based version is PREFERRED as it avoids string conversions. * * This method is called by the SelectCall when the index is already known. - * Default implementation converts to string and calls control(const std::string&). + * Default implementation converts to string and calls control(). + * Override this to work directly with indices and avoid string conversions. * * @param index The index as validated by the SelectCall. */ virtual void control(size_t index) { this->control(this->option_at(index)); } + protected: + friend class SelectCall; + + size_t active_index_{0}; + /** Set the value of the select, this is a virtual method that each select integration can implement. * * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes. diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 3c495f3160c..b5dbf032114 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -25,7 +25,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_SPEED): cv.invalid( "Configuring individual speeds is deprecated." ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255), cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ) diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index e9a389e0f3f..16e6c277ced 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -10,7 +10,7 @@ namespace speed { class SpeedFan : public Component, public fan::Fan { public: - SpeedFan(int speed_count) : speed_count_(speed_count) {} + SpeedFan(uint8_t speed_count) : speed_count_(speed_count) {} void setup() override; void dump_config() override; void set_output(output::FloatOutput *output) { this->output_ = output; } @@ -26,7 +26,7 @@ class SpeedFan : public Component, public fan::Fan { output::FloatOutput *output_; output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; - int speed_count_{}; + uint8_t speed_count_{}; fan::FanTraits traits_; std::vector preset_modes_{}; }; diff --git a/esphome/components/template/fan/__init__.py b/esphome/components/template/fan/__init__.py index 72b20e1efea..1ab092ca0e0 100644 --- a/esphome/components/template/fan/__init__.py +++ b/esphome/components/template/fan/__init__.py @@ -19,7 +19,7 @@ CONFIG_SCHEMA = ( { cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, - cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), + cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1, max=255), cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ) diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index b7e1d4ab5a6..c60f57c0e57 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -12,7 +12,7 @@ class TemplateFan final : public Component, public fan::Fan { void dump_config() override; void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } - void set_speed_count(int count) { this->speed_count_ = count; } + void set_speed_count(uint8_t count) { this->speed_count_ = count; } void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } @@ -21,7 +21,7 @@ class TemplateFan final : public Component, public fan::Fan { bool has_oscillating_{false}; bool has_direction_{false}; - int speed_count_{0}; + uint8_t speed_count_{0}; fan::FanTraits traits_; std::vector preset_modes_{}; }; diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index de95888b6b8..dd8626115f7 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), + cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=255), } ) .extend(cv.COMPONENT_SCHEMA), diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index 527efa8246d..93b4fc39ae2 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -9,7 +9,7 @@ namespace tuya { class TuyaFan : public Component, public fan::Fan { public: - TuyaFan(Tuya *parent, int speed_count) : parent_(parent), speed_count_(speed_count) {} + TuyaFan(Tuya *parent, uint8_t speed_count) : parent_(parent), speed_count_(speed_count) {} void setup() override; void dump_config() override; void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; } @@ -27,7 +27,7 @@ class TuyaFan : public Component, public fan::Fan { optional switch_id_{}; optional oscillation_id_{}; optional direction_id_{}; - int speed_count_{}; + uint8_t speed_count_{}; TuyaDatapointType speed_type_{}; TuyaDatapointType oscillation_type_{}; }; diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index fd7b5fb03f3..a6df61c053e 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -2,7 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include +#include namespace esphome { namespace tx20 { @@ -45,25 +45,25 @@ std::string Tx20Component::get_wind_cardinal_direction() const { return this->wi void Tx20Component::decode_and_publish_() { ESP_LOGVV(TAG, "Decode Tx20"); - std::string string_buffer; - std::string string_buffer_2; - std::vector bit_buffer; + std::array bit_buffer{}; + size_t bit_pos = 0; bool current_bit = true; + // Cap at MAX_BUFFER_SIZE - 1 to prevent out-of-bounds access (buffer_index can exceed MAX_BUFFER_SIZE in ISR) + const int max_buffer_index = + std::min(static_cast(this->store_.buffer_index), static_cast(MAX_BUFFER_SIZE - 1)); - for (int i = 1; i <= this->store_.buffer_index; i++) { - string_buffer_2 += to_string(this->store_.buffer[i]) + ", "; + for (int i = 1; i <= max_buffer_index; i++) { uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME; // ignore segments at the end that were too short - string_buffer.append(repeat, current_bit ? '1' : '0'); - bit_buffer.insert(bit_buffer.end(), repeat, current_bit); + for (uint8_t j = 0; j < repeat && bit_pos < MAX_BUFFER_SIZE; j++) { + bit_buffer[bit_pos++] = current_bit; + } current_bit = !current_bit; } current_bit = !current_bit; - if (string_buffer.length() < MAX_BUFFER_SIZE) { - uint8_t remain = MAX_BUFFER_SIZE - string_buffer.length(); - string_buffer_2 += to_string(remain) + ", "; - string_buffer.append(remain, current_bit ? '1' : '0'); - bit_buffer.insert(bit_buffer.end(), remain, current_bit); + size_t bits_before_padding = bit_pos; + while (bit_pos < MAX_BUFFER_SIZE) { + bit_buffer[bit_pos++] = current_bit; } uint8_t tx20_sa = 0; @@ -108,8 +108,24 @@ void Tx20Component::decode_and_publish_() { // 2. Check received checksum matches calculated checksum // 3. Check that Wind Direction matches Wind Direction (Inverted) // 4. Check that Wind Speed matches Wind Speed (Inverted) - ESP_LOGVV(TAG, "BUFFER %s", string_buffer_2.c_str()); - ESP_LOGVV(TAG, "Decoded bits %s", string_buffer.c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + // Build debug strings from completed data + char debug_buf[320]; // buffer values: max 40 entries * 7 chars each + size_t debug_pos = 0; + for (int i = 1; i <= max_buffer_index; i++) { + debug_pos = buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%u, ", this->store_.buffer[i]); + } + if (bits_before_padding < MAX_BUFFER_SIZE) { + buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%zu, ", MAX_BUFFER_SIZE - bits_before_padding); + } + char bits_buf[MAX_BUFFER_SIZE + 1]; + for (size_t i = 0; i < MAX_BUFFER_SIZE; i++) { + bits_buf[i] = bit_buffer[i] ? '1' : '0'; + } + bits_buf[MAX_BUFFER_SIZE] = '\0'; + ESP_LOGVV(TAG, "BUFFER %s", debug_buf); + ESP_LOGVV(TAG, "Decoded bits %s", bits_buf); +#endif if (tx20_sa == 4) { if (chk == tx20_sd) { diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 3793f01eb5d..4be162ccd32 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -32,8 +32,15 @@ class OTARequestHandler : public AsyncWebHandler { void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, size_t len, bool final) override; bool canHandle(AsyncWebServerRequest *request) const override { - // Check if this is an OTA update request - bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST; + if (request->method() != HTTP_POST) + return false; + // Check if this is an OTA update request +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + bool is_ota_request = request->url_to(url_buf) == "/update"; +#else + bool is_ota_request = request->url() == ESPHOME_F("/update"); +#endif #if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL) // IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e538a35e8cd..b43383cafac 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2175,7 +2175,12 @@ std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_ #endif bool WebServer::canHandle(AsyncWebServerRequest *request) const { +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + StringRef url = request->url_to(url_buf); +#else const auto &url = request->url(); +#endif const auto method = request->method(); // Static URL checks - use ESPHOME_F to keep strings in flash on ESP8266 @@ -2311,30 +2316,35 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + StringRef url = request->url_to(url_buf); +#else const auto &url = request->url(); +#endif // Handle static routes first - if (url == "/") { + if (url == ESPHOME_F("/")) { this->handle_index_request(request); return; } #if !defined(USE_ESP32) && defined(USE_ARDUINO) - if (url == "/events") { + if (url == ESPHOME_F("/events")) { this->events_.add_new_client(this, request); return; } #endif #ifdef USE_WEBSERVER_CSS_INCLUDE - if (url == "/0.css") { + if (url == ESPHOME_F("/0.css")) { this->handle_css_request(request); return; } #endif #ifdef USE_WEBSERVER_JS_INCLUDE - if (url == "/0.js") { + if (url == ESPHOME_F("/0.js")) { this->handle_js_request(request); return; } diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index d5d75b395d3..4be653c362a 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -48,4 +48,5 @@ async def to_code(config): if CORE.is_libretiny: CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"]) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json - cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10") + # Testing PR #370 for ESP8266 SSE crash fix + cg.add_library("https://github.com/bdraco/ESPAsyncWebServer.git#pr-370", None) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index abeda5fc463..c32a4bc9599 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -246,21 +246,16 @@ optional AsyncWebServerRequest::get_header(const char *name) const return request_get_header(*this, name); } -std::string AsyncWebServerRequest::url() const { - auto *query_start = strchr(this->req_->uri, '?'); - std::string result; - if (query_start == nullptr) { - result = this->req_->uri; - } else { - result = std::string(this->req_->uri, query_start - this->req_->uri); - } +StringRef AsyncWebServerRequest::url_to(std::span buffer) const { + const char *uri = this->req_->uri; + const char *query_start = strchr(uri, '?'); + size_t uri_len = query_start ? static_cast(query_start - uri) : strlen(uri); + size_t copy_len = std::min(uri_len, URL_BUF_SIZE - 1); + memcpy(buffer.data(), uri, copy_len); + buffer[copy_len] = '\0'; // Decode URL-encoded characters in-place (e.g., %20 -> space) - // This matches AsyncWebServer behavior on Arduino - if (!result.empty()) { - size_t new_len = url_decode(&result[0]); - result.resize(new_len); - } - return result; + size_t decoded_len = url_decode(buffer.data()); + return StringRef(buffer.data(), decoded_len); } std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } @@ -406,8 +401,9 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) { void AsyncResponseStream::print(float value) { // Use stack buffer to avoid temporary string allocation // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety - char buf[32]; - int len = snprintf(buf, sizeof(buf), "%f", value); + constexpr size_t float_buf_size = 32; + char buf[float_buf_size]; + int len = snprintf(buf, float_buf_size, "%f", value); this->content_.append(buf, len); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index a6c984792a2..e38913ef4a1 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -3,12 +3,14 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include #include #include #include #include +#include #include #include #include @@ -110,7 +112,15 @@ class AsyncWebServerRequest { ~AsyncWebServerRequest(); http_method method() const { return static_cast(this->req_->method); } - std::string url() const; + static constexpr size_t URL_BUF_SIZE = CONFIG_HTTPD_MAX_URI_LEN + 1; ///< Buffer size for url_to() + /// Write URL (without query string) to buffer, returns StringRef pointing to buffer. + /// URL is decoded (e.g., %20 -> space). + StringRef url_to(std::span buffer) const; + /// Get URL as std::string. Prefer url_to() to avoid heap allocation. + std::string url() const { + char buffer[URL_BUF_SIZE]; + return std::string(this->url_to(buffer)); + } std::string host() const; // NOLINTNEXTLINE(readability-identifier-naming) size_t contentLength() const { return this->req_->content_len; } @@ -306,7 +316,10 @@ class AsyncEventSource : public AsyncWebHandler { // NOLINTNEXTLINE(readability-identifier-naming) bool canHandle(AsyncWebServerRequest *request) const override { - return request->method() == HTTP_GET && request->url() == this->url_; + if (request->method() != HTTP_GET) + return false; + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + return request->url_to(url_buf) == this->url_; } // NOLINTNEXTLINE(readability-identifier-naming) void handleRequest(AsyncWebServerRequest *request) override; diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c8ea0387392..d5f3b076268 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1811,6 +1811,7 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); this->set_sta_priority(failed_bssid.value(), new_priority); } + char bssid_s[18]; format_mac_addr_upper(failed_bssid.value().data(), bssid_s); ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "", diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 9a7dd496099..ea690f2edc7 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -762,6 +762,15 @@ class EsphomeCore: def relative_piolibdeps_path(self, *path: str | Path) -> Path: return self.relative_build_path(".piolibdeps", *path) + @property + def platformio_cache_dir(self) -> str: + """Get the PlatformIO cache directory path.""" + # Check if running in Docker/HA addon with custom cache dir + if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip(): + return cache_dir + # Default PlatformIO cache location + return os.path.expanduser("~/.platformio/.cache") + @property def firmware_bin(self) -> Path: # Check if using native ESP-IDF build (--native-idf) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 67cda05d33a..ae7e3aee7af 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -348,7 +348,10 @@ std::string format_hex(const uint8_t *data, size_t length) { format_hex_to(&ret[0], length * 2 + 1, data, length); return ret; } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } +#pragma GCC diagnostic pop char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { return format_hex_internal(buffer, buffer_size, data, length, separator, 'A'); @@ -517,10 +520,8 @@ int8_t step_to_accuracy_decimals(float step) { return str.length() - dot_pos - 1; } -// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes) -static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/"; +// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms +static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // Helper function to find the index of a base64/base64url character in the lookup table. // Returns the character's position (0-63) if found, or 0 if not found. diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 1d836f34990..0c48c8a48d3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -134,6 +134,78 @@ template class ConstVector { size_t size_; }; +/// Small buffer optimization - stores data inline when small, heap-allocates for large data +/// This avoids heap fragmentation for common small allocations while supporting arbitrary sizes. +/// Memory management is encapsulated - callers just use set() and data(). +template class SmallInlineBuffer { + public: + SmallInlineBuffer() = default; + ~SmallInlineBuffer() { + if (!this->is_inline_()) + delete[] this->heap_; + } + + // Move constructor + SmallInlineBuffer(SmallInlineBuffer &&other) noexcept : len_(other.len_) { + if (other.is_inline_()) { + memcpy(this->inline_, other.inline_, this->len_); + } else { + this->heap_ = other.heap_; + other.heap_ = nullptr; + } + other.len_ = 0; + } + + // Move assignment + SmallInlineBuffer &operator=(SmallInlineBuffer &&other) noexcept { + if (this != &other) { + if (!this->is_inline_()) + delete[] this->heap_; + this->len_ = other.len_; + if (other.is_inline_()) { + memcpy(this->inline_, other.inline_, this->len_); + } else { + this->heap_ = other.heap_; + other.heap_ = nullptr; + } + other.len_ = 0; + } + return *this; + } + + // Disable copy (would need deep copy of heap data) + SmallInlineBuffer(const SmallInlineBuffer &) = delete; + SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete; + + /// Set buffer contents, allocating heap if needed + void set(const uint8_t *src, size_t size) { + // Free existing heap allocation if switching from heap to inline or different heap size + if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) { + delete[] this->heap_; + this->heap_ = nullptr; // Defensive: prevent use-after-free if logic changes + } + // Allocate new heap buffer if needed + if (size > InlineSize && (this->is_inline_() || size != this->len_)) { + this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory) + } + this->len_ = size; + memcpy(this->data(), src, size); + } + + uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; } + const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; } + size_t size() const { return this->len_; } + + protected: + bool is_inline_() const { return this->len_ <= InlineSize; } + + size_t len_{0}; + union { + uint8_t inline_[InlineSize]{}; // Zero-init ensures clean initial state + uint8_t *heap_; + }; +}; + /// Minimal static vector - saves memory by avoiding std::vector overhead template class StaticVector { public: @@ -168,6 +240,9 @@ template class StaticVector { size_t size() const { return count_; } bool empty() const { return count_ == 0; } + // Direct access to size counter for efficient in-place construction + size_t &count() { return count_; } + // Direct access to underlying data T *data() { return data_.data(); } const T *data() const { return data_.data(); } @@ -655,9 +730,11 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) { } /// snprintf-like function returning std::string of maximum length \p len (excluding null terminator). +/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...); /// sprintf-like function returning std::string. +/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); #ifdef USE_ESP8266 @@ -1030,13 +1107,17 @@ std::string format_hex(const std::vector &data); /// Causes heap fragmentation on long-running devices. template::value, int> = 0> std::string format_hex(T val) { val = convert_big_endian(val); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return format_hex(reinterpret_cast(&val), sizeof(T)); +#pragma GCC diagnostic pop } /// Format the std::array \p data in lowercased hex. /// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. /// Causes heap fragmentation on long-running devices. template std::string format_hex(const std::array &data) { return format_hex(data.data(), data.size()); +#pragma GCC diagnostic pop } /** Format a byte array in pretty-printed, human-readable hex format. diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 7de1023e6df..64b9eb6a8e2 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -118,10 +118,9 @@ class Scheduler { } name_; uint32_t interval; // Split time to handle millis() rollover. The scheduler combines the 32-bit millis() - // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits). - // This is intentionally limited to 48 bits, not stored as a full 64-bit value. - // With 49.7 days per 32-bit rollover, the 16-bit counter supports - // 49.7 days × 65536 = ~8900 years. This ensures correct scheduling + // with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit + // for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter + // supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling // even when devices run for months. Split into two fields for better memory // alignment on 32-bit systems. uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index e66f9a2c978..bc241d39e2f 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -171,7 +171,16 @@ def run_compile(config, verbose): args = [] if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]: args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"] - return run_platformio_cli_run(config, verbose, *args) + result = run_platformio_cli_run(config, verbose, *args) + + # Run memory analysis if enabled + if config.get(CONF_ESPHOME, {}).get("analyze_memory", False): + try: + analyze_memory_usage(config) + except Exception as e: + _LOGGER.warning("Failed to analyze memory usage: %s", e) + + return result def _run_idedata(config): @@ -425,3 +434,74 @@ class IDEData: def defines(self) -> list[str]: """Return the list of preprocessor defines from idedata.""" return self.raw.get("defines", []) + + +def analyze_memory_usage(config: dict[str, Any]) -> None: + """Analyze memory usage by component after compilation.""" + # Lazy import to avoid overhead when not needed + from esphome.analyze_memory.cli import MemoryAnalyzerCLI + from esphome.analyze_memory.helpers import get_esphome_components + + idedata = get_idedata(config) + + # Get paths to tools + elf_path = idedata.firmware_elf_path + objdump_path = idedata.objdump_path + readelf_path = idedata.readelf_path + + # Debug logging + _LOGGER.debug("ELF path from idedata: %s", elf_path) + + # Check if file exists + if not Path(elf_path).exists(): + # Try alternate path + alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf")) + if alt_path.exists(): + elf_path = str(alt_path) + _LOGGER.debug("Using alternate ELF path: %s", elf_path) + else: + _LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path) + return + + # Extract external components from config + external_components = set() + + # Get the list of built-in ESPHome components + builtin_components = get_esphome_components() + + # Special non-component keys that appear in configs + NON_COMPONENT_KEYS = { + CONF_ESPHOME, + "substitutions", + "packages", + "globals", + "<<", + } + + # Check all top-level keys in config + for key in config: + if key not in builtin_components and key not in NON_COMPONENT_KEYS: + # This is an external component + external_components.add(key) + + _LOGGER.debug("Detected external components: %s", external_components) + + # Create analyzer and run analysis + analyzer = MemoryAnalyzerCLI( + elf_path, objdump_path, readelf_path, external_components + ) + analyzer.analyze() + + # Generate and print report + report = analyzer.generate_report() + _LOGGER.info("\n%s", report) + + # Optionally save to file + if config.get(CONF_ESPHOME, {}).get("memory_report_file"): + report_file = Path(config[CONF_ESPHOME]["memory_report_file"]) + if report_file.suffix == ".json": + report_file.write_text(analyzer.to_json()) + _LOGGER.info("Memory report saved to %s", report_file) + else: + report_file.write_text(report) + _LOGGER.info("Memory report saved to %s", report_file) diff --git a/script/ci-custom.py b/script/ci-custom.py index 01e197057a1..b146c9867ee 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -692,6 +692,8 @@ HEAP_ALLOCATING_HELPERS = { "str_truncate": "removal (function is unused)", "str_upper_case": "removal (function is unused)", "str_snake_case": "removal (function is unused)", + "str_sprintf": "snprintf() with a stack buffer", + "str_snprintf": "snprintf() with a stack buffer", } @@ -710,7 +712,9 @@ HEAP_ALLOCATING_HELPERS = { r"str_sanitize(?!_)|" r"str_truncate|" r"str_upper_case|" - r"str_snake_case" + r"str_snake_case|" + r"str_sprintf|" + r"str_snprintf" r")\s*\(" + CPP_RE_EOL, include=cpp_include, exclude=[ diff --git a/tests/components/globals/common.yaml b/tests/components/globals/common.yaml index 224a91a2709..efa3cba0766 100644 --- a/tests/components/globals/common.yaml +++ b/tests/components/globals/common.yaml @@ -10,6 +10,7 @@ globals: type: int restore_value: true initial_value: "0" + update_interval: 5s - id: glob_float type: float restore_value: true diff --git a/tests/integration/fixtures/api_user_services_union.yaml b/tests/integration/fixtures/api_user_services_union.yaml new file mode 100644 index 00000000000..4fd93437722 --- /dev/null +++ b/tests/integration/fixtures/api_user_services_union.yaml @@ -0,0 +1,59 @@ +esphome: + name: test-user-services-union + friendly_name: Test User Services Union Storage + +esp32: + board: esp32dev + framework: + type: esp-idf + +logger: + level: DEBUG + +wifi: + ssid: "test" + password: "password" + +api: + actions: + # Test service with no arguments + - action: test_no_args + then: + - logger.log: "No args service called" + + # Test service with one argument + - action: test_one_arg + variables: + value: int + then: + - logger.log: + format: "One arg service: %d" + args: [value] + + # Test service with multiple arguments of different types + - action: test_multi_args + variables: + int_val: int + float_val: float + str_val: string + bool_val: bool + then: + - logger.log: + format: "Multi args: %d, %.2f, %s, %d" + args: [int_val, float_val, str_val.c_str(), bool_val] + + # Test service with max typical arguments + - action: test_many_args + variables: + arg1: int + arg2: int + arg3: int + arg4: string + arg5: float + then: + - logger.log: "Many args service called" + +binary_sensor: + - platform: template + name: "Test Binary Sensor" + id: test_sensor diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index ac05e0d31bb..b0aaaad63de 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -453,6 +453,7 @@ def test_clean_build( mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir mock_core.relative_build_path.return_value = dependencies_lock + mock_core.platformio_cache_dir = str(platformio_cache_dir) # Verify all exist before assert pioenvs_dir.exists()