From 69867bf8187a1bfc491ec657ed5a42b195c37283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Sat, 3 Jan 2026 19:58:56 +0100 Subject: [PATCH 01/51] [nrf52, zephyr] move nrf52-specific code to nrf52 component (#12582) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/logger/__init__.py | 2 +- esphome/components/nrf52/__init__.py | 16 +++++++++++- esphome/components/nrf52/gpio.py | 9 ++++++- esphome/components/zephyr/__init__.py | 30 ++++++++--------------- esphome/components/zephyr/core.cpp | 9 ++++++- esphome/components/zephyr/gpio.cpp | 29 +++++----------------- esphome/components/zephyr/gpio.h | 13 +++++++--- esphome/components/zephyr/preferences.cpp | 2 ++ 8 files changed, 60 insertions(+), 50 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 8968a5eab81..7132cd89566 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -386,7 +386,7 @@ async def to_code(config): except cv.Invalid: pass - if CORE.using_zephyr: + if CORE.is_nrf52: if config[CONF_HARDWARE_UART] == UART0: zephyr_add_overlay("""&uart0 { status = "okay";};""") if config[CONF_HARDWARE_UART] == UART1: diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 03927e8ea27..bf90a41df50 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -12,6 +12,7 @@ from esphome.components.zephyr import ( zephyr_add_prj_conf, zephyr_data, zephyr_set_core_data, + zephyr_setup_preferences, zephyr_to_code, ) from esphome.components.zephyr.const import ( @@ -49,7 +50,7 @@ from .const import ( from .gpio import nrf52_pin_to_code # noqa CODEOWNERS = ["@tomaszduda23"] -AUTO_LOAD = ["zephyr"] +AUTO_LOAD = ["zephyr", "preferences"] IS_TARGET_PLATFORM = True _LOGGER = logging.getLogger(__name__) @@ -194,6 +195,7 @@ async def to_code(config: ConfigType) -> None: cg.add_platformio_option("board_upload.require_upload_port", "true") cg.add_platformio_option("board_upload.wait_for_upload_port", "true") + zephyr_setup_preferences() zephyr_to_code(config) if dfu_config := config.get(CONF_DFU): @@ -206,6 +208,18 @@ async def to_code(config: ConfigType) -> None: if reg0_config[CONF_UICR_ERASE]: cg.add_define("USE_NRF52_UICR_ERASE") + # c++ support + zephyr_add_prj_conf("CPLUSPLUS", True) + zephyr_add_prj_conf("LIB_CPLUSPLUS", True) + # watchdog + zephyr_add_prj_conf("WATCHDOG", True) + zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) + # disable console + zephyr_add_prj_conf("UART_CONSOLE", False) + zephyr_add_prj_conf("CONSOLE", False) + # use NFC pins as GPIO + zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) + @coroutine_with_priority(CoroPriority.DIAGNOSTICS) async def _dfu_to_code(dfu_config): diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py index 17329042b26..498e8cc3307 100644 --- a/esphome/components/nrf52/gpio.py +++ b/esphome/components/nrf52/gpio.py @@ -71,8 +71,15 @@ NRF52_PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_NRF52, NRF52_PIN_SCHEMA) async def nrf52_pin_to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] + port = num // 32 + pin_name_prefix = f"P{port}." + var = cg.new_Pvariable( + config[CONF_ID], + cg.RawExpression(f"DEVICE_DT_GET_OR_NULL(DT_NODELABEL(gpio{port}))"), + 32, + pin_name_prefix, + ) cg.add(var.set_pin(num)) # Only set if true to avoid bloating setup() function # (inverted bit in pin_flags_ bitfield is zero-initialized to false) diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 0381fbcba9d..a91d976e6b3 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -21,7 +21,6 @@ from .const import ( ) CODEOWNERS = ["@tomaszduda23"] -AUTO_LOAD = ["preferences"] PrjConfValueType = bool | str | int @@ -111,32 +110,15 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None: def zephyr_to_code(config): - cg.add(zephyr_ns.setup_preferences()) cg.add_build_flag("-DUSE_ZEPHYR") cg.set_cpp_standard("gnu++20") # build is done by west so bypass board checking in platformio cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) - # c++ support zephyr_add_prj_conf("NEWLIB_LIBC", True) - zephyr_add_prj_conf("CONFIG_FPU", True) + zephyr_add_prj_conf("FPU", True) zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) - zephyr_add_prj_conf("CPLUSPLUS", True) - zephyr_add_prj_conf("CONFIG_STD_CPP20", True) - zephyr_add_prj_conf("LIB_CPLUSPLUS", True) - # preferences - zephyr_add_prj_conf("SETTINGS", True) - zephyr_add_prj_conf("NVS", True) - zephyr_add_prj_conf("FLASH_MAP", True) - zephyr_add_prj_conf("CONFIG_FLASH", True) - # watchdog - zephyr_add_prj_conf("WATCHDOG", True) - zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) - # disable console - zephyr_add_prj_conf("UART_CONSOLE", False) - zephyr_add_prj_conf("CONSOLE", False, False) - # use NFC pins as GPIO - zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) + zephyr_add_prj_conf("STD_CPP20", True) # os: ***** USAGE FAULT ***** # os: Illegal load of EXC_RETURN into PC @@ -149,6 +131,14 @@ def zephyr_to_code(config): ) +def zephyr_setup_preferences(): + cg.add(zephyr_ns.setup_preferences()) + zephyr_add_prj_conf("SETTINGS", True) + zephyr_add_prj_conf("NVS", True) + zephyr_add_prj_conf("FLASH_MAP", True) + zephyr_add_prj_conf("FLASH", True) + + def _format_prj_conf_val(value: PrjConfValueType) -> str: if isinstance(value, bool): return "y" if value else "n" diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d5427a0ebfc..46589cdb628 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -10,8 +10,10 @@ namespace esphome { +#ifdef CONFIG_WATCHDOG static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); +#endif void yield() { ::k_yield(); } uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } @@ -20,6 +22,7 @@ void delayMicroseconds(uint32_t us) { ::k_usleep(us); } void delay(uint32_t ms) { ::k_msleep(ms); } void arch_init() { +#ifdef CONFIG_WATCHDOG if (device_is_ready(WDT)) { static wdt_timeout_cfg wdt_config{}; wdt_config.flags = WDT_FLAG_RESET_SOC; @@ -36,12 +39,15 @@ void arch_init() { wdt_setup(WDT, options); } } +#endif } void arch_feed_wdt() { +#ifdef CONFIG_WATCHDOG if (wdt_channel_id >= 0) { wdt_feed(WDT, wdt_channel_id); } +#endif } void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } @@ -72,6 +78,7 @@ bool random_bytes(uint8_t *data, size_t len) { return true; } +#ifdef USE_NRF52 void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) mac[0] = ((NRF_FICR->DEVICEADDR[1] & 0xFFFF) >> 8) | 0xC0; mac[1] = NRF_FICR->DEVICEADDR[1] & 0xFFFF; @@ -80,7 +87,7 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame mac[4] = NRF_FICR->DEVICEADDR[0] >> 8; mac[5] = NRF_FICR->DEVICEADDR[0]; } - +#endif } // namespace esphome void setup(); diff --git a/esphome/components/zephyr/gpio.cpp b/esphome/components/zephyr/gpio.cpp index 8041c361cc1..1d5b0f282b3 100644 --- a/esphome/components/zephyr/gpio.cpp +++ b/esphome/components/zephyr/gpio.cpp @@ -50,25 +50,7 @@ void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::Inte } void ZephyrGPIOPin::setup() { - const struct device *gpio = nullptr; - if (this->pin_ < 32) { -#define GPIO0 DT_NODELABEL(gpio0) -#if DT_NODE_HAS_STATUS(GPIO0, okay) - gpio = DEVICE_DT_GET(GPIO0); -#else -#error "gpio0 is disabled" -#endif - } else { -#define GPIO1 DT_NODELABEL(gpio1) -#if DT_NODE_HAS_STATUS(GPIO1, okay) - gpio = DEVICE_DT_GET(GPIO1); -#else -#error "gpio1 is disabled" -#endif - } - if (device_is_ready(gpio)) { - this->gpio_ = gpio; - } else { + if (!device_is_ready(this->gpio_)) { ESP_LOGE(TAG, "gpio %u is not ready.", this->pin_); return; } @@ -79,21 +61,22 @@ void ZephyrGPIOPin::pin_mode(gpio::Flags flags) { if (nullptr == this->gpio_) { return; } - auto ret = gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); + auto ret = gpio_pin_configure(this->gpio_, this->pin_ % this->gpio_size_, + flags_to_mode(flags, this->inverted_, this->value_)); if (ret != 0) { ESP_LOGE(TAG, "gpio %u cannot be configured %d.", this->pin_, ret); } } size_t ZephyrGPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32); + return snprintf(buffer, len, "GPIO%u, %s%u", this->pin_, this->pin_name_prefix_, this->pin_ % this->gpio_size_); } bool ZephyrGPIOPin::digital_read() { if (nullptr == this->gpio_) { return false; } - return bool(gpio_pin_get(this->gpio_, this->pin_ % 32) != this->inverted_); + return bool(gpio_pin_get(this->gpio_, this->pin_ % this->gpio_size_) != this->inverted_); } void ZephyrGPIOPin::digital_write(bool value) { @@ -103,7 +86,7 @@ void ZephyrGPIOPin::digital_write(bool value) { if (nullptr == this->gpio_) { return; } - gpio_pin_set(this->gpio_, this->pin_ % 32, value != this->inverted_ ? 1 : 0); + gpio_pin_set(this->gpio_, this->pin_ % this->gpio_size_, value != this->inverted_ ? 1 : 0); } void ZephyrGPIOPin::detach_interrupt() const { // TODO diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index b405f385bcd..c9540f4f01a 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -8,6 +8,11 @@ namespace zephyr { class ZephyrGPIOPin : public InternalGPIOPin { public: + ZephyrGPIOPin(const device *gpio, int gpio_size, const char *pin_name_prefix) { + this->gpio_ = gpio; + this->gpio_size_ = gpio_size; + this->pin_name_prefix_ = pin_name_prefix; + } void set_pin(uint8_t pin) { this->pin_ = pin; } void set_inverted(bool inverted) { this->inverted_ = inverted; } void set_flags(gpio::Flags flags) { this->flags_ = flags; } @@ -25,10 +30,12 @@ class ZephyrGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_; - bool inverted_{}; - gpio::Flags flags_{}; const device *gpio_{nullptr}; + const char *pin_name_prefix_{nullptr}; + gpio::Flags flags_{}; + uint8_t pin_; + uint8_t gpio_size_{}; + bool inverted_{}; bool value_{false}; }; diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index d7023660442..08b361b8fb4 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -1,4 +1,5 @@ #ifdef USE_ZEPHYR +#ifdef CONFIG_SETTINGS #include #include "esphome/core/preferences.h" @@ -154,3 +155,4 @@ ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const } // namespace esphome #endif +#endif From c34665f6500bb6449236ef583790249d88647d1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 09:13:07 -1000 Subject: [PATCH 02/51] [api] Fix KeyError when running logs after password removal (#12831) --- esphome/components/api/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index ca1fc089fa6..200d0938bd5 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -16,7 +16,7 @@ with warnings.catch_warnings(): import contextlib -from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ +from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.core import CORE from . import CONF_ENCRYPTION @@ -35,7 +35,6 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: conf = config["api"] name = config["esphome"]["name"] port: int = int(conf[CONF_PORT]) - password: str = conf[CONF_PASSWORD] noise_psk: str | None = None if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): noise_psk = key @@ -50,7 +49,7 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: cli = APIClient( addresses[0], # Primary address for compatibility port, - password, + "", # Password auth removed in 2026.1.0 client_info=f"ESPHome Logs {__version__}", noise_psk=noise_psk, addresses=addresses, # Pass all addresses for automatic retry From 5cfcf8d104fae1b3c77e34c4278cd9510252e640 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Sat, 3 Jan 2026 21:51:48 +0100 Subject: [PATCH 03/51] [mhz19] Make detection range configurable (#12677) Co-authored-by: Fabio Pugliese Ornellas --- esphome/components/mhz19/mhz19.cpp | 44 ++++++++++++++++++++++++++++++ esphome/components/mhz19/mhz19.h | 29 +++++++++++++++++++- esphome/components/mhz19/sensor.py | 41 ++++++++++++++++++++++++++++ tests/components/mhz19/common.yaml | 1 + 4 files changed, 114 insertions(+), 1 deletion(-) diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index c3c81203624..00e6e14d853 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -13,6 +13,9 @@ static const uint8_t MHZ19_COMMAND_GET_PPM[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x static const uint8_t MHZ19_COMMAND_ABC_ENABLE[] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00}; static const uint8_t MHZ19_COMMAND_ABC_DISABLE[] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00}; static const uint8_t MHZ19_COMMAND_CALIBRATE_ZERO[] = {0xFF, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x07, 0xD0}; +static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88}; +static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x27, 0x10}; uint8_t mhz19_checksum(const uint8_t *command) { uint8_t sum = 0; @@ -28,6 +31,8 @@ void MHZ19Component::setup() { } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) { this->abc_disable(); } + + this->range_set(this->detection_range_); } void MHZ19Component::update() { @@ -86,6 +91,26 @@ void MHZ19Component::abc_disable() { this->mhz19_write_command_(MHZ19_COMMAND_ABC_DISABLE, nullptr); } +void MHZ19Component::range_set(MHZ19DetectionRange detection_ppm) { + switch (detection_ppm) { + case MHZ19_DETECTION_RANGE_DEFAULT: + ESP_LOGV(TAG, "Using previously set detection range (no change)"); + break; + case MHZ19_DETECTION_RANGE_0_2000PPM: + ESP_LOGD(TAG, "Setting detection range to 0 to 2000ppm"); + this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM, nullptr); + break; + case MHZ19_DETECTION_RANGE_0_5000PPM: + ESP_LOGD(TAG, "Setting detection range to 0 to 5000ppm"); + this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM, nullptr); + break; + case MHZ19_DETECTION_RANGE_0_10000PPM: + ESP_LOGD(TAG, "Setting detection range to 0 to 10000ppm"); + this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM, nullptr); + break; + } +} + bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *response) { // Empty RX Buffer while (this->available()) @@ -99,7 +124,9 @@ bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *respo return this->read_array(response, MHZ19_RESPONSE_LENGTH); } + float MHZ19Component::get_setup_priority() const { return setup_priority::DATA; } + void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); @@ -113,6 +140,23 @@ void MHZ19Component::dump_config() { } ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_); + + const char *range_str; + switch (this->detection_range_) { + case MHZ19_DETECTION_RANGE_DEFAULT: + range_str = "default"; + break; + case MHZ19_DETECTION_RANGE_0_2000PPM: + range_str = "0 to 2000ppm"; + break; + case MHZ19_DETECTION_RANGE_0_5000PPM: + range_str = "0 to 5000ppm"; + break; + case MHZ19_DETECTION_RANGE_0_10000PPM: + range_str = "0 to 10000ppm"; + break; + } + ESP_LOGCONFIG(TAG, " Detection range: %s", range_str); } } // namespace mhz19 diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index be36886d622..feb67bdd826 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -8,7 +8,18 @@ namespace esphome { namespace mhz19 { -enum MHZ19ABCLogic { MHZ19_ABC_NONE = 0, MHZ19_ABC_ENABLED, MHZ19_ABC_DISABLED }; +enum MHZ19ABCLogic { + MHZ19_ABC_NONE = 0, + MHZ19_ABC_ENABLED, + MHZ19_ABC_DISABLED, +}; + +enum MHZ19DetectionRange { + MHZ19_DETECTION_RANGE_DEFAULT = 0, + MHZ19_DETECTION_RANGE_0_2000PPM, + MHZ19_DETECTION_RANGE_0_5000PPM, + MHZ19_DETECTION_RANGE_0_10000PPM, +}; class MHZ19Component : public PollingComponent, public uart::UARTDevice { public: @@ -21,11 +32,13 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { void calibrate_zero(); void abc_enable(); void abc_disable(); + void range_set(MHZ19DetectionRange detection_ppm); void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void set_abc_enabled(bool abc_enabled) { abc_boot_logic_ = abc_enabled ? MHZ19_ABC_ENABLED : MHZ19_ABC_DISABLED; } void set_warmup_seconds(uint32_t seconds) { warmup_seconds_ = seconds; } + void set_detection_range(MHZ19DetectionRange detection_range) { detection_range_ = detection_range; } protected: bool mhz19_write_command_(const uint8_t *command, uint8_t *response); @@ -33,7 +46,10 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *co2_sensor_{nullptr}; MHZ19ABCLogic abc_boot_logic_{MHZ19_ABC_NONE}; + uint32_t warmup_seconds_; + + MHZ19DetectionRange detection_range_{MHZ19_DETECTION_RANGE_DEFAULT}; }; template class MHZ19CalibrateZeroAction : public Action { @@ -66,5 +82,16 @@ template class MHZ19ABCDisableAction : public Action { MHZ19Component *mhz19_; }; +template class MHZ19DetectionRangeSetAction : public Action { + public: + MHZ19DetectionRangeSetAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + TEMPLATABLE_VALUE(MHZ19DetectionRange, detection_range) + + void play(const Ts &...x) override { this->mhz19_->range_set(this->detection_range_.value(x...)); } + + protected: + MHZ19Component *mhz19_; +}; + } // namespace mhz19 } // namespace esphome diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 106636a6ba0..0156ee4be06 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -19,6 +19,7 @@ DEPENDENCIES = ["uart"] CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration" CONF_WARMUP_TIME = "warmup_time" +CONF_DETECTION_RANGE = "detection_range" mhz19_ns = cg.esphome_ns.namespace("mhz19") MHZ19Component = mhz19_ns.class_("MHZ19Component", cg.PollingComponent, uart.UARTDevice) @@ -27,6 +28,18 @@ MHZ19CalibrateZeroAction = mhz19_ns.class_( ) MHZ19ABCEnableAction = mhz19_ns.class_("MHZ19ABCEnableAction", automation.Action) MHZ19ABCDisableAction = mhz19_ns.class_("MHZ19ABCDisableAction", automation.Action) +MHZ19DetectionRangeSetAction = mhz19_ns.class_( + "MHZ19DetectionRangeSetAction", automation.Action +) + +mhz19_detection_range = mhz19_ns.enum("MHZ19DetectionRange") +MHZ19_DETECTION_RANGE_ENUM = { + 2000: mhz19_detection_range.MHZ19_DETECTION_RANGE_0_2000PPM, + 5000: mhz19_detection_range.MHZ19_DETECTION_RANGE_0_5000PPM, + 10000: mhz19_detection_range.MHZ19_DETECTION_RANGE_0_10000PPM, +} + +_validate_ppm = cv.float_with_unit("parts per million", "ppm") CONFIG_SCHEMA = ( cv.Schema( @@ -49,6 +62,9 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_WARMUP_TIME, default="75s" ): cv.positive_time_period_seconds, + cv.Optional(CONF_DETECTION_RANGE): cv.All( + _validate_ppm, cv.enum(MHZ19_DETECTION_RANGE_ENUM) + ), } ) .extend(cv.polling_component_schema("60s")) @@ -78,6 +94,9 @@ async def to_code(config): cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME])) + if CONF_DETECTION_RANGE in config: + cg.add(var.set_detection_range(config[CONF_DETECTION_RANGE])) + CALIBRATION_ACTION_SCHEMA = maybe_simple_id( { @@ -98,3 +117,25 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( async def mhz19_calibration_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) + + +RANGE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(MHZ19Component), + cv.Required(CONF_DETECTION_RANGE): cv.All( + _validate_ppm, cv.enum(MHZ19_DETECTION_RANGE_ENUM) + ), + } +) + + +@automation.register_action( + "mhz19.detection_range_set", MHZ19DetectionRangeSetAction, RANGE_ACTION_SCHEMA +) +async def mhz19_detection_range_set_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + detection_range = config.get(CONF_DETECTION_RANGE) + template_ = await cg.templatable(detection_range, args, mhz19_detection_range) + cg.add(var.set_detection_range(template_)) + return var diff --git a/tests/components/mhz19/common.yaml b/tests/components/mhz19/common.yaml index 94989fecbe7..b12ca501975 100644 --- a/tests/components/mhz19/common.yaml +++ b/tests/components/mhz19/common.yaml @@ -6,3 +6,4 @@ sensor: name: MH-Z19 Temperature automatic_baseline_calibration: false update_interval: 15s + detection_range: 5000ppm From ede7391582cc2842f89de62efb105d2836481a3b Mon Sep 17 00:00:00 2001 From: Conrad Juhl Andersen Date: Sat, 3 Jan 2026 23:06:33 +0100 Subject: [PATCH 04/51] [wts01] Fix negative values for WTS01 sensor (#12835) --- esphome/components/wts01/wts01.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/wts01/wts01.cpp b/esphome/components/wts01/wts01.cpp index cb910d89cf1..a7948c805a1 100644 --- a/esphome/components/wts01/wts01.cpp +++ b/esphome/components/wts01/wts01.cpp @@ -71,17 +71,20 @@ void WTS01Sensor::process_packet_() { } // Extract temperature value - int8_t temp = this->buffer_[6]; - int32_t sign = 1; + const uint8_t raw = this->buffer_[6]; - // Handle negative temperatures - if (temp < 0) { - sign = -1; + // WTS01 encodes sign in bit 7, magnitude in bits 0-6 + const bool negative = (raw & 0x80) != 0; + const uint8_t magnitude = raw & 0x7F; + + const float decimal = static_cast(this->buffer_[7]) / 100.0f; + + float temperature = static_cast(magnitude) + decimal; + + if (negative) { + temperature = -temperature; } - // Calculate temperature (temp + decimal/100) - float temperature = static_cast(temp) + (sign * static_cast(this->buffer_[7]) / 100.0f); - ESP_LOGV(TAG, "Received new temperature: %.2f°C", temperature); this->publish_state(temperature); From a6e9aa78765b4659fcbb51c893faa6b9c9f42753 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Sat, 3 Jan 2026 23:11:02 +0100 Subject: [PATCH 05/51] [mhz19] Refactor Actions to Parented (#12837) --- esphome/components/mhz19/mhz19.h | 35 +++++++----------------------- esphome/components/mhz19/sensor.py | 33 +++++++++++++++++----------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index feb67bdd826..5898bab649e 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -52,45 +52,26 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { MHZ19DetectionRange detection_range_{MHZ19_DETECTION_RANGE_DEFAULT}; }; -template class MHZ19CalibrateZeroAction : public Action { +template class MHZ19CalibrateZeroAction : public Action, public Parented { public: - MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - - void play(const Ts &...x) override { this->mhz19_->calibrate_zero(); } - - protected: - MHZ19Component *mhz19_; + void play(const Ts &...x) override { this->parent_->calibrate_zero(); } }; -template class MHZ19ABCEnableAction : public Action { +template class MHZ19ABCEnableAction : public Action, public Parented { public: - MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - - void play(const Ts &...x) override { this->mhz19_->abc_enable(); } - - protected: - MHZ19Component *mhz19_; + void play(const Ts &...x) override { this->parent_->abc_enable(); } }; -template class MHZ19ABCDisableAction : public Action { +template class MHZ19ABCDisableAction : public Action, public Parented { public: - MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - - void play(const Ts &...x) override { this->mhz19_->abc_disable(); } - - protected: - MHZ19Component *mhz19_; + void play(const Ts &...x) override { this->parent_->abc_disable(); } }; -template class MHZ19DetectionRangeSetAction : public Action { +template class MHZ19DetectionRangeSetAction : public Action, public Parented { public: - MHZ19DetectionRangeSetAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} TEMPLATABLE_VALUE(MHZ19DetectionRange, detection_range) - void play(const Ts &...x) override { this->mhz19_->range_set(this->detection_range_.value(x...)); } - - protected: - MHZ19Component *mhz19_; + void play(const Ts &...x) override { this->parent_->range_set(this->detection_range_.value(x...)); } }; } // namespace mhz19 diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 0156ee4be06..1f698be4043 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -24,12 +24,18 @@ CONF_DETECTION_RANGE = "detection_range" mhz19_ns = cg.esphome_ns.namespace("mhz19") MHZ19Component = mhz19_ns.class_("MHZ19Component", cg.PollingComponent, uart.UARTDevice) MHZ19CalibrateZeroAction = mhz19_ns.class_( - "MHZ19CalibrateZeroAction", automation.Action + "MHZ19CalibrateZeroAction", automation.Action, cg.Parented.template(MHZ19Component) +) +MHZ19ABCEnableAction = mhz19_ns.class_( + "MHZ19ABCEnableAction", automation.Action, cg.Parented.template(MHZ19Component) +) +MHZ19ABCDisableAction = mhz19_ns.class_( + "MHZ19ABCDisableAction", automation.Action, cg.Parented.template(MHZ19Component) ) -MHZ19ABCEnableAction = mhz19_ns.class_("MHZ19ABCEnableAction", automation.Action) -MHZ19ABCDisableAction = mhz19_ns.class_("MHZ19ABCDisableAction", automation.Action) MHZ19DetectionRangeSetAction = mhz19_ns.class_( - "MHZ19DetectionRangeSetAction", automation.Action + "MHZ19DetectionRangeSetAction", + automation.Action, + cg.Parented.template(MHZ19Component), ) mhz19_detection_range = mhz19_ns.enum("MHZ19DetectionRange") @@ -98,7 +104,7 @@ async def to_code(config): cg.add(var.set_detection_range(config[CONF_DETECTION_RANGE])) -CALIBRATION_ACTION_SCHEMA = maybe_simple_id( +NO_ARGS_ACTION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(MHZ19Component), } @@ -106,17 +112,18 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "mhz19.calibrate_zero", MHZ19CalibrateZeroAction, CALIBRATION_ACTION_SCHEMA + "mhz19.calibrate_zero", MHZ19CalibrateZeroAction, NO_ARGS_ACTION_SCHEMA ) @automation.register_action( - "mhz19.abc_enable", MHZ19ABCEnableAction, CALIBRATION_ACTION_SCHEMA + "mhz19.abc_enable", MHZ19ABCEnableAction, NO_ARGS_ACTION_SCHEMA ) @automation.register_action( - "mhz19.abc_disable", MHZ19ABCDisableAction, CALIBRATION_ACTION_SCHEMA + "mhz19.abc_disable", MHZ19ABCDisableAction, NO_ARGS_ACTION_SCHEMA ) -async def mhz19_calibration_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) +async def mhz19_no_args_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var RANGE_ACTION_SCHEMA = maybe_simple_id( @@ -133,8 +140,8 @@ RANGE_ACTION_SCHEMA = maybe_simple_id( "mhz19.detection_range_set", MHZ19DetectionRangeSetAction, RANGE_ACTION_SCHEMA ) async def mhz19_detection_range_set_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) detection_range = config.get(CONF_DETECTION_RANGE) template_ = await cg.templatable(detection_range, args, mhz19_detection_range) cg.add(var.set_detection_range(template_)) From 0a0501c1403206c2f7ee6e6f68252e3680142dce Mon Sep 17 00:00:00 2001 From: John Hollowell Date: Sat, 3 Jan 2026 17:11:48 -0500 Subject: [PATCH 06/51] Fix comment typos (#12828) --- esphome/components/i2c/__init__.py | 2 +- esphome/components/one_wire/__init__.py | 2 +- esphome/components/uart/__init__.py | 2 +- esphome/cpp_generator.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 56e0c8e4ab1..19efda0b492 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -250,7 +250,7 @@ async def register_i2c_device(var, config): Sets the i2c bus to use and the i2c address. - This is a coroutine, you need to await it with a 'yield' expression! + This is a coroutine, you need to await it with an 'await' expression! """ parent = await cg.get_variable(config[CONF_I2C_ID]) cg.add(var.set_i2c_bus(parent)) diff --git a/esphome/components/one_wire/__init__.py b/esphome/components/one_wire/__init__.py index e12cca3e27f..9173b7014bd 100644 --- a/esphome/components/one_wire/__init__.py +++ b/esphome/components/one_wire/__init__.py @@ -32,7 +32,7 @@ async def register_one_wire_device(var, config): Sets the 1-wire bus to use and the 1-wire address. - This is a coroutine, you need to await it with a 'yield' expression! + This is a coroutine, you need to await it with an 'await' expression! """ parent = await cg.get_variable(config[CONF_ONE_WIRE_ID]) cg.add(var.set_one_wire_bus(parent)) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 9baa6ebd81e..9ec95964ece 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -482,7 +482,7 @@ def final_validate_device_schema( async def register_uart_device(var, config): """Register a UART device, setting up all the internal values. - This is a coroutine, you need to await it with a 'yield' expression! + This is a coroutine, you need to await it with an 'await' expression! """ parent = await cg.get_variable(config[CONF_UART_ID]) cg.add(var.set_uart_parent(parent)) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index ddccb574e44..cff0748c956 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -643,7 +643,7 @@ async def get_variable(id_: ID) -> "MockObj": Wait for the given ID to be defined in the code generation and return it as a MockObj. - This is a coroutine, you need to await it with a 'await' expression! + This is a coroutine, you need to await it with an 'await' expression! :param id_: The ID to retrieve :return: The variable as a MockObj. @@ -656,7 +656,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: Wait for the given ID to be defined in the code generation and return it as a MockObj. - This is a coroutine, you need to await it with a 'await' expression! + This is a coroutine, you need to await it with an 'await' expression! :param id_: The ID to retrieve :return: The variable as a MockObj. From 9781073f2a78aa0f416a5c7ea09090bfc8f53223 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 12:31:38 -1000 Subject: [PATCH 07/51] [espnow] Use stack-based MAC formatting and remove dead code (#12836) --- .../components/espnow/espnow_component.cpp | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 16e2331937b..991803d8703 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -64,18 +64,6 @@ static const LogString *espnow_error_to_str(esp_err_t error) { } } -std::string peer_str(uint8_t *peer) { - if (peer == nullptr || peer[0] == 0) { - return "[Not Set]"; - } else if (memcmp(peer, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { - return "[Broadcast]"; - } else if (memcmp(peer, ESPNOW_MULTICAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { - return "[Multicast]"; - } else { - return format_mac_address_pretty(peer); - } -} - #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status) #else @@ -140,11 +128,13 @@ void ESPNowComponent::dump_config() { ESP_LOGCONFIG(TAG, " Disabled"); return; } + char own_addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(this->own_address_, own_addr_buf); ESP_LOGCONFIG(TAG, " Own address: %s\n" " Version: v%" PRIu32 "\n" " Wi-Fi channel: %d", - format_mac_address_pretty(this->own_address_).c_str(), version, this->wifi_channel_); + own_addr_buf, version, this->wifi_channel_); #ifdef USE_WIFI ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled())); #endif @@ -300,9 +290,12 @@ void ESPNowComponent::loop() { // Intentionally left as if instead of else in case the peer is added above if (esp_now_is_peer_exist(info.src_addr)) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)]; - ESP_LOGV(TAG, "<<< [%s -> %s] %s", format_mac_address_pretty(info.src_addr).c_str(), - format_mac_address_pretty(info.des_addr).c_str(), + format_mac_addr_upper(info.src_addr, src_buf); + format_mac_addr_upper(info.des_addr, dst_buf); + ESP_LOGV(TAG, "<<< [%s -> %s] %s", src_buf, dst_buf, format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size)); #endif if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { @@ -321,8 +314,9 @@ void ESPNowComponent::loop() { } case ESPNowPacket::SENT: { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - ESP_LOGV(TAG, ">>> [%s] %s", format_mac_address_pretty(packet->packet_.sent.address).c_str(), - LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status))); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(packet->packet_.sent.address, addr_buf); + ESP_LOGV(TAG, ">>> [%s] %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status))); #endif if (this->current_send_packet_ != nullptr) { this->current_send_packet_->callback_(packet->packet_.sent.status); @@ -409,8 +403,9 @@ void ESPNowComponent::send_() { this->current_send_packet_ = packet; esp_err_t err = esp_now_send(packet->address_, packet->data_, packet->size_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to send packet to %s - %s", format_mac_address_pretty(packet->address_).c_str(), - LOG_STR_ARG(espnow_error_to_str(err))); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(packet->address_, addr_buf); + ESP_LOGE(TAG, "Failed to send packet to %s - %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(err))); if (packet->callback_ != nullptr) { packet->callback_(err); } @@ -439,8 +434,9 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) { esp_err_t err = esp_now_add_peer(&peer_info); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to add peer %s - %s", format_mac_address_pretty(peer).c_str(), - LOG_STR_ARG(espnow_error_to_str(err))); + char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(peer, peer_buf); + ESP_LOGE(TAG, "Failed to add peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err))); this->status_momentary_warning("peer-add-failed"); return err; } @@ -468,8 +464,9 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) { if (esp_now_is_peer_exist(peer)) { esp_err_t err = esp_now_del_peer(peer); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to delete peer %s - %s", format_mac_address_pretty(peer).c_str(), - LOG_STR_ARG(espnow_error_to_str(err))); + char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(peer, peer_buf); + ESP_LOGE(TAG, "Failed to delete peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err))); this->status_momentary_warning("peer-del-failed"); return err; } From d505f0316b7df4ccd2afc0fe3f58a3c991130c91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 12:31:58 -1000 Subject: [PATCH 08/51] [wifi] Combine scan result log lines to reduce loop blocking with many matching APs (#12830) --- esphome/components/wifi/wifi_component.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a0a7d3d9467..0738a767770 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1049,15 +1049,27 @@ template static void insertion_sort_scan_results(VectorType } // Helper function to log matching scan results - marked noinline to prevent re-inlining into loop +// +// IMPORTANT: This function deliberately uses a SINGLE log call to minimize blocking. +// In environments with many matching networks (e.g., 18+ mesh APs), multiple log calls +// per network would block the main loop for an unacceptable duration. Each log call +// has overhead from UART transmission, so combining INFO+DEBUG into one line halves +// the blocking time. Do NOT split this into separate ESP_LOGI/ESP_LOGD calls. __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) { char bssid_s[18]; auto bssid = res.get_bssid(); format_mac_addr_upper(bssid.data(), bssid_s); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + // Single combined log line with all details when DEBUG enabled + ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s Ch:%2u %3ddB P:%d", res.get_ssid().c_str(), + res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi())), res.get_channel(), res.get_rssi(), res.get_priority()); +#else ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority()); +#endif } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE From 6685fa1da98a4c422c3aa5455eb28fe02ec64518 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 12:32:10 -1000 Subject: [PATCH 09/51] [core] Fix startup delay from setup timing logs when console connected (#12832) --- esphome/core/component.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 97ab2edb5aa..90be6cf6460 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -205,7 +205,13 @@ void Component::call() { this->call_setup(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG uint32_t setup_time = millis() - start_time; - ESP_LOGCONFIG(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time); + // Only log at CONFIG level if setup took longer than the blocking threshold + // to avoid spamming the log and blocking the event loop + if (setup_time >= WARN_IF_BLOCKING_OVER_MS) { + ESP_LOGCONFIG(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time); + } else { + ESP_LOGV(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time); + } #endif break; } From cb3edfc654af364e9a6cf2bb2b59acaa284b42c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 12:32:22 -1000 Subject: [PATCH 10/51] [wifi] Use stack-based MAC formatting in ESP8266 and IDF event handlers (#12834) --- .../wifi/wifi_component_esp8266.cpp | 33 +++++++++++++++---- .../wifi/wifi_component_esp_idf.cpp | 25 +++++++++++--- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 055a13afc82..9d99e0b94c5 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -518,8 +518,12 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { auto it = event->event_info.connected; - ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=%s channel=%u", it.ssid_len, (const char *) it.ssid, - format_mac_address_pretty(it.bssid).c_str(), it.channel); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.bssid, bssid_buf); + ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=%s channel=%u", it.ssid_len, (const char *) it.ssid, bssid_buf, + it.channel); +#endif s_sta_connected = true; #ifdef USE_WIFI_LISTENERS for (auto *listener : global_wifi_component->connect_state_listeners_) { @@ -594,18 +598,30 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { break; } case EVENT_SOFTAPMODE_STACONNECTED: { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE auto it = event->event_info.sta_connected; - ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", mac_buf, it.aid); +#endif break; } case EVENT_SOFTAPMODE_STADISCONNECTED: { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE auto it = event->event_info.sta_disconnected; - ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", mac_buf, it.aid); +#endif break; } case EVENT_SOFTAPMODE_PROBEREQRECVED: { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE auto it = event->event_info.ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", mac_buf, it.rssi); +#endif break; } #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) @@ -616,9 +632,12 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { break; } case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE auto it = event->event_info.distribute_sta_ip; - ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), - format_ip_addr(it.ip).c_str(), it.aid); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", mac_buf, format_ip_addr(it.ip).c_str(), it.aid); +#endif break; } #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index f68a095bff5..820725ed31d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -734,9 +734,12 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_CONNECTED) { const auto &it = data->data.sta_connected; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.bssid, bssid_buf); ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", it.ssid_len, - (const char *) it.ssid, format_mac_address_pretty(it.bssid).c_str(), it.channel, - get_auth_mode_str(it.authmode)); + (const char *) it.ssid, bssid_buf, it.channel, get_auth_mode_str(it.authmode)); +#endif s_sta_connected = true; #ifdef USE_WIFI_LISTENERS for (auto *listener : this->connect_state_listeners_) { @@ -855,16 +858,28 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { this->ap_started_ = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE const auto &it = data->data.ap_probe_req_rx; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", mac_buf, it.rssi); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE const auto &it = data->data.ap_staconnected; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(it.mac).c_str()); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP client connected MAC=%s", mac_buf); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE const auto &it = data->data.ap_stadisconnected; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(it.mac).c_str()); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf); +#endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; From c29aa61e2a0fef32cc9bf3bbdfa33c8a8b5a3b44 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 4 Jan 2026 09:08:47 +1000 Subject: [PATCH 11/51] [image] Use alternative version of CairoSVG on Windows (#12811) --- requirements.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 833ccbb0ed4..bada581f56b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,13 @@ ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 pillow==11.3.0 -cairosvg==2.8.2 + +# pycairo fork for Windows +cairosvg @ git+https://github.com/clydebarrow/cairosvg.git@release ; sys_platform == 'win32' + +# Original for everything else +cairosvg==2.8.2 ; sys_platform != 'win32' + freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 From 2e2e54811ac3f98b9eaccd4eac919e1ed0690004 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 13:52:23 -1000 Subject: [PATCH 12/51] [absolute_humidity] Combine log statements to reduce loop blocking (#12838) --- esphome/components/absolute_humidity/absolute_humidity.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 74d675b80b8..b13fcd519aa 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -90,13 +90,16 @@ void AbsoluteHumidityComponent::loop() { this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); return; } - ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); // Calculate absolute humidity const float absolute_humidity = vapor_density(es, hr, temperature_k); + ESP_LOGD(TAG, + "Saturation vapor pressure %f kPa\n" + "Publishing absolute humidity %f g/m³", + es, absolute_humidity); + // Publish absolute humidity - ESP_LOGD(TAG, "Publishing absolute humidity %f g/m³", absolute_humidity); this->status_clear_warning(); this->publish_state(absolute_humidity); } From ec05692f0db5394c1d2d7c9097be9196c991a781 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 4 Jan 2026 01:12:31 +0100 Subject: [PATCH 13/51] [nrf52] add printk doc (#12839) --- esphome/components/logger/logger_zephyr.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 330dfa96ec6..41f53beec04 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -66,6 +66,8 @@ void Logger::pre_setup() { void HOT Logger::write_msg_(const char *msg, size_t len) { // Single write with newline already in buffer (added by caller) #ifdef CONFIG_PRINTK + // Requires the debug component and an active SWD connection. + // It is used for pyocd rtt -t nrf52840 k_str_out(const_cast(msg), len); #endif if (this->uart_dev_ == nullptr) { From f11abc7dbfb6b0d2c8c8d48baca54094fd534297 Mon Sep 17 00:00:00 2001 From: Douwe <61123717+dhoeben@users.noreply.github.com> Date: Sun, 4 Jan 2026 01:45:49 +0100 Subject: [PATCH 14/51] [water_heater] (2/4) Implement template for new water_heater component (#12516) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../template/water_heater/__init__.py | 123 ++++++++++++++++++ .../template/water_heater/automation.h | 35 +++++ .../water_heater/template_water_heater.cpp | 88 +++++++++++++ .../water_heater/template_water_heater.h | 53 ++++++++ tests/components/template/common-base.yaml | 30 +++++ 5 files changed, 329 insertions(+) create mode 100644 esphome/components/template/water_heater/__init__.py create mode 100644 esphome/components/template/water_heater/automation.h create mode 100644 esphome/components/template/water_heater/template_water_heater.cpp create mode 100644 esphome/components/template/water_heater/template_water_heater.h diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py new file mode 100644 index 00000000000..716289035a2 --- /dev/null +++ b/esphome/components/template/water_heater/__init__.py @@ -0,0 +1,123 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import water_heater +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MODE, + CONF_OPTIMISTIC, + CONF_RESTORE_MODE, + CONF_SET_ACTION, + CONF_SUPPORTED_MODES, + CONF_TARGET_TEMPERATURE, +) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType + +from .. import template_ns + +CONF_CURRENT_TEMPERATURE = "current_temperature" + +TemplateWaterHeater = template_ns.class_( + "TemplateWaterHeater", water_heater.WaterHeater +) + +TemplateWaterHeaterPublishAction = template_ns.class_( + "TemplateWaterHeaterPublishAction", + automation.Action, + cg.Parented.template(TemplateWaterHeater), +) + +TemplateWaterHeaterRestoreMode = template_ns.enum("TemplateWaterHeaterRestoreMode") +RESTORE_MODES = { + "NO_RESTORE": TemplateWaterHeaterRestoreMode.WATER_HEATER_NO_RESTORE, + "RESTORE": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE, + "RESTORE_AND_CALL": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE_AND_CALL, +} + +CONFIG_SCHEMA = water_heater.water_heater_schema(TemplateWaterHeater).extend( + { + cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, + cv.Optional(CONF_MODE): cv.returning_lambda, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + water_heater.validate_water_heater_mode + ), + } +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await water_heater.register_water_heater(var, config) + + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [], config[CONF_SET_ACTION] + ) + + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + + if CONF_CURRENT_TEMPERATURE in config: + template_ = await cg.process_lambda( + config[CONF_CURRENT_TEMPERATURE], + [], + return_type=cg.optional.template(cg.float_), + ) + cg.add(var.set_current_temperature_lambda(template_)) + + if CONF_MODE in config: + template_ = await cg.process_lambda( + config[CONF_MODE], + [], + return_type=cg.optional.template(water_heater.WaterHeaterMode), + ) + cg.add(var.set_mode_lambda(template_)) + + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + + +@automation.register_action( + "water_heater.template.publish", + TemplateWaterHeaterPublishAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(TemplateWaterHeater), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_MODE): cv.templatable( + water_heater.validate_water_heater_mode + ), + } + ), +) +async def water_heater_template_publish_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + if current_temp := config.get(CONF_CURRENT_TEMPERATURE): + template_ = await cg.templatable(current_temp, args, float) + cg.add(var.set_current_temperature(template_)) + + if target_temp := config.get(CONF_TARGET_TEMPERATURE): + template_ = await cg.templatable(target_temp, args, float) + cg.add(var.set_target_temperature(template_)) + + if mode := config.get(CONF_MODE): + template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode) + cg.add(var.set_mode(template_)) + + return var diff --git a/esphome/components/template/water_heater/automation.h b/esphome/components/template/water_heater/automation.h new file mode 100644 index 00000000000..3dad2b85ae9 --- /dev/null +++ b/esphome/components/template/water_heater/automation.h @@ -0,0 +1,35 @@ +#pragma once + +#include "template_water_heater.h" +#include "esphome/core/automation.h" + +namespace esphome::template_ { + +template +class TemplateWaterHeaterPublishAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, current_temperature) + TEMPLATABLE_VALUE(float, target_temperature) + TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode) + + void play(const Ts &...x) override { + if (this->current_temperature_.has_value()) { + this->parent_->set_current_temperature(this->current_temperature_.value(x...)); + } + bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value(); + if (needs_call) { + auto call = this->parent_->make_call(); + if (this->target_temperature_.has_value()) { + call.set_target_temperature(this->target_temperature_.value(x...)); + } + if (this->mode_.has_value()) { + call.set_mode(this->mode_.value(x...)); + } + call.perform(); + } else { + this->parent_->publish_state(); + } + } +}; + +} // namespace esphome::template_ diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp new file mode 100644 index 00000000000..18ef8d3f061 --- /dev/null +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -0,0 +1,88 @@ +#include "template_water_heater.h" +#include "esphome/core/log.h" + +namespace esphome::template_ { + +static const char *const TAG = "template.water_heater"; + +TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {} + +void TemplateWaterHeater::setup() { + if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE || + this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE_AND_CALL) { + auto restore = this->restore_state(); + + if (restore.has_value()) { + restore->perform(); + } + } + if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value()) + this->disable_loop(); +} + +water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { + auto traits = water_heater::WaterHeater::get_traits(); + + if (!this->supported_modes_.empty()) { + traits.set_supported_modes(this->supported_modes_); + } + + traits.set_supports_current_temperature(true); + return traits; +} + +void TemplateWaterHeater::loop() { + bool changed = false; + + auto curr_temp = this->current_temperature_f_.call(); + if (curr_temp.has_value()) { + if (*curr_temp != this->current_temperature_) { + this->current_temperature_ = *curr_temp; + changed = true; + } + } + + auto new_mode = this->mode_f_.call(); + if (new_mode.has_value()) { + if (*new_mode != this->mode_) { + this->mode_ = *new_mode; + changed = true; + } + } + + if (changed) { + this->publish_state(); + } +} + +void TemplateWaterHeater::dump_config() { + LOG_WATER_HEATER("", "Template Water Heater", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); +} + +float TemplateWaterHeater::get_setup_priority() const { return setup_priority::HARDWARE; } + +water_heater::WaterHeaterCallInternal TemplateWaterHeater::make_call() { + return water_heater::WaterHeaterCallInternal(this); +} + +void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { + if (call.get_mode().has_value()) { + if (this->optimistic_) { + this->mode_ = *call.get_mode(); + } + } + if (!std::isnan(call.get_target_temperature())) { + if (this->optimistic_) { + this->target_temperature_ = call.get_target_temperature(); + } + } + + this->set_trigger_->trigger(); + + if (this->optimistic_) { + this->publish_state(); + } +} + +} // namespace esphome::template_ diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h new file mode 100644 index 00000000000..e5f51b72dcd --- /dev/null +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" +#include "esphome/components/water_heater/water_heater.h" + +namespace esphome::template_ { + +enum TemplateWaterHeaterRestoreMode { + WATER_HEATER_NO_RESTORE, + WATER_HEATER_RESTORE, + WATER_HEATER_RESTORE_AND_CALL, +}; + +class TemplateWaterHeater : public water_heater::WaterHeater { + public: + TemplateWaterHeater(); + + template void set_current_temperature_lambda(F &&f) { + this->current_temperature_f_.set(std::forward(f)); + } + template void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward(f)); } + + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + void set_supported_modes(const std::initializer_list &modes) { + this->supported_modes_ = modes; + } + + Trigger<> *get_set_trigger() const { return this->set_trigger_; } + + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + water_heater::WaterHeaterCallInternal make_call() override; + + protected: + void control(const water_heater::WaterHeaterCall &call) override; + water_heater::WaterHeaterTraits traits() override; + + // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller + Trigger<> *set_trigger_; + TemplateLambda current_temperature_f_; + TemplateLambda mode_f_; + TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; + water_heater::WaterHeaterModeMask supported_modes_; + bool optimistic_{true}; +}; + +} // namespace esphome::template_ diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index f101eea942c..e050c0b3070 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -9,6 +9,18 @@ esphome: id: template_sens state: !lambda "return 42.0;" + - water_heater.template.publish: + id: template_water_heater + target_temperature: 50.0 + mode: ECO + + # Templated + - water_heater.template.publish: + id: template_water_heater + current_temperature: !lambda "return 45.0;" + target_temperature: !lambda "return 55.0;" + mode: !lambda "return water_heater::WATER_HEATER_MODE_GAS;" + # Test C++ API: set_template() with stateless lambda (no captures) # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. - lambda: |- @@ -299,6 +311,24 @@ alarm_control_panel: codes: - "1234" +water_heater: + - platform: template + id: template_water_heater + name: "Template Water Heater" + optimistic: true + current_temperature: !lambda "return 42.0f;" + mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" + supported_modes: + - "OFF" + - ECO + - GAS + - ELECTRIC + - HEAT_PUMP + - HIGH_DEMAND + - PERFORMANCE + set_action: + - logger.log: "set_action" + datetime: - platform: template name: Date From d7a1ac83ca5e76e4d157385d706b4c07726894ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:00:51 -1000 Subject: [PATCH 15/51] [esp32_ble_tracker] Combine log statements to reduce loop blocking (#12860) --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 63675ec3774..73a5dfb187c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -657,8 +657,10 @@ void ESP32BLETracker::dump_config() { " Continuous Scanning: %s", this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f, this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); - ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_)); - ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, + ESP_LOGCONFIG(TAG, + " Scanner State: %s\n" + " Connecting: %d, discovered: %d, disconnecting: %d", + this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting, this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); if (this->scan_start_fail_count_) { ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); From 5e24469ce34d64c99f78352f6fb0342baf84883d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:01:01 -1000 Subject: [PATCH 16/51] [http_request] Combine log statements to reduce loop blocking (#12859) --- esphome/components/http_request/ota/ota_http_request.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 2cd7489e38f..2a7db9137f9 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -105,9 +105,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() { // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it md5_receive.init(); - ESP_LOGV(TAG, "MD5Digest initialized"); - - ESP_LOGV(TAG, "OTA backend begin"); + ESP_LOGV(TAG, "MD5Digest initialized\n" + "OTA backend begin"); auto backend = ota::make_ota_backend(); auto error_code = backend->begin(container->content_length); if (error_code != ota::OTA_RESPONSE_OK) { From 5f5edf90e9ff45b86089e3eda127c298f10c74f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:01:12 -1000 Subject: [PATCH 17/51] [water_heater] Combine log statements to reduce loop blocking (#12858) --- esphome/components/water_heater/water_heater.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 441872ec000..d092203d061 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -152,8 +152,10 @@ void WaterHeater::setup() { void WaterHeater::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(this->mode_))); + ESP_LOGD(TAG, + "'%s' - Sending state:\n" + " Mode: %s", + this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_))); if (!std::isnan(this->current_temperature_)) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature_); } From 07a581e13a68682d86d17e11cc1fa66d4743fb22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:01:24 -1000 Subject: [PATCH 18/51] [update] Combine log statements to reduce loop blocking (#12857) --- esphome/components/update/update_entity.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 567fc9fc8e0..6d13341a8a9 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -9,8 +9,10 @@ namespace update { static const char *const TAG = "update"; void UpdateEntity::publish_state() { - ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str()); - ESP_LOGD(TAG, " Current Version: %s", this->update_info_.current_version.c_str()); + ESP_LOGD(TAG, + "'%s' - Publishing:\n" + " Current Version: %s", + this->name_.c_str(), this->update_info_.current_version.c_str()); if (!this->update_info_.md5.empty()) { ESP_LOGD(TAG, " Latest Version: %s", this->update_info_.latest_version.c_str()); From 2a6b192af8a330cef9714ca6aeaf624fd8568451 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:01:35 -1000 Subject: [PATCH 19/51] [ethernet] Combine log statements to reduce loop blocking (#12854) --- esphome/components/ethernet/ethernet_component.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index af4f652d8b1..896c5cc8741 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -813,8 +813,10 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi ESPHL_ERROR_CHECK(err, "Select PHY Register page failed"); } - ESP_LOGD(TAG, "Writing to PHY Register Address: 0x%02" PRIX32, register_data.address); - ESP_LOGD(TAG, "Writing to PHY Register Value: 0x%04" PRIX32, register_data.value); + ESP_LOGD(TAG, + "Writing to PHY Register Address: 0x%02" PRIX32 "\n" + "Writing to PHY Register Value: 0x%04" PRIX32, + register_data.address, register_data.value); err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); From d364432e3a095d85a63b18a17aa266a9cd8c4cb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:02:12 -1000 Subject: [PATCH 20/51] [uart] Combine log statements to reduce loop blocking (#12855) --- esphome/components/uart/uart_component_libretiny.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 01c7063fe86..863732c88d1 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -120,8 +120,10 @@ void LibreTinyUARTComponent::setup() { void LibreTinyUARTComponent::dump_config() { bool is_software = this->hardware_idx_ == -1; - ESP_LOGCONFIG(TAG, "UART Bus:"); - ESP_LOGCONFIG(TAG, " Type: %s", UART_TYPE[is_software]); + ESP_LOGCONFIG(TAG, + "UART Bus:\n" + " Type: %s", + UART_TYPE[is_software]); if (!is_software) { ESP_LOGCONFIG(TAG, " Port number: %d", this->hardware_idx_); } From 8ddfeb2d38588e1d1f99b58f318f6e1bb8f5e931 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:02:26 -1000 Subject: [PATCH 21/51] [captive_portal] Combine log statements to reduce loop blocking (#12853) --- esphome/components/captive_portal/captive_portal.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 749aa705df4..d0515166b61 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -49,9 +49,11 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr) std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr) - ESP_LOGI(TAG, "Requested WiFi Settings Change:"); - ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); - ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); + ESP_LOGI(TAG, + "Requested WiFi Settings Change:\n" + " SSID='%s'\n" + " Password=" LOG_SECRET("'%s'"), + ssid.c_str(), psk.c_str()); // Defer save to main loop thread to avoid NVS operations from HTTP thread this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); request->redirect(ESPHOME_F("/?save")); From 41a188ac3589bf646e4297840da316869fb72335 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:03:01 -1000 Subject: [PATCH 22/51] [ac_dimmer] Fix ESP8266 build by requiring waveform support (#12852) --- esphome/components/ac_dimmer/output.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index 5e24779510e..9f9afb6d807 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER +from esphome.core import CORE CODEOWNERS = ["@glmnet"] @@ -36,6 +37,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + if CORE.is_esp8266: + # ac_dimmer uses setTimer1Callback which requires the waveform generator + from esphome.components.esp8266.const import require_waveform + + require_waveform() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) From ea848db683867152fe731202438ce1efb98ae227 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:03:20 -1000 Subject: [PATCH 23/51] [bp1658cj] Combine log statements to reduce loop blocking (#12851) --- esphome/components/bp1658cj/bp1658cj.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp index b8ad5dc3d23..d5516384ff3 100644 --- a/esphome/components/bp1658cj/bp1658cj.cpp +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -22,13 +22,13 @@ void BP1658CJ::setup() { this->pwm_amounts_.resize(5, 0); } void BP1658CJ::dump_config() { - ESP_LOGCONFIG(TAG, "BP1658CJ:"); - LOG_PIN(" Data Pin: ", this->data_pin_); - LOG_PIN(" Clock Pin: ", this->clock_pin_); ESP_LOGCONFIG(TAG, + "BP1658CJ:\n" " Color Channels Max Power: %u\n" " White Channels Max Power: %u", this->max_power_color_channels_, this->max_power_white_channels_); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); } void BP1658CJ::loop() { From 0196d6ee5573c338f3823a7a649c23dcd46cac45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:03:44 -1000 Subject: [PATCH 24/51] [ble_nus] Combine log statements to reduce loop blocking (#12850) --- esphome/components/ble_nus/ble_nus.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index bd80592d895..0de65b623f6 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -103,8 +103,10 @@ void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t #endif void BLENUS::dump_config() { - ESP_LOGCONFIG(TAG, "ble nus:"); - ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_)); + ESP_LOGCONFIG(TAG, + "ble nus:\n" + " log: %s", + YESNO(this->expose_log_)); uint32_t mtu = 0; bt_conn *conn = this->conn_.load(); if (conn) { From 41e7ecb29fd86a7468b6f93ac6eff3a64a504b57 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:04:21 -1000 Subject: [PATCH 25/51] [bedjet] Combine log statements to reduce loop blocking (#12848) --- esphome/components/bedjet/bedjet_hub.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 38fcf29b3bb..a3054cf48ee 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -216,11 +216,14 @@ bool BedJetHub::discover_characteristics_() { } } - ESP_LOGI(TAG, "[%s] Discovered service characteristics: ", this->get_name().c_str()); - ESP_LOGI(TAG, " - Command char: 0x%x", this->char_handle_cmd_); - ESP_LOGI(TAG, " - Status char: 0x%x", this->char_handle_status_); - ESP_LOGI(TAG, " - config descriptor: 0x%x", this->config_descr_status_); - ESP_LOGI(TAG, " - Name char: 0x%x", this->char_handle_name_); + ESP_LOGI(TAG, + "[%s] Discovered service characteristics:\n" + " - Command char: 0x%x\n" + " - Status char: 0x%x\n" + " - config descriptor: 0x%x\n" + " - Name char: 0x%x", + this->get_name().c_str(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_, + this->char_handle_name_); return result; } From 6bbee3cfc67a522a26c785fb90831500f5bfe37f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:04:38 -1000 Subject: [PATCH 26/51] [as3935] Combine log statements to reduce loop blocking (#12846) --- esphome/components/as3935/as3935.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index 2609af07d3e..93a0bff5b31 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -305,12 +305,14 @@ bool AS3935Component::calibrate_oscillator() { } void AS3935Component::tune_antenna() { - ESP_LOGI(TAG, "Starting antenna tuning"); uint8_t div_ratio = this->read_div_ratio(); uint8_t tune_val = this->read_capacitance(); - ESP_LOGI(TAG, "Division Ratio is set to: %d", div_ratio); - ESP_LOGI(TAG, "Internal Capacitor is set to: %d", tune_val); - ESP_LOGI(TAG, "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio"); + ESP_LOGI(TAG, + "Starting antenna tuning\n" + "Division Ratio is set to: %d\n" + "Internal Capacitor is set to: %d\n" + "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", + div_ratio, tune_val); this->display_oscillator(true, ANTFREQ); } From d84562f87882b7cee98701a13830a62b24d67b10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:04:57 -1000 Subject: [PATCH 27/51] [anova] Combine log statements to reduce loop blocking (#12845) --- esphome/components/anova/anova.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 2693224a97b..50544880899 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -67,8 +67,10 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); - ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); + ESP_LOGW(TAG, + "[%s] No control service found at device, not an Anova..?\n" + "[%s] Note, this component does not currently support Anova Nano.", + this->get_name().c_str(), this->get_name().c_str()); break; } this->char_handle_ = chr->handle; From 9cb265347ce27d809704c6c01d8bfd4190911c13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:05:15 -1000 Subject: [PATCH 28/51] [ads1118] Combine log statements to reduce loop blocking (#12844) --- esphome/components/ads1118/sensor/ads1118_sensor.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/ads1118/sensor/ads1118_sensor.cpp b/esphome/components/ads1118/sensor/ads1118_sensor.cpp index c3ce3bdc9cb..7193c3c8801 100644 --- a/esphome/components/ads1118/sensor/ads1118_sensor.cpp +++ b/esphome/components/ads1118/sensor/ads1118_sensor.cpp @@ -9,8 +9,10 @@ static const char *const TAG = "ads1118.sensor"; void ADS1118Sensor::dump_config() { LOG_SENSOR(" ", "ADS1118 Sensor", this); - ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); - ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); + ESP_LOGCONFIG(TAG, + " Multiplexer: %u\n" + " Gain: %u", + this->multiplexer_, this->gain_); } float ADS1118Sensor::sample() { From 102862e99dceea007fbb14ba2dcccf24c224a491 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:05:29 -1000 Subject: [PATCH 29/51] [ads1115] Combine log statements to reduce loop blocking (#12843) --- esphome/components/ads1115/sensor/ads1115_sensor.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/ads1115/sensor/ads1115_sensor.cpp b/esphome/components/ads1115/sensor/ads1115_sensor.cpp index 6de95f1d12f..fac6b60d0ae 100644 --- a/esphome/components/ads1115/sensor/ads1115_sensor.cpp +++ b/esphome/components/ads1115/sensor/ads1115_sensor.cpp @@ -21,10 +21,12 @@ void ADS1115Sensor::update() { void ADS1115Sensor::dump_config() { LOG_SENSOR(" ", "ADS1115 Sensor", this); - ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); - ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); - ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_); - ESP_LOGCONFIG(TAG, " Sample rate: %u", this->samplerate_); + ESP_LOGCONFIG(TAG, + " Multiplexer: %u\n" + " Gain: %u\n" + " Resolution: %u\n" + " Sample rate: %u", + this->multiplexer_, this->gain_, this->resolution_, this->samplerate_); } } // namespace ads1115 From 723ccd7547f73eb0647f54e91631958b5fcb2ff0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:05:41 -1000 Subject: [PATCH 30/51] [ade7880] Combine log statements to reduce loop blocking (#12842) --- esphome/components/ade7880/ade7880.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp index fd560e06766..f6a15190cd4 100644 --- a/esphome/components/ade7880/ade7880.cpp +++ b/esphome/components/ade7880/ade7880.cpp @@ -162,11 +162,13 @@ void ADE7880::update() { } void ADE7880::dump_config() { - ESP_LOGCONFIG(TAG, "ADE7880:"); + ESP_LOGCONFIG(TAG, + "ADE7880:\n" + " Frequency: %.0f Hz", + this->frequency_); LOG_PIN(" IRQ0 Pin: ", this->irq0_pin_); LOG_PIN(" IRQ1 Pin: ", this->irq1_pin_); LOG_PIN(" RESET Pin: ", this->reset_pin_); - ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_); if (this->channel_a_ != nullptr) { ESP_LOGCONFIG(TAG, " Phase A:"); From ee65f2f0cd9bf93b63053fe7d335c4458f383be7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:24:41 -1000 Subject: [PATCH 31/51] [adc] Combine log statements to reduce loop blocking (#12841) --- esphome/components/adc/adc_sensor_esp32.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index 120cb1c9263..ea1263db5f7 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -121,23 +121,21 @@ void ADCSensor::setup() { void ADCSensor::dump_config() { LOG_SENSOR("", "ADC Sensor", this); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, - " Channel: %d\n" - " Unit: %s\n" - " Attenuation: %s\n" - " Samples: %i\n" - " Sampling mode: %s", - this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), - this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, - LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); - ESP_LOGCONFIG( TAG, + " Channel: %d\n" + " Unit: %s\n" + " Attenuation: %s\n" + " Samples: %i\n" + " Sampling mode: %s\n" " Setup Status:\n" " Handle Init: %s\n" " Config: %s\n" " Calibration: %s\n" " Overall Init: %s", + this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), + this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, + LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)), this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED", this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED"); From 8b80fe9c6bb73746b881e09baf3959853c9779f4 Mon Sep 17 00:00:00 2001 From: Frederic Meeuwissen <13856291+Frederic98@users.noreply.github.com> Date: Sun, 4 Jan 2026 02:32:27 +0100 Subject: [PATCH 32/51] [esp32_rmt_led_strip] Support inverted logic (#12825) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32_rmt_led_strip/led_strip.cpp | 2 +- esphome/components/esp32_rmt_led_strip/led_strip.h | 2 ++ esphome/components/esp32_rmt_led_strip/light.py | 8 ++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 2c7963b3664..4ca0b998b18 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -98,7 +98,7 @@ void ESP32RMTLEDStripLightOutput::setup() { channel.trans_queue_depth = 1; channel.flags.io_loop_back = 0; channel.flags.io_od_mode = 0; - channel.flags.invert_out = 0; + channel.flags.invert_out = this->invert_out_; channel.flags.with_dma = this->use_dma_; channel.intr_priority = 0; if (rmt_new_tx_channel(&channel, &this->channel_) != ESP_OK) { diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index 72ce659b4f1..6f3aea98784 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -49,6 +49,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { } void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->invert_out_ = inverted; } void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; } @@ -93,6 +94,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { bool is_wrgb_{false}; bool use_dma_{false}; bool use_psram_{false}; + bool invert_out_{false}; RGBOrder rgb_order_{ORDER_RGB}; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index f020d02e86c..3be3c758f18 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -8,9 +8,11 @@ from esphome.components.const import CONF_USE_PSRAM import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, + CONF_INVERTED, CONF_IS_RGBW, CONF_MAX_REFRESH_RATE, CONF_NUM_LEDS, + CONF_NUMBER, CONF_OUTPUT_ID, CONF_PIN, CONF_RGB_ORDER, @@ -71,7 +73,7 @@ CONFIG_SCHEMA = cv.All( light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), - cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), cv.SplitDefault( @@ -132,7 +134,9 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) - cg.add(var.set_pin(config[CONF_PIN])) + cg.add(var.set_pin(config[CONF_PIN][CONF_NUMBER])) + if config[CONF_PIN][CONF_INVERTED]: + cg.add(var.set_inverted(True)) if CONF_MAX_REFRESH_RATE in config: cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) From 997ab553c1b80148433f911e0cac50fefc3de07c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:36:08 -1000 Subject: [PATCH 33/51] [ac_dimmer] Combine log statements to reduce loop blocking (#12840) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/ac_dimmer/ac_dimmer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index e6f7a1214af..04c01948c88 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -211,13 +211,13 @@ void AcDimmer::write_state(float state) { this->store_.value = new_value; } void AcDimmer::dump_config() { - ESP_LOGCONFIG(TAG, "AcDimmer:"); - LOG_PIN(" Output Pin: ", this->gate_pin_); - LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_); ESP_LOGCONFIG(TAG, + "AcDimmer:\n" " Min Power: %.1f%%\n" " Init with half cycle: %s", this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_)); + LOG_PIN(" Output Pin: ", this->gate_pin_); + LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_); if (method_ == DIM_METHOD_LEADING_PULSE) { ESP_LOGCONFIG(TAG, " Method: leading pulse"); } else if (method_ == DIM_METHOD_LEADING) { From 7b74f94360603e18e230036b04544694369d5bb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:54:56 -1000 Subject: [PATCH 34/51] [wifi] Combine log statements to reduce loop blocking (#12856) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 28 +++++++++++++++------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0738a767770..ca7b1ba9cc1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -781,8 +781,10 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); #ifdef ESPHOME_LOG_HAS_VERBOSE - ESP_LOGV(TAG, "Connection Params:"); - ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); + ESP_LOGV(TAG, + "Connection Params:\n" + " SSID: '%s'", + ap.get_ssid().c_str()); if (ap.has_bssid()) { ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { @@ -791,20 +793,28 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { #ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { - ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:"); EAPAuth eap_config = ap.get_eap().value(); - ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); - ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); - ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); + // clang-format off + ESP_LOGV( + TAG, + " WPA2 Enterprise authentication configured:\n" + " Identity: " LOG_SECRET("'%s'") "\n" + " Username: " LOG_SECRET("'%s'") "\n" + " Password: " LOG_SECRET("'%s'"), + eap_config.identity.c_str(), eap_config.username.c_str(), eap_config.password.c_str()); + // clang-format on #if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2)); #endif bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert); bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert); bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key); - ESP_LOGV(TAG, " CA Cert: %s", ca_cert_present ? "present" : "not present"); - ESP_LOGV(TAG, " Client Cert: %s", client_cert_present ? "present" : "not present"); - ESP_LOGV(TAG, " Client Key: %s", client_key_present ? "present" : "not present"); + ESP_LOGV(TAG, + " CA Cert: %s\n" + " Client Cert: %s\n" + " Client Key: %s", + ca_cert_present ? "present" : "not present", client_cert_present ? "present" : "not present", + client_key_present ? "present" : "not present"); } else { #endif ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); From 6b4b1272db27b7292a6f2adf1cad1e269d6a6eeb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:56:52 -1000 Subject: [PATCH 35/51] [binary_sensor] Combine log statements to reduce loop blocking (#12849) --- esphome/components/binary_sensor/automation.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 66d8d6e90f3..dfe911a2f84 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -21,8 +21,10 @@ void MultiClickTrigger::on_state_(bool state) { // Start matching MultiClickTriggerEvent evt = this->timing_[0]; if (evt.state == state) { - ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); - ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); + ESP_LOGV(TAG, + "START min=%" PRIu32 " max=%" PRIu32 "\n" + "Multi Click: Starting multi click action!", + evt.min_length, evt.max_length); this->at_index_ = 1; if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); From 32562ca9916794c6450ccff4d7bb2af5182f7a74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 01:59:03 +0000 Subject: [PATCH 36/51] Bump aioesphomeapi from 43.10.0 to 43.10.1 (#12865) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bada581f56b..6631cb55bda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==43.10.0 +aioesphomeapi==43.10.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 5d384c77c545c4452e21f0737353eba34db585b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 16:00:50 -1000 Subject: [PATCH 37/51] [esp32] Move heap functions to flash, saving ~6KB (#12862) --- esphome/components/esp32/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index da550e58dcc..aa7d215c060 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -644,6 +644,7 @@ CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" CONF_FREERTOS_IN_IRAM = "freertos_in_iram" CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram" +CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" # VFS requirement tracking @@ -745,6 +746,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, cv.Optional(CONF_FREERTOS_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_RINGBUF_IN_IRAM, default=False): cv.boolean, + cv.Optional(CONF_HEAP_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 @@ -1090,6 +1092,12 @@ async def to_code(config): # Place in flash to save IRAM (default) add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True) + # Place heap functions into flash to save IRAM (~4-6KB savings) + # Safe as long as heap functions are not called from ISRs (which they shouldn't be) + # Users can set heap_in_iram: true as an escape hatch if needed + if not conf[CONF_ADVANCED][CONF_HEAP_IN_IRAM]: + add_idf_sdkconfig_option("CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH", True) + # Setup watchdog add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True) From 5f1eacf4ecf8fcc73775311508a92d4d7ff3296b Mon Sep 17 00:00:00 2001 From: Douwe <61123717+dhoeben@users.noreply.github.com> Date: Sun, 4 Jan 2026 03:43:31 +0100 Subject: [PATCH 38/51] [water_heater] (4/4) Implement tests for new water_heater component (#12517) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../water_heater/template_water_heater.cpp | 2 +- tests/components/water_heater/common.yaml | 16 +++ tests/components/web_server/common.yaml | 1 + .../fixtures/water_heater_template.yaml | 23 ++++ .../integration/test_water_heater_template.py | 109 ++++++++++++++++++ 5 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 tests/components/water_heater/common.yaml create mode 100644 tests/integration/fixtures/water_heater_template.yaml create mode 100644 tests/integration/test_water_heater_template.py diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index 18ef8d3f061..5ae5c30f365 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -21,7 +21,7 @@ void TemplateWaterHeater::setup() { } water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { - auto traits = water_heater::WaterHeater::get_traits(); + water_heater::WaterHeaterTraits traits; if (!this->supported_modes_.empty()) { traits.set_supported_modes(this->supported_modes_); diff --git a/tests/components/water_heater/common.yaml b/tests/components/water_heater/common.yaml new file mode 100644 index 00000000000..8ec2b1b2977 --- /dev/null +++ b/tests/components/water_heater/common.yaml @@ -0,0 +1,16 @@ +water_heater: + - platform: template + id: my_boiler + name: "Test Boiler" + min_temperature: 10 + max_temperature: 85 + target_temperature_step: 0.5 + current_temperature_step: 0.1 + optimistic: true + current_temperature: 45.0 + mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" + visual: + min_temperature: 10 + max_temperature: 85 + target_temperature_step: 0.5 + current_temperature_step: 0.1 diff --git a/tests/components/web_server/common.yaml b/tests/components/web_server/common.yaml index eb768eeb912..82307c189c7 100644 --- a/tests/components/web_server/common.yaml +++ b/tests/components/web_server/common.yaml @@ -36,3 +36,4 @@ datetime: optimistic: yes event: update: +water_heater: diff --git a/tests/integration/fixtures/water_heater_template.yaml b/tests/integration/fixtures/water_heater_template.yaml new file mode 100644 index 00000000000..b54ebed7891 --- /dev/null +++ b/tests/integration/fixtures/water_heater_template.yaml @@ -0,0 +1,23 @@ +esphome: + name: water-heater-template-test +host: +api: +logger: + +water_heater: + - platform: template + id: test_boiler + name: Test Boiler + optimistic: true + current_temperature: !lambda "return 45.0f;" + # Note: No mode lambda - we want optimistic mode changes to stick + # A mode lambda would override mode changes in loop() + supported_modes: + - "off" + - eco + - gas + - performance + visual: + min_temperature: 30.0 + max_temperature: 85.0 + target_temperature_step: 0.5 diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py new file mode 100644 index 00000000000..b5f1fb64c07 --- /dev/null +++ b/tests/integration/test_water_heater_template.py @@ -0,0 +1,109 @@ +"""Integration test for template water heater component.""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import WaterHeaterInfo, WaterHeaterMode, WaterHeaterState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_water_heater_template( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test template water heater basic state and mode changes.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + states: dict[int, aioesphomeapi.EntityState] = {} + gas_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future() + eco_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future() + + def on_state(state: aioesphomeapi.EntityState) -> None: + states[state.key] = state + if isinstance(state, WaterHeaterState): + # Wait for GAS mode + if state.mode == WaterHeaterMode.GAS and not gas_mode_future.done(): + gas_mode_future.set_result(state) + # Wait for ECO mode (we start at OFF, so test transitioning to ECO) + elif state.mode == WaterHeaterMode.ECO and not eco_mode_future.done(): + eco_mode_future.set_result(state) + + # Get entities and set up state synchronization + entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + water_heater_infos = [e for e in entities if isinstance(e, WaterHeaterInfo)] + assert len(water_heater_infos) == 1, ( + f"Expected exactly 1 water heater entity, got {len(water_heater_infos)}. Entity types: {[type(e).__name__ for e in entities]}" + ) + + test_water_heater = water_heater_infos[0] + + # Verify water heater entity info + assert test_water_heater.object_id == "test_boiler" + assert test_water_heater.name == "Test Boiler" + assert test_water_heater.min_temperature == 30.0 + assert test_water_heater.max_temperature == 85.0 + assert test_water_heater.target_temperature_step == 0.5 + + # Verify supported modes + supported_modes = test_water_heater.supported_modes + assert WaterHeaterMode.OFF in supported_modes, "Expected OFF in supported modes" + assert WaterHeaterMode.ECO in supported_modes, "Expected ECO in supported modes" + assert WaterHeaterMode.GAS in supported_modes, "Expected GAS in supported modes" + assert WaterHeaterMode.PERFORMANCE in supported_modes, ( + "Expected PERFORMANCE in supported modes" + ) + assert len(supported_modes) == 4, ( + f"Expected 4 supported modes, got {len(supported_modes)}: {supported_modes}" + ) + + # Subscribe with the wrapper that filters initial states + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Get initial state and verify + initial_state = initial_state_helper.initial_states.get(test_water_heater.key) + assert initial_state is not None, "Water heater initial state not found" + assert isinstance(initial_state, WaterHeaterState) + # Initial mode is OFF (default) since we don't have a mode lambda + # A mode lambda would override optimistic mode changes + assert initial_state.mode == WaterHeaterMode.OFF, ( + f"Expected initial mode OFF, got {initial_state.mode}" + ) + assert initial_state.current_temperature == 45.0, ( + f"Expected current temp 45.0, got {initial_state.current_temperature}" + ) + + # Test changing to GAS mode + client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS) + + try: + gas_state = await asyncio.wait_for(gas_mode_future, timeout=5.0) + except TimeoutError: + pytest.fail("GAS mode change not received within 5 seconds") + + assert isinstance(gas_state, WaterHeaterState) + assert gas_state.mode == WaterHeaterMode.GAS + + # Test changing to ECO mode (from GAS) + client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.ECO) + + try: + eco_state = await asyncio.wait_for(eco_mode_future, timeout=5.0) + except TimeoutError: + pytest.fail("ECO mode change not received within 5 seconds") + + assert isinstance(eco_state, WaterHeaterState) + assert eco_state.mode == WaterHeaterMode.ECO From 12c6f5749e22bbaf4c8bfba02d452b11f1b4219b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 16:46:29 -1000 Subject: [PATCH 39/51] [cst816] Combine log statements to reduce loop blocking (#12872) --- .../components/cst816/touchscreen/cst816_touchscreen.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 5be93692c0b..d18d4e7c94c 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -83,14 +83,14 @@ void CST816Touchscreen::update_touches() { } void CST816Touchscreen::dump_config() { - ESP_LOGCONFIG(TAG, "CST816 Touchscreen:"); - LOG_I2C_DEVICE(this); - LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); - LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, + "CST816 Touchscreen:\n" " X Raw Min: %d, X Raw Max: %d\n" " Y Raw Min: %d, Y Raw Max: %d", this->x_raw_min_, this->x_raw_max_, this->y_raw_min_, this->y_raw_max_); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); const char *name; switch (this->chip_id_) { case CST716_CHIP_ID: From c96d0015a003f0a918bd648059b32e6980709cdf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 16:48:04 -1000 Subject: [PATCH 40/51] [esp_ldo] Combine log statements to reduce loop blocking (#12886) --- esphome/components/esp_ldo/esp_ldo.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index 5e3d4159f39..2eee855b469 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -21,9 +21,11 @@ void EspLdo::setup() { } } void EspLdo::dump_config() { - ESP_LOGCONFIG(TAG, "ESP LDO Channel %d:", this->channel_); - ESP_LOGCONFIG(TAG, " Voltage: %fV", this->voltage_); - ESP_LOGCONFIG(TAG, " Adjustable: %s", YESNO(this->adjustable_)); + ESP_LOGCONFIG(TAG, + "ESP LDO Channel %d:\n" + " Voltage: %fV\n" + " Adjustable: %s", + this->channel_, this->voltage_, YESNO(this->adjustable_)); } void EspLdo::adjust_voltage(float voltage) { From 16ada4d477146f2ff33caa445f295c4817d1825b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 16:48:39 -1000 Subject: [PATCH 41/51] [epaper_spi] Combine log statements to reduce loop blocking (#12881) --- esphome/components/epaper_spi/epaper_spi.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index 4e6b4a7fd6e..0b600feeae1 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -331,20 +331,21 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) { void EPaperBase::dump_config() { LOG_DISPLAY("", "E-Paper SPI", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->name_); - LOG_PIN(" Reset Pin: ", this->reset_pin_); - LOG_PIN(" DC Pin: ", this->dc_pin_); - LOG_PIN(" Busy Pin: ", this->busy_pin_); - LOG_PIN(" CS Pin: ", this->cs_); - LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, + " Model: %s\n" " SPI Data Rate: %uMHz\n" " Full update every: %d\n" " Swap X/Y: %s\n" " Mirror X: %s\n" " Mirror Y: %s", - (unsigned) (this->data_rate_ / 1000000), this->full_update_every_, YESNO(this->transform_ & SWAP_XY), - YESNO(this->transform_ & MIRROR_X), YESNO(this->transform_ & MIRROR_Y)); + this->name_, (unsigned) (this->data_rate_ / 1000000), this->full_update_every_, + YESNO(this->transform_ & SWAP_XY), YESNO(this->transform_ & MIRROR_X), + YESNO(this->transform_ & MIRROR_Y)); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_UPDATE_INTERVAL(this); } } // namespace esphome::epaper_spi From cf93b66306a7e7f941a8a204709856be9eebb4fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 16:59:55 -1000 Subject: [PATCH 42/51] [chsc6x] Combine log statements to reduce loop blocking (#12871) --- esphome/components/chsc6x/chsc6x_touchscreen.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/chsc6x/chsc6x_touchscreen.cpp b/esphome/components/chsc6x/chsc6x_touchscreen.cpp index 31c9466691f..941144e451d 100644 --- a/esphome/components/chsc6x/chsc6x_touchscreen.cpp +++ b/esphome/components/chsc6x/chsc6x_touchscreen.cpp @@ -32,14 +32,14 @@ void CHSC6XTouchscreen::update_touches() { } void CHSC6XTouchscreen::dump_config() { - ESP_LOGCONFIG(TAG, "CHSC6X Touchscreen:"); - LOG_I2C_DEVICE(this); - LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); ESP_LOGCONFIG(TAG, + "CHSC6X Touchscreen:\n" " Touch timeout: %d\n" " x_raw_max_: %d\n" " y_raw_max_: %d", this->touch_timeout_, this->x_raw_max_, this->y_raw_max_); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } } // namespace chsc6x From bc9093127e5fff67c901b29691c4c4dae000bc2f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 17:00:14 -1000 Subject: [PATCH 43/51] [cap1188] Combine log statements to reduce loop blocking (#12868) --- esphome/components/cap1188/cap1188.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index 683e5cf4871..9e8c87d1472 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -63,14 +63,14 @@ void CAP1188Component::finish_setup_() { } void CAP1188Component::dump_config() { - ESP_LOGCONFIG(TAG, "CAP1188:"); - LOG_I2C_DEVICE(this); - LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, + "CAP1188:\n" " Product ID: 0x%x\n" " Manufacture ID: 0x%x\n" " Revision ID: 0x%x", this->cap1188_product_id_, this->cap1188_manufacture_id_, this->cap1188_revision_); + LOG_I2C_DEVICE(this); + LOG_PIN(" Reset Pin: ", this->reset_pin_); switch (this->error_code_) { case COMMUNICATION_FAILED: From 44fa6bae95bf3a60962c1752c187532251ebe5c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 17:57:53 -1000 Subject: [PATCH 44/51] [dht] Combine log statements to reduce loop blocking (#12877) --- esphome/components/dht/dht.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index e0abb7c5f09..6cb204c8de3 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -17,11 +17,14 @@ void DHT::setup() { } void DHT::dump_config() { - ESP_LOGCONFIG(TAG, "DHT:"); + ESP_LOGCONFIG(TAG, + "DHT:\n" + " %sModel: %s\n" + " Internal pull-up: %s", + this->is_auto_detect_ ? "Auto-detected " : "", + this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent", + ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP)); LOG_PIN(" Pin: ", this->t_pin_); - ESP_LOGCONFIG(TAG, " %sModel: %s", this->is_auto_detect_ ? "Auto-detected " : "", - this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent"); - ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP)); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); From 9f06f046d67f64041eb472bb017e0b03d223b341 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 17:59:53 -1000 Subject: [PATCH 45/51] [espnow] Combine log statements to reduce loop blocking (#12887) --- .../espnow/packet_transport/espnow_transport.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index c1252acc9dc..3d16f28c7de 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -21,9 +21,11 @@ void ESPNowTransport::setup() { return; } - ESP_LOGI(TAG, "Registering ESP-NOW handlers"); - ESP_LOGI(TAG, "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], this->peer_address_[1], - this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]); + ESP_LOGI(TAG, + "Registering ESP-NOW handlers\n" + "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", + this->peer_address_[0], this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], + this->peer_address_[4], this->peer_address_[5]); // Register received handler this->parent_->register_received_handler(this); From 6e8817cbc47f1dcf116eb2bdd06fbbfdb8b8f892 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 18:00:11 -1000 Subject: [PATCH 46/51] [esp8266_pwm] Combine log statements to reduce loop blocking (#12885) --- esphome/components/esp8266_pwm/esp8266_pwm.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.cpp b/esphome/components/esp8266_pwm/esp8266_pwm.cpp index 0aaef597d37..cc6bfbc8a88 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.cpp +++ b/esphome/components/esp8266_pwm/esp8266_pwm.cpp @@ -18,9 +18,11 @@ void ESP8266PWM::setup() { this->turn_off(); } void ESP8266PWM::dump_config() { - ESP_LOGCONFIG(TAG, "ESP8266 PWM:"); + ESP_LOGCONFIG(TAG, + "ESP8266 PWM:\n" + " Frequency: %.1f Hz", + this->frequency_); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); LOG_FLOAT_OUTPUT(this); } void HOT ESP8266PWM::write_state(float state) { From cb598c43e891c6887efd1ede490e621041a72a0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 18:00:31 -1000 Subject: [PATCH 47/51] [endstop] Combine log statements to reduce loop blocking (#12879) --- esphome/components/endstop/endstop_cover.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 381f098eb5e..2c281ea2e61 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -104,10 +104,12 @@ void EndstopCover::loop() { } void EndstopCover::dump_config() { LOG_COVER("", "Endstop Cover", this); + ESP_LOGCONFIG(TAG, + " Open Duration: %.1fs\n" + " Close Duration: %.1fs", + this->open_duration_ / 1e3f, this->close_duration_ / 1e3f); LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_); - ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f); LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_); - ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); } float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } void EndstopCover::stop_prev_trigger_() { From e94158a12f303543bea4e901601373a60d471af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 18:00:52 -1000 Subject: [PATCH 48/51] [fan] Combine log statements to reduce loop blocking (#12889) --- esphome/components/fan/fan.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index bf5506da4b4..0ffb60e50da 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -179,8 +179,10 @@ void Fan::add_on_state_callback(std::function &&callback) { this->state_ void Fan::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); - ESP_LOGD(TAG, " State: %s", ONOFF(this->state)); + ESP_LOGD(TAG, + "'%s' - Sending state:\n" + " State: %s", + this->name_.c_str(), ONOFF(this->state)); if (traits.supports_speed()) { ESP_LOGD(TAG, " Speed: %d", this->speed); } From c59314ec09c5e311ca313d9c4baa37c29ccf2e7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 18:01:28 -1000 Subject: [PATCH 49/51] [debug] Combine log statements to reduce loop blocking (#12875) --- esphome/components/debug/debug_esp8266.cpp | 19 +++++---- esphome/components/debug/debug_libretiny.cpp | 15 ++++--- esphome/components/debug/debug_zephyr.cpp | 43 +++++++++++--------- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 3395d9db121..7427b32290f 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -47,14 +47,17 @@ void DebugComponent::get_device_info_(std::string &device_info) { #if !defined(CLANG_TIDY) auto reset_reason = get_reset_reason_(); - ESP_LOGD(TAG, "Chip ID: 0x%08X", ESP.getChipId()); - ESP_LOGD(TAG, "SDK Version: %s", ESP.getSdkVersion()); - ESP_LOGD(TAG, "Core Version: %s", ESP.getCoreVersion().c_str()); - ESP_LOGD(TAG, "Boot Version=%u Mode=%u", ESP.getBootVersion(), ESP.getBootMode()); - ESP_LOGD(TAG, "CPU Frequency: %u", ESP.getCpuFreqMHz()); - ESP_LOGD(TAG, "Flash Chip ID=0x%08X", ESP.getFlashChipId()); - ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); - ESP_LOGD(TAG, "Reset Info: %s", ESP.getResetInfo().c_str()); + ESP_LOGD(TAG, + "Chip ID: 0x%08X\n" + "SDK Version: %s\n" + "Core Version: %s\n" + "Boot Version=%u Mode=%u\n" + "CPU Frequency: %u\n" + "Flash Chip ID=0x%08X\n" + "Reset Reason: %s\n" + "Reset Info: %s", + ESP.getChipId(), ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), ESP.getBootVersion(), ESP.getBootMode(), + ESP.getCpuFreqMHz(), ESP.getFlashChipId(), reset_reason.c_str(), ESP.getResetInfo().c_str()); device_info += "|Chip: 0x" + format_hex(ESP.getChipId()); device_info += "|SDK: "; diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index b5e2a5b3103..e823ac6c77e 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -13,12 +13,15 @@ uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); } void DebugComponent::get_device_info_(std::string &device_info) { std::string reset_reason = get_reset_reason_(); - ESP_LOGD(TAG, "LibreTiny Version: %s", lt_get_version()); - ESP_LOGD(TAG, "Chip: %s (%04x) @ %u MHz", lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz()); - ESP_LOGD(TAG, "Chip ID: 0x%06X", lt_cpu_get_mac_id()); - ESP_LOGD(TAG, "Board: %s", lt_get_board_code()); - ESP_LOGD(TAG, "Flash: %u KiB / RAM: %u KiB", lt_flash_get_size() / 1024, lt_ram_get_size() / 1024); - ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); + ESP_LOGD(TAG, + "LibreTiny Version: %s\n" + "Chip: %s (%04x) @ %u MHz\n" + "Chip ID: 0x%06X\n" + "Board: %s\n" + "Flash: %u KiB / RAM: %u KiB\n" + "Reset Reason: %s", + lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), lt_cpu_get_mac_id(), + lt_get_board_code(), lt_flash_get_size() / 1024, lt_ram_get_size() / 1024, reset_reason.c_str()); device_info += "|Version: "; device_info += LT_BANNER_STR + 10; diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index c888c41a782..6abf983e9eb 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -106,13 +106,13 @@ static void fa_cb(const struct flash_area *fa, void *user_data) { void DebugComponent::log_partition_info_() { #if CONFIG_FLASH_MAP_LABELS ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Label | Offset | Size"); - ESP_LOGCONFIG(TAG, "--------------------------------------------" + "| Label | Offset | Size\n" + "--------------------------------------------" "-----------------------------------------------"); #else ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Offset | Size"); - ESP_LOGCONFIG(TAG, "-----------------------------------------" + "| Offset | Size\n" + "-----------------------------------------" "------------------------------"); #endif flash_area_foreach(fa_cb, nullptr); @@ -300,18 +300,18 @@ void DebugComponent::get_device_info_(std::string &device_info) { return "Unspecified"; }; - ESP_LOGD(TAG, "Code page size: %u, code size: %u, device id: 0x%08x%08x", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, - NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0]); - ESP_LOGD(TAG, "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x", NRF_FICR->ER[0], + ESP_LOGD(TAG, + "Code page size: %u, code size: %u, device id: 0x%08x%08x\n" + "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" + "Device address type: %s, address: %s\n" + "Part code: nRF%x, version: %c%c%c%c, package: %s\n" + "RAM: %ukB, Flash: %ukB, production test: %sdone", + NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0], NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2], - NRF_FICR->IR[3]); - ESP_LOGD(TAG, "Device address type: %s, address: %s", (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), - get_mac_address_pretty().c_str()); - ESP_LOGD(TAG, "Part code: nRF%x, version: %c%c%c%c, package: %s", NRF_FICR->INFO.PART, - NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, NRF_FICR->INFO.VARIANT >> 8 & 0xFF, - NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); - ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, - (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); + NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), get_mac_address_pretty().c_str(), + NRF_FICR->INFO.PART, NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, + NRF_FICR->INFO.VARIANT >> 8 & 0xFF, NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE), + NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] && (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos; @@ -329,9 +329,10 @@ void DebugComponent::get_device_info_(std::string &device_info) { #else ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, (BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF); - ESP_LOGD(TAG, "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR), - NRF_UICR->NRFFW[0]); - ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), + ESP_LOGD(TAG, + "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x\n" + "MBR param page addr 0x%08x, UICR param page addr 0x%08x", + read_mem_u32(MBR_BOOTLOADER_ADDR), NRF_UICR->NRFFW[0], read_mem_u32(MBR_PARAM_PAGE_ADDR), NRF_UICR->NRFFW[1]); if (is_sd_present()) { uint32_t const sd_id = sd_id_get(); @@ -368,8 +369,10 @@ void DebugComponent::get_device_info_(std::string &device_info) { } return res; }; - ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); - ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); + ESP_LOGD(TAG, + "NRFFW %s\n" + "NRFHW %s", + uicr(NRF_UICR->NRFFW, 13).c_str(), uicr(NRF_UICR->NRFHW, 12).c_str()); } void DebugComponent::update_platform_() {} From 096de869b6c67cb72126da67ca4d4a6699bc78e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 18:01:55 -1000 Subject: [PATCH 50/51] [esp32_ble_client] Combine log statements to reduce loop blocking (#12883) --- .../components/esp32_ble_client/ble_client_base.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 8017b577f42..26eb5dd0925 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -70,9 +70,9 @@ float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_B void BLEClientBase::dump_config() { ESP_LOGCONFIG(TAG, " Address: %s\n" - " Auto-Connect: %s", - this->address_str(), TRUEFALSE(this->auto_connect_)); - ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state())); + " Auto-Connect: %s\n" + " State: %s", + this->address_str(), TRUEFALSE(this->auto_connect_), espbt::client_state_to_string(this->state())); if (this->status_ == ESP_GATT_NO_RESOURCES) { ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config."); } else if (this->status_ != ESP_GATT_OK) { @@ -415,8 +415,10 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ for (auto &svc : this->services_) { char uuid_buf[espbt::UUID_STR_LEN]; svc->uuid.to_str(uuid_buf); - ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf); - ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, + ESP_LOGV(TAG, + "[%d] [%s] Service UUID: %s\n" + "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", + this->connection_index_, this->address_str_, uuid_buf, this->connection_index_, this->address_str_, svc->start_handle, svc->end_handle); } #endif From facf4777a400327b1120e1898425d06a025b9c11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 18:04:00 -1000 Subject: [PATCH 51/51] [ezo_pmp] Combine log statements to reduce loop blocking (#12888) --- esphome/components/ezo_pmp/ezo_pmp.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index 9ec41cce30d..61b601328a7 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -148,10 +148,13 @@ void EzoPMP::read_command_result_() { char current_char = response_buffer[i]; if (current_char == '\0') { - ESP_LOGV(TAG, "Read Response from device: %s", (char *) response_buffer); - ESP_LOGV(TAG, "First Component: %s", (char *) first_parameter_buffer); - ESP_LOGV(TAG, "Second Component: %s", (char *) second_parameter_buffer); - ESP_LOGV(TAG, "Third Component: %s", (char *) third_parameter_buffer); + ESP_LOGV(TAG, + "Read Response from device: %s\n" + "First Component: %s\n" + "Second Component: %s\n" + "Third Component: %s", + (char *) response_buffer, (char *) first_parameter_buffer, (char *) second_parameter_buffer, + (char *) third_parameter_buffer); break; }