From b91b12d77a28236644970051d576c0c4b284b564 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 23 Oct 2025 02:55:34 +0200 Subject: [PATCH 01/12] [nrf52] support BLE --device for logging (#9861) Co-authored-by: J. Nick Koston --- esphome/components/nrf52/__init__.py | 17 ++++++++ esphome/components/nrf52/ble_logger.py | 60 ++++++++++++++++++++++++++ esphome/components/zephyr/__init__.py | 3 ++ requirements.txt | 1 + 4 files changed, 81 insertions(+) create mode 100644 esphome/components/nrf52/ble_logger.py diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 727607933d..27e1246744 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import logging from pathlib import Path @@ -277,3 +278,19 @@ def upload_program(config: ConfigType, args, host: str) -> bool: raise EsphomeError(f"Upload failed with result: {result}") return handled + + +def show_logs(config: ConfigType, args, devices: list[str]) -> bool: + address = devices[0] + from .ble_logger import is_mac_address, logger_connect, logger_scan + + if devices[0] == "BLE": + ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"])) + if ble_device: + address = ble_device.address + else: + return True + if is_mac_address(address): + asyncio.run(logger_connect(address)) + return True + return False diff --git a/esphome/components/nrf52/ble_logger.py b/esphome/components/nrf52/ble_logger.py new file mode 100644 index 0000000000..f74a49ea89 --- /dev/null +++ b/esphome/components/nrf52/ble_logger.py @@ -0,0 +1,60 @@ +import asyncio +import logging +import re +from typing import Final + +from bleak import BleakClient, BleakScanner, BLEDevice +from bleak.exc import ( + BleakCharacteristicNotFoundError, + BleakDBusError, + BleakDeviceNotFoundError, +) + +_LOGGER = logging.getLogger(__name__) + + +NUS_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +MAC_ADDRESS_PATTERN: Final = re.compile( + r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE +) + + +def is_mac_address(value: str) -> bool: + return MAC_ADDRESS_PATTERN.match(value) + + +async def logger_scan(name: str) -> BLEDevice | None: + _LOGGER.info("Scanning bluetooth for %s...", name) + device = await BleakScanner.find_device_by_name(name) + if not device: + _LOGGER.error("%s Bluetooth LE device was not found!", name) + return device + + +async def logger_connect(host: str) -> int | None: + disconnected_event = asyncio.Event() + + def handle_disconnect(client): + disconnected_event.set() + + def handle_rx(_, data: bytearray): + print(data.decode("utf-8"), end="") + + _LOGGER.info("Connecting %s...", host) + try: + async with BleakClient(host, disconnected_callback=handle_disconnect) as client: + _LOGGER.info("Connected %s...", host) + try: + await client.start_notify(NUS_TX_CHAR_UUID, handle_rx) + except BleakDBusError as e: + _LOGGER.error("Bluetooth LE logger: %s", e) + disconnected_event.set() + await disconnected_event.wait() + except BleakDeviceNotFoundError: + _LOGGER.error("Device %s not found", host) + return 1 + except BleakCharacteristicNotFoundError: + _LOGGER.error("Device %s has no NUS characteristic", host) + return 1 diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 634c99876b..a2fb12a5e2 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -234,6 +234,9 @@ def copy_files(): "url": "https://esphome.io/", "vendor": "esphome", "build": { + "bsp": { + "name": "adafruit" + }, "softdevice": { "sd_fwid": "0x00B6" } diff --git a/requirements.txt b/requirements.txt index ec7794c75a..6966ebe583 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ pillow==11.3.0 cairosvg==2.8.2 freetype-py==2.5.1 jinja2==3.1.6 +bleak==1.0.1 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From f2f6c597ef3768a37507acb5c4831ad7e858cf6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 15:17:57 -1000 Subject: [PATCH 02/12] [light] Store effect names in flash (const char*) to save RAM (#11487) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../adalight/adalight_light_effect.cpp | 2 +- .../adalight/adalight_light_effect.h | 2 +- esphome/components/e131/e131.cpp | 8 ++++---- .../e131/e131_addressable_light_effect.cpp | 6 +++--- .../e131/e131_addressable_light_effect.h | 2 +- .../light/addressable_light_effect.h | 19 +++++++++---------- esphome/components/light/base_light_effects.h | 12 ++++++------ esphome/components/light/light_call.cpp | 4 ++-- esphome/components/light/light_effect.h | 12 +++++++----- esphome/components/light/light_state.h | 2 +- esphome/components/wled/wled_light_effect.cpp | 2 +- esphome/components/wled/wled_light_effect.h | 2 +- 12 files changed, 37 insertions(+), 36 deletions(-) diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index 35e98d7360..4cf639a01f 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect"; static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; -AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} +AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {} void AdalightLightEffect::start() { AddressableLightEffect::start(); diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h index 72faf44269..bb7319c99c 100644 --- a/esphome/components/adalight/adalight_light_effect.h +++ b/esphome/components/adalight/adalight_light_effect.h @@ -11,7 +11,7 @@ namespace adalight { class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { public: - AdalightLightEffect(const std::string &name); + AdalightLightEffect(const char *name); void start() override; void stop() override; diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index a74fc9be4a..d18d945cec 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -80,8 +80,8 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { return; } - ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), - light_effect->get_first_universe(), light_effect->get_last_universe()); + ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), + light_effect->get_last_universe()); light_effects_.insert(light_effect); @@ -95,8 +95,8 @@ void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { return; } - ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), - light_effect->get_first_universe(), light_effect->get_last_universe()); + ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), + light_effect->get_last_universe()); light_effects_.erase(light_effect); diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 4d1f98ab6c..780e181f04 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -9,7 +9,7 @@ namespace e131 { static const char *const TAG = "e131_addressable_light_effect"; static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); -E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} +E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {} int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } @@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); auto *input_data = packet.values + 1; - ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe, - output_offset, output_end); + ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset, + output_end); switch (channels_) { case E131_MONO: diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index 17d7bd2829..381e08163b 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; class E131AddressableLightEffect : public light::AddressableLightEffect { public: - E131AddressableLightEffect(const std::string &name); + E131AddressableLightEffect(const char *name); void start() override; void stop() override; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 9caccad634..9840112040 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -30,7 +30,7 @@ inline static uint8_t half_sin8(uint8_t v) { return sin16_c(uint16_t(v) * 128u) class AddressableLightEffect : public LightEffect { public: - explicit AddressableLightEffect(const std::string &name) : LightEffect(name) {} + explicit AddressableLightEffect(const char *name) : LightEffect(name) {} void start_internal() override { this->get_addressable_()->set_effect_active(true); this->get_addressable_()->clear_effect_data(); @@ -57,8 +57,7 @@ class AddressableLightEffect : public LightEffect { class AddressableLambdaLightEffect : public AddressableLightEffect { public: - AddressableLambdaLightEffect(const std::string &name, - std::function f, + AddressableLambdaLightEffect(const char *name, std::function f, uint32_t update_interval) : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } @@ -81,7 +80,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { class AddressableRainbowLightEffect : public AddressableLightEffect { public: - explicit AddressableRainbowLightEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableRainbowLightEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const Color ¤t_color) override { ESPHSVColor hsv; hsv.value = 255; @@ -112,7 +111,7 @@ struct AddressableColorWipeEffectColor { class AddressableColorWipeEffect : public AddressableLightEffect { public: - explicit AddressableColorWipeEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {} void set_colors(const std::initializer_list &colors) { this->colors_ = colors; } void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; } void set_reverse(bool reverse) { this->reverse_ = reverse; } @@ -165,7 +164,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { class AddressableScanEffect : public AddressableLightEffect { public: - explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableScanEffect(const char *name) : AddressableLightEffect(name) {} void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } void apply(AddressableLight &it, const Color ¤t_color) override { @@ -202,7 +201,7 @@ class AddressableScanEffect : public AddressableLightEffect { class AddressableTwinkleEffect : public AddressableLightEffect { public: - explicit AddressableTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableTwinkleEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &addressable, const Color ¤t_color) override { const uint32_t now = millis(); uint8_t pos_add = 0; @@ -244,7 +243,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { class AddressableRandomTwinkleEffect : public AddressableLightEffect { public: - explicit AddressableRandomTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableRandomTwinkleEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); uint8_t pos_add = 0; @@ -293,7 +292,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { class AddressableFireworksEffect : public AddressableLightEffect { public: - explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableFireworksEffect(const char *name) : AddressableLightEffect(name) {} void start() override { auto &it = *this->get_addressable_(); it.all() = Color::BLACK; @@ -342,7 +341,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { class AddressableFlickerEffect : public AddressableLightEffect { public: - explicit AddressableFlickerEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableFlickerEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); const uint8_t intensity = this->intensity_; diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index c74d19fe14..327c243525 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -17,7 +17,7 @@ inline static float random_cubic_float() { /// Pulse effect. class PulseLightEffect : public LightEffect { public: - explicit PulseLightEffect(const std::string &name) : LightEffect(name) {} + explicit PulseLightEffect(const char *name) : LightEffect(name) {} void apply() override { const uint32_t now = millis(); @@ -60,7 +60,7 @@ class PulseLightEffect : public LightEffect { /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. class RandomLightEffect : public LightEffect { public: - explicit RandomLightEffect(const std::string &name) : LightEffect(name) {} + explicit RandomLightEffect(const char *name) : LightEffect(name) {} void apply() override { const uint32_t now = millis(); @@ -112,7 +112,7 @@ class RandomLightEffect : public LightEffect { class LambdaLightEffect : public LightEffect { public: - LambdaLightEffect(const std::string &name, std::function f, uint32_t update_interval) + LambdaLightEffect(const char *name, std::function f, uint32_t update_interval) : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } @@ -138,7 +138,7 @@ class LambdaLightEffect : public LightEffect { class AutomationLightEffect : public LightEffect { public: - AutomationLightEffect(const std::string &name) : LightEffect(name) {} + AutomationLightEffect(const char *name) : LightEffect(name) {} void stop() override { this->trig_->stop_action(); } void apply() override { if (!this->trig_->is_action_running()) { @@ -163,7 +163,7 @@ struct StrobeLightEffectColor { class StrobeLightEffect : public LightEffect { public: - explicit StrobeLightEffect(const std::string &name) : LightEffect(name) {} + explicit StrobeLightEffect(const char *name) : LightEffect(name) {} void apply() override { const uint32_t now = millis(); if (now - this->last_switch_ < this->colors_[this->at_color_].duration) @@ -198,7 +198,7 @@ class StrobeLightEffect : public LightEffect { class FlickerLightEffect : public LightEffect { public: - explicit FlickerLightEffect(const std::string &name) : LightEffect(name) {} + explicit FlickerLightEffect(const char *name) : LightEffect(name) {} void apply() override { LightColorValues remote = this->state_->remote_values; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index af193e1f11..f611baba71 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -156,7 +156,7 @@ void LightCall::perform() { if (this->effect_ == 0u) { effect_s = "None"; } else { - effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str(); + effect_s = this->parent_->effects_[this->effect_ - 1]->get_name(); } if (publish) { @@ -511,7 +511,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { LightEffect *e = this->parent_->effects_[i]; - if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) { + if (strcasecmp(effect.c_str(), e->get_name()) == 0) { this->set_effect(i + 1); found = true; break; diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index dbaf1faf24..d4c2dc3582 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/component.h" namespace esphome { @@ -11,7 +9,7 @@ class LightState; class LightEffect { public: - explicit LightEffect(std::string name) : name_(std::move(name)) {} + explicit LightEffect(const char *name) : name_(name) {} /// Initialize this LightEffect. Will be called once after creation. virtual void start() {} @@ -24,7 +22,11 @@ class LightEffect { /// Apply this effect. Use the provided state for starting transitions, ... virtual void apply() = 0; - const std::string &get_name() { return this->name_; } + /** + * Returns the name of this effect. + * The returned pointer is valid for the lifetime of the program and must not be freed. + */ + const char *get_name() const { return this->name_; } /// Internal method called by the LightState when this light effect is registered in it. virtual void init() {} @@ -47,7 +49,7 @@ class LightEffect { protected: LightState *state_{nullptr}; - std::string name_; + const char *name_; /// Internal method to find this effect's index in the parent light's effect list. uint32_t get_index_in_parent_() const; diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index a07aeb6ae5..502a08c635 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -177,7 +177,7 @@ class LightState : public EntityBase, public Component { return 0; } for (size_t i = 0; i < this->effects_.size(); i++) { - if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name().c_str()) == 0) { + if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name()) == 0) { return i + 1; // Effects are 1-indexed in active_effect_index_ } } diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 25577ccc11..d26b7a1750 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -28,7 +28,7 @@ const int DEFAULT_BLANK_TIME = 1000; static const char *const TAG = "wled_light_effect"; -WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {} +WLEDLightEffect::WLEDLightEffect(const char *name) : AddressableLightEffect(name) {} void WLEDLightEffect::start() { AddressableLightEffect::start(); diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index a591e1fd1a..6da5f4e9f9 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -15,7 +15,7 @@ namespace wled { class WLEDLightEffect : public light::AddressableLightEffect { public: - WLEDLightEffect(const std::string &name); + WLEDLightEffect(const char *name); void start() override; void stop() override; From 6efe346cc5bf18c29ff2a08042f51a6255dee9f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 15:21:53 -1000 Subject: [PATCH 03/12] [light] Use std::initializer_list for add_effects to reduce flash overhead (#11485) --- esphome/components/light/light_state.cpp | 7 ++----- esphome/components/light/light_state.h | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 979dc2f5a1..7b0a698bb8 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -178,12 +178,9 @@ void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } bool LightState::supports_effects() { return !this->effects_.empty(); } const FixedVector &LightState::get_effects() const { return this->effects_; } -void LightState::add_effects(const std::vector &effects) { +void LightState::add_effects(const std::initializer_list &effects) { // Called once from Python codegen during setup with all effects from YAML config - this->effects_.init(effects.size()); - for (auto *effect : effects) { - this->effects_.push_back(effect); - } + this->effects_ = effects; } void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 502a08c635..bf63c0ec27 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -163,7 +163,7 @@ class LightState : public EntityBase, public Component { const FixedVector &get_effects() const; /// Add effects for this light state. - void add_effects(const std::vector &effects); + void add_effects(const std::initializer_list &effects); /// Get the total number of effects available for this light. size_t get_effect_count() const { return this->effects_.size(); } From 2864e989bdbf9948cf3822575346556624daff56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 15:22:46 -1000 Subject: [PATCH 04/12] [light] Extract ColorModeMask into generic FiniteSetMask helper (#11472) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/light/color_mode.h | 215 +++++++----------------- esphome/components/light/light_call.cpp | 2 +- esphome/components/light/light_traits.h | 4 +- esphome/core/finite_set_mask.h | 171 +++++++++++++++++++ 4 files changed, 237 insertions(+), 155 deletions(-) create mode 100644 esphome/core/finite_set_mask.h diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index a26f917167..aa3448c145 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -1,6 +1,7 @@ #pragma once #include +#include "esphome/core/finite_set_mask.h" namespace esphome { namespace light { @@ -107,13 +108,9 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { // Type alias for raw color mode bitmask values using color_mode_bitmask_t = uint16_t; -// Constants for ColorMode count and bit range -static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE -static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type - -// Compile-time array of all ColorMode values in declaration order -// Bit positions (0-9) map directly to enum declaration order -static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = { +// Lookup table for ColorMode bit mapping +// This array defines the canonical order of color modes (bit 0-9) +constexpr ColorMode COLOR_MODE_LOOKUP[] = { ColorMode::UNKNOWN, // bit 0 ColorMode::ON_OFF, // bit 1 ColorMode::BRIGHTNESS, // bit 2 @@ -126,33 +123,42 @@ static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = { ColorMode::RGB_COLD_WARM_WHITE, // bit 9 }; -/// Map ColorMode enum values to bit positions (0-9) -/// Bit positions follow the enum declaration order -static constexpr int mode_to_bit(ColorMode mode) { - // Linear search through COLOR_MODES array - // Compiler optimizes this to efficient code since array is constexpr - for (int i = 0; i < COLOR_MODE_COUNT; ++i) { - if (COLOR_MODES[i] == mode) - return i; - } - return 0; -} +/// Bit mapping policy for ColorMode +/// Uses lookup table for non-contiguous enum values +struct ColorModeBitPolicy { + using mask_t = uint16_t; // 10 bits requires uint16_t + static constexpr int MAX_BITS = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); -/// Map bit positions (0-9) to ColorMode enum values -/// Bit positions follow the enum declaration order -static constexpr ColorMode bit_to_mode(int bit) { - // Direct lookup in COLOR_MODES array - return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN; -} + static constexpr unsigned to_bit(ColorMode mode) { + // Linear search through lookup table + // Compiler optimizes this to efficient code since array is constexpr + for (int i = 0; i < MAX_BITS; ++i) { + if (COLOR_MODE_LOOKUP[i] == mode) + return i; + } + return 0; + } + + static constexpr ColorMode from_bit(unsigned bit) { + return (bit < MAX_BITS) ? COLOR_MODE_LOOKUP[bit] : ColorMode::UNKNOWN; + } +}; + +// Type alias for ColorMode bitmask using policy-based design +using ColorModeMask = FiniteSetMask; + +// Number of ColorCapability enum values +constexpr int COLOR_CAPABILITY_COUNT = 6; /// Helper to compute capability bitmask at compile time -static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) { - color_mode_bitmask_t mask = 0; +constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { + uint16_t mask = 0; uint8_t cap_bit = static_cast(capability); // Check each ColorMode to see if it has this capability - for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) { - uint8_t mode_val = static_cast(bit_to_mode(bit)); + constexpr int color_mode_count = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); + for (int bit = 0; bit < color_mode_count; ++bit) { + uint8_t mode_val = static_cast(COLOR_MODE_LOOKUP[bit]); if ((mode_val & cap_bit) != 0) { mask |= (1 << bit); } @@ -160,12 +166,9 @@ static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability return mask; } -// Number of ColorCapability enum values -static constexpr int COLOR_CAPABILITY_COUNT = 6; - /// Compile-time lookup table mapping ColorCapability to bitmask /// This array is computed at compile time using constexpr -static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = { +constexpr uint16_t CAPABILITY_BITMASKS[] = { compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0 compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1 compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2 @@ -174,130 +177,38 @@ static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = { compute_capability_bitmask(ColorCapability::RGB), // 1 << 5 }; -/// Bitmask for storing a set of ColorMode values efficiently. -/// Replaces std::set to eliminate red-black tree overhead (~586 bytes). -class ColorModeMask { - public: - constexpr ColorModeMask() = default; - - /// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE} - constexpr ColorModeMask(std::initializer_list modes) { - for (auto mode : modes) { - this->add(mode); - } - } - - constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); } - - /// Add multiple modes at once using initializer list - constexpr void add(std::initializer_list modes) { - for (auto mode : modes) { - this->add(mode); - } - } - - constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; } - - constexpr size_t size() const { - // Count set bits using Brian Kernighan's algorithm - // More efficient for sparse bitmasks (typical case: 2-4 modes out of 10) - uint16_t n = this->mask_; - size_t count = 0; - while (n) { - n &= n - 1; // Clear the least significant set bit - count++; - } - return count; - } - - constexpr bool empty() const { return this->mask_ == 0; } - - /// Iterator support for API encoding - class Iterator { - public: - using iterator_category = std::forward_iterator_tag; - using value_type = ColorMode; - using difference_type = std::ptrdiff_t; - using pointer = const ColorMode *; - using reference = ColorMode; - - constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } - - constexpr ColorMode operator*() const { return bit_to_mode(bit_); } - - constexpr Iterator &operator++() { - ++bit_; - advance_to_next_set_bit_(); - return *this; - } - - constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; } - - constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } - - private: - constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); } - - color_mode_bitmask_t mask_; - int bit_; - }; - - constexpr Iterator begin() const { return Iterator(mask_, 0); } - constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); } - - /// Get the raw bitmask value for API encoding - constexpr color_mode_bitmask_t get_mask() const { return this->mask_; } - - /// Find the next set bit in a bitmask starting from a given position - /// Returns the bit position, or MAX_BIT_INDEX if no more bits are set - static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) { - int bit = start_bit; - while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) { - ++bit; - } - return bit; - } - - /// Find the first set bit in a bitmask and return the corresponding ColorMode - /// Used for optimizing compute_color_mode_() intersection logic - static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) { - return bit_to_mode(find_next_set_bit(mask, 0)); - } - - /// Check if a ColorMode is present in a raw bitmask value - /// Useful for checking intersection results without creating a temporary ColorModeMask - static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) { - return (mask & (1 << mode_to_bit(mode))) != 0; - } - - /// Check if any mode in the bitmask has a specific capability - /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) - bool has_capability(ColorCapability capability) const { - // Lookup the pre-computed bitmask for this capability and check intersection with our mask - // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5 - // We need to convert the power-of-2 value to an index - uint8_t cap_val = static_cast(capability); +/** + * @brief Helper function to convert a power-of-2 ColorCapability value to an array index for CAPABILITY_BITMASKS + * lookup. + * + * This function maps ColorCapability values (1, 2, 4, 8, 16, 32) to array indices (0, 1, 2, 3, 4, 5). + * Used to index into the CAPABILITY_BITMASKS lookup table. + * + * @param capability A ColorCapability enum value (must be a power of 2). + * @return The corresponding array index (0-based). + */ +inline int capability_to_index(ColorCapability capability) { + uint8_t cap_val = static_cast(capability); #if defined(__GNUC__) || defined(__clang__) - // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) - int index = __builtin_ctz(cap_val); + // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) + return __builtin_ctz(cap_val); #else - // Fallback for compilers without __builtin_ctz - int index = 0; - while (cap_val > 1) { - cap_val >>= 1; - ++index; - } -#endif - return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0; + // Fallback for compilers without __builtin_ctz + int index = 0; + while (cap_val > 1) { + cap_val >>= 1; + ++index; } + return index; +#endif +} - private: - // Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan). - // Currently only 10 ColorMode values exist, so 16 bits is sufficient. - // Can be changed to uint32_t if more than 16 color modes are needed in the future. - // Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes). - color_mode_bitmask_t mask_{0}; -}; +/// Check if any mode in the bitmask has a specific capability +/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) +inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) { + // Lookup the pre-computed bitmask for this capability and check intersection with our mask + return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0; +} } // namespace light } // namespace esphome diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index f611baba71..df17f53adc 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() { // Use the preferred suitable mode. if (intersection != 0) { - ColorMode mode = ColorModeMask::first_mode_from_mask(intersection); + ColorMode mode = ColorModeMask::first_value_from_mask(intersection); ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 4532edca83..294b0cad1d 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -26,9 +26,9 @@ class LightTraits { this->supported_color_modes_ = ColorModeMask(modes); } - bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); } + bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode) > 0; } bool supports_color_capability(ColorCapability color_capability) const { - return this->supported_color_modes_.has_capability(color_capability); + return has_capability(this->supported_color_modes_, color_capability); } float get_min_mireds() const { return this->min_mireds_; } diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h new file mode 100644 index 0000000000..f9cd0377c7 --- /dev/null +++ b/esphome/core/finite_set_mask.h @@ -0,0 +1,171 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace esphome { + +/// Default bit mapping policy for contiguous enums starting at 0 +/// Provides 1:1 mapping where enum value equals bit position +template struct DefaultBitPolicy { + // Automatic bitmask type selection based on MaxBits + // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t + using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t, + typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; + + static constexpr int MAX_BITS = MaxBits; + + static constexpr unsigned to_bit(ValueType value) { return static_cast(value); } + + static constexpr ValueType from_bit(unsigned bit) { return static_cast(bit); } +}; + +/// Generic bitmask for storing a finite set of discrete values efficiently. +/// Replaces std::set to eliminate red-black tree overhead (~586 bytes per instantiation). +/// +/// Template parameters: +/// ValueType: The type to store (typically enum, but can be any discrete bounded type) +/// BitPolicy: Policy class defining bit mapping and mask type (defaults to DefaultBitPolicy) +/// +/// BitPolicy requirements: +/// - using mask_t = // Bitmask storage type +/// - static constexpr int MAX_BITS // Maximum number of bits +/// - static constexpr unsigned to_bit(ValueType) // Convert value to bit position +/// - static constexpr ValueType from_bit(unsigned) // Convert bit position to value +/// +/// Example usage (1:1 mapping - climate enums): +/// // For contiguous enums starting at 0, use DefaultBitPolicy +/// using ClimateModeMask = FiniteSetMask>; +/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); +/// if (modes.count(CLIMATE_MODE_HEAT)) { ... } +/// for (auto mode : modes) { ... } +/// +/// Example usage (custom mapping - ColorMode): +/// // For custom mappings, define a custom BitPolicy +/// // See esphome/components/light/color_mode.h for complete example +/// +/// Design notes: +/// - Policy-based design allows custom bit mappings without template specialization +/// - Iterator converts bit positions to actual values during traversal +/// - All operations are constexpr-compatible for compile-time initialization +/// - Drop-in replacement for std::set with simpler API +/// +template> class FiniteSetMask { + public: + using bitmask_t = typename BitPolicy::mask_t; + + constexpr FiniteSetMask() = default; + + /// Construct from initializer list: {VALUE1, VALUE2, ...} + constexpr FiniteSetMask(std::initializer_list values) { + for (auto value : values) { + this->insert(value); + } + } + + /// Add a single value to the set (std::set compatibility) + constexpr void insert(ValueType value) { this->mask_ |= (static_cast(1) << BitPolicy::to_bit(value)); } + + /// Add multiple values from initializer list + constexpr void insert(std::initializer_list values) { + for (auto value : values) { + this->insert(value); + } + } + + /// Remove a value from the set (std::set compatibility) + constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast(1) << BitPolicy::to_bit(value)); } + + /// Clear all values from the set + constexpr void clear() { this->mask_ = 0; } + + /// Check if the set contains a specific value (std::set compatibility) + /// Returns 1 if present, 0 if not (same as std::set for unique elements) + constexpr size_t count(ValueType value) const { + return (this->mask_ & (static_cast(1) << BitPolicy::to_bit(value))) != 0 ? 1 : 0; + } + + /// Count the number of values in the set + constexpr size_t size() const { + // Brian Kernighan's algorithm - efficient for sparse bitmasks + // Typical case: 2-4 modes out of 10 possible + bitmask_t n = this->mask_; + size_t count = 0; + while (n) { + n &= n - 1; // Clear the least significant set bit + count++; + } + return count; + } + + /// Check if the set is empty + constexpr bool empty() const { return this->mask_ == 0; } + + /// Iterator support for range-based for loops and API encoding + /// Iterates over set bits and converts bit positions to values + /// Optimization: removes bits from mask as we iterate + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = ValueType; + using difference_type = std::ptrdiff_t; + using pointer = const ValueType *; + using reference = ValueType; + + constexpr explicit Iterator(bitmask_t mask) : mask_(mask) {} + + constexpr ValueType operator*() const { + // Return value for the first set bit + return BitPolicy::from_bit(find_next_set_bit(mask_, 0)); + } + + constexpr Iterator &operator++() { + // Clear the lowest set bit (Brian Kernighan's algorithm) + mask_ &= mask_ - 1; + return *this; + } + + constexpr bool operator==(const Iterator &other) const { return mask_ == other.mask_; } + + constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } + + private: + bitmask_t mask_; + }; + + constexpr Iterator begin() const { return Iterator(mask_); } + constexpr Iterator end() const { return Iterator(0); } + + /// Get the raw bitmask value for optimized operations + constexpr bitmask_t get_mask() const { return this->mask_; } + + /// Check if a specific value is present in a raw bitmask + /// Useful for checking intersection results without creating temporary objects + static constexpr bool mask_contains(bitmask_t mask, ValueType value) { + return (mask & (static_cast(1) << BitPolicy::to_bit(value))) != 0; + } + + /// Get the first value from a raw bitmask + /// Used for optimizing intersection logic (e.g., "pick first suitable mode") + static constexpr ValueType first_value_from_mask(bitmask_t mask) { + return BitPolicy::from_bit(find_next_set_bit(mask, 0)); + } + + /// Find the next set bit in a bitmask starting from a given position + /// Returns the bit position, or MAX_BITS if no more bits are set + static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) { + int bit = start_bit; + while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast(1) << bit))) { + ++bit; + } + return bit; + } + + protected: + bitmask_t mask_{0}; +}; + +} // namespace esphome From 7e5b82c5f3ece45a0f7c9d98eeecc1c2cf93126d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 22 Oct 2025 20:24:08 -0500 Subject: [PATCH 05/12] [improv_serial] Various optimizations (#11473) Co-authored-by: J. Nick Koston --- .../improv_serial/improv_serial_component.cpp | 183 +++++++++--------- .../improv_serial/improv_serial_component.h | 27 ++- .../components/improv_base/common-uart0.yaml | 8 + .../improv_base/test-uart0.esp8266-ard.yaml | 1 + 4 files changed, 128 insertions(+), 91 deletions(-) create mode 100644 tests/components/improv_base/common-uart0.yaml create mode 100644 tests/components/improv_base/test-uart0.esp8266-ard.yaml diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index ce82504d3c..9d080ea98e 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -28,6 +28,38 @@ void ImprovSerialComponent::setup() { } } +void ImprovSerialComponent::loop() { + if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { + this->last_read_byte_ = 0; + this->rx_buffer_.clear(); + ESP_LOGV(TAG, "Timeout"); + } + + auto byte = this->read_byte_(); + while (byte.has_value()) { + if (this->parse_improv_serial_byte_(byte.value())) { + this->last_read_byte_ = millis(); + } else { + this->last_read_byte_ = 0; + this->rx_buffer_.clear(); + } + byte = this->read_byte_(); + } + + if (this->state_ == improv::STATE_PROVISIONING) { + if (wifi::global_wifi_component->is_connected()) { + wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), + this->connecting_sta_.get_password()); + this->connecting_sta_ = {}; + this->cancel_timeout("wifi-connect-timeout"); + this->set_state_(improv::STATE_PROVISIONED); + + std::vector url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); + this->send_response_(url); + } + } +} + void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } optional ImprovSerialComponent::read_byte_() { @@ -78,8 +110,28 @@ optional ImprovSerialComponent::read_byte_() { return byte; } -void ImprovSerialComponent::write_data_(std::vector &data) { - data.push_back('\n'); +void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) { + // First, set length field + this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1; + + const bool there_is_data = data != nullptr && size > 0; + // If there_is_data, checksum must not include our optional data byte + const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2; + // Only transmit the full buffer length if there is no data (only state/error byte is provided in this case) + const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE; + // Calculate checksum for message + uint8_t checksum = 0; + for (uint8_t i = 0; i < header_checksum_len; i++) { + checksum += this->tx_header_[i]; + } + if (there_is_data) { + // Include data in checksum + for (size_t i = 0; i < size; i++) { + checksum += data[i]; + } + } + this->tx_header_[TX_CHECKSUM_IDX] = checksum; + #ifdef USE_ESP32 switch (logger::global_logger->get_uart()) { case logger::UART_SELECTION_UART0: @@ -87,63 +139,45 @@ void ImprovSerialComponent::write_data_(std::vector &data) { #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) case logger::UART_SELECTION_UART2: -#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 - uart_write_bytes(this->uart_num_, data.data(), data.size()); +#endif + uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len); + if (there_is_data) { + uart_write_bytes(this->uart_num_, data, size); + uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline + } break; #if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) - case logger::UART_SELECTION_USB_CDC: { - const char *msg = (char *) data.data(); - esp_usb_console_write_buf(msg, data.size()); + case logger::UART_SELECTION_USB_CDC: + esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len); + if (there_is_data) { + esp_usb_console_write_buf((const char *) data, size); + esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX], + 2); // Footer: checksum and newline + } break; - } -#endif // USE_LOGGER_USB_CDC +#endif #ifdef USE_LOGGER_USB_SERIAL_JTAG case logger::UART_SELECTION_USB_SERIAL_JTAG: - usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); - delay(10); - usb_serial_jtag_ll_txfifo_flush(); // fixes for issue in IDF 4.4.7 + usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS); + if (there_is_data) { + usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS); + usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2, + 20 / portTICK_PERIOD_MS); // Footer: checksum and newline + } break; -#endif // USE_LOGGER_USB_SERIAL_JTAG +#endif default: break; } #elif defined(USE_ARDUINO) - this->hw_serial_->write(data.data(), data.size()); + this->hw_serial_->write(this->tx_header_, header_tx_len); + if (there_is_data) { + this->hw_serial_->write(data, size); + this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline + } #endif } -void ImprovSerialComponent::loop() { - if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { - this->last_read_byte_ = 0; - this->rx_buffer_.clear(); - ESP_LOGV(TAG, "Improv Serial timeout"); - } - - auto byte = this->read_byte_(); - while (byte.has_value()) { - if (this->parse_improv_serial_byte_(byte.value())) { - this->last_read_byte_ = millis(); - } else { - this->last_read_byte_ = 0; - this->rx_buffer_.clear(); - } - byte = this->read_byte_(); - } - - if (this->state_ == improv::STATE_PROVISIONING) { - if (wifi::global_wifi_component->is_connected()) { - wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), - this->connecting_sta_.get_password()); - this->connecting_sta_ = {}; - this->cancel_timeout("wifi-connect-timeout"); - this->set_state_(improv::STATE_PROVISIONED); - - std::vector url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); - this->send_response_(url); - } - } -} - std::vector ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { std::vector urls; #ifdef USE_IMPROV_SERIAL_NEXT_URL @@ -177,13 +211,13 @@ std::vector ImprovSerialComponent::build_version_info_() { bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); - ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte); + ESP_LOGV(TAG, "Byte: 0x%02X", byte); const uint8_t *raw = &this->rx_buffer_[0]; return improv::parse_improv_serial_byte( at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); }, [this](improv::Error error) -> void { - ESP_LOGW(TAG, "Error decoding Improv payload"); + ESP_LOGW(TAG, "Error decoding payload"); this->set_error_(error); }); } @@ -199,7 +233,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command wifi::global_wifi_component->set_sta(sta); wifi::global_wifi_component->start_connecting(sta, false); this->set_state_(improv::STATE_PROVISIONING); - ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), + ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); @@ -240,7 +274,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command return true; } default: { - ESP_LOGW(TAG, "Unknown Improv payload"); + ESP_LOGW(TAG, "Unknown payload"); this->set_error_(improv::ERROR_UNKNOWN_RPC); return false; } @@ -249,57 +283,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command void ImprovSerialComponent::set_state_(improv::State state) { this->state_ = state; - - std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; - data.resize(11); - data[6] = IMPROV_SERIAL_VERSION; - data[7] = TYPE_CURRENT_STATE; - data[8] = 1; - data[9] = state; - - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; - data[10] = checksum; - - this->write_data_(data); + this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE; + this->tx_header_[TX_DATA_IDX] = state; + this->write_data_(); } void ImprovSerialComponent::set_error_(improv::Error error) { - std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; - data.resize(11); - data[6] = IMPROV_SERIAL_VERSION; - data[7] = TYPE_ERROR_STATE; - data[8] = 1; - data[9] = error; - - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; - data[10] = checksum; - this->write_data_(data); + this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE; + this->tx_header_[TX_DATA_IDX] = error; + this->write_data_(); } void ImprovSerialComponent::send_response_(std::vector &response) { - std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; - data.resize(9); - data[6] = IMPROV_SERIAL_VERSION; - data[7] = TYPE_RPC_RESPONSE; - data[8] = response.size(); - data.insert(data.end(), response.begin(), response.end()); - - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; - data.push_back(checksum); - - this->write_data_(data); + this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE; + this->write_data_(response.data(), response.size()); } void ImprovSerialComponent::on_wifi_connect_timeout_() { this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); this->set_state_(improv::STATE_AUTHORIZED); - ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); + ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network"); wifi::global_wifi_component->clear_sta(); } diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index c3c9aee24e..057247f376 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -26,6 +26,16 @@ namespace esphome { namespace improv_serial { +// TX buffer layout constants +static constexpr uint8_t TX_HEADER_SIZE = 6; // Bytes 0-5 = "IMPROV" +static constexpr uint8_t TX_VERSION_IDX = 6; +static constexpr uint8_t TX_TYPE_IDX = 7; +static constexpr uint8_t TX_LENGTH_IDX = 8; +static constexpr uint8_t TX_DATA_IDX = 9; // For state/error messages only +static constexpr uint8_t TX_CHECKSUM_IDX = 10; +static constexpr uint8_t TX_NEWLINE_IDX = 11; +static constexpr uint8_t TX_BUFFER_SIZE = 12; + enum ImprovSerialType : uint8_t { TYPE_CURRENT_STATE = 0x01, TYPE_ERROR_STATE = 0x02, @@ -57,7 +67,22 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase { std::vector build_version_info_(); optional read_byte_(); - void write_data_(std::vector &data); + void write_data_(const uint8_t *data = nullptr, size_t size = 0); + + uint8_t tx_header_[TX_BUFFER_SIZE] = { + 'I', // 0: Header + 'M', // 1: Header + 'P', // 2: Header + 'R', // 3: Header + 'O', // 4: Header + 'V', // 5: Header + IMPROV_SERIAL_VERSION, // 6: Version + 0, // 7: ImprovSerialType + 0, // 8: Length + 0, // 9...X: Data (here, one byte reserved for state/error) + 0, // X + 10: Checksum + '\n', + }; #ifdef USE_ESP32 uart_port_t uart_num_; diff --git a/tests/components/improv_base/common-uart0.yaml b/tests/components/improv_base/common-uart0.yaml new file mode 100644 index 0000000000..7b7730fd46 --- /dev/null +++ b/tests/components/improv_base/common-uart0.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +logger: + hardware_uart: UART0 + +improv_serial: diff --git a/tests/components/improv_base/test-uart0.esp8266-ard.yaml b/tests/components/improv_base/test-uart0.esp8266-ard.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_base/test-uart0.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml From 9b78098eec3c965df24933d2230885381997b399 Mon Sep 17 00:00:00 2001 From: optimusprimespace <62800678+optimusprimespace@users.noreply.github.com> Date: Thu, 23 Oct 2025 05:24:17 +0300 Subject: [PATCH 06/12] [hdc2010] New component (#6674) Co-authored-by: Keith Burzinski 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: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hdc2010/__init__.py | 1 + esphome/components/hdc2010/hdc2010.cpp | 111 ++++++++++++++++++ esphome/components/hdc2010/hdc2010.h | 32 +++++ esphome/components/hdc2010/sensor.py | 56 +++++++++ tests/components/hdc2010/common.yaml | 7 ++ .../components/hdc2010/test.esp32-c3-idf.yaml | 4 + tests/components/hdc2010/test.esp32-idf.yaml | 4 + .../components/hdc2010/test.esp8266-ard.yaml | 4 + tests/components/hdc2010/test.rp2040-ard.yaml | 4 + 10 files changed, 224 insertions(+) create mode 100644 esphome/components/hdc2010/__init__.py create mode 100644 esphome/components/hdc2010/hdc2010.cpp create mode 100644 esphome/components/hdc2010/hdc2010.h create mode 100644 esphome/components/hdc2010/sensor.py create mode 100644 tests/components/hdc2010/common.yaml create mode 100644 tests/components/hdc2010/test.esp32-c3-idf.yaml create mode 100644 tests/components/hdc2010/test.esp32-idf.yaml create mode 100644 tests/components/hdc2010/test.esp8266-ard.yaml create mode 100644 tests/components/hdc2010/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 4f860375d9..667a44fc03 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -201,6 +201,7 @@ esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 +esphome/components/hdc2010/* @optimusprimespace @ssieb esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal diff --git a/esphome/components/hdc2010/__init__.py b/esphome/components/hdc2010/__init__.py new file mode 100644 index 0000000000..badf9dbb0c --- /dev/null +++ b/esphome/components/hdc2010/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@optimusprimespace", "@ssieb"] diff --git a/esphome/components/hdc2010/hdc2010.cpp b/esphome/components/hdc2010/hdc2010.cpp new file mode 100644 index 0000000000..c53fdb3f5b --- /dev/null +++ b/esphome/components/hdc2010/hdc2010.cpp @@ -0,0 +1,111 @@ +#include "esphome/core/hal.h" +#include "hdc2010.h" +// https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js +// https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp +namespace esphome { +namespace hdc2010 { + +static const char *const TAG = "hdc2010"; + +static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet +static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; +static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; +static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; +static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; +static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; +static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; +static const uint8_t CONFIG = 0x0E; +static const uint8_t MEASUREMENT_CONFIG = 0x0F; + +void HDC2010Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + + const uint8_t data[2] = { + 0b00000000, // resolution 14bit for both humidity and temperature + 0b00000000 // reserved + }; + + if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { + ESP_LOGW(TAG, "Initial config instruction error"); + this->status_set_warning(); + return; + } + + // Set measurement mode to temperature and humidity + uint8_t config_contents; + this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); + config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode + this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + + // Set rate to manual + this->read_register(CONFIG, &config_contents, 1); + config_contents &= 0x8F; + this->write_bytes(CONFIG, &config_contents, 1); + + // Set temperature resolution to 14bit + this->read_register(CONFIG, &config_contents, 1); + config_contents &= 0x3F; + this->write_bytes(CONFIG, &config_contents, 1); + + // Set humidity resolution to 14bit + this->read_register(CONFIG, &config_contents, 1); + config_contents &= 0xCF; + this->write_bytes(CONFIG, &config_contents, 1); +} + +void HDC2010Component::dump_config() { + ESP_LOGCONFIG(TAG, "HDC2010:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void HDC2010Component::update() { + // Trigger measurement + uint8_t config_contents; + this->read_register(CONFIG, &config_contents, 1); + config_contents |= 0x01; + this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + + // 1ms delay after triggering the sample + set_timeout(1, [this]() { + if (this->temperature_sensor_ != nullptr) { + float temp = this->read_temp(); + this->temperature_sensor_->publish_state(temp); + ESP_LOGD(TAG, "Temp=%.1f°C", temp); + } + + if (this->humidity_sensor_ != nullptr) { + float humidity = this->read_humidity(); + this->humidity_sensor_->publish_state(humidity); + ESP_LOGD(TAG, "Humidity=%.1f%%", humidity); + } + }); +} + +float HDC2010Component::read_temp() { + uint8_t byte[2]; + + this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); + this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); + + uint16_t temp = encode_uint16(byte[1], byte[0]); + return (float) temp * 0.0025177f - 40.0f; +} + +float HDC2010Component::read_humidity() { + uint8_t byte[2]; + + this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); + this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); + + uint16_t humidity = encode_uint16(byte[1], byte[0]); + return (float) humidity * 0.001525879f; +} + +} // namespace hdc2010 +} // namespace esphome diff --git a/esphome/components/hdc2010/hdc2010.h b/esphome/components/hdc2010/hdc2010.h new file mode 100644 index 0000000000..52c00686e6 --- /dev/null +++ b/esphome/components/hdc2010/hdc2010.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace hdc2010 { + +class HDC2010Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } + + void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } + + /// Setup the sensor and check for connection. + void setup() override; + void dump_config() override; + /// Retrieve the latest sensor values. This operation takes approximately 16ms. + void update() override; + + float read_temp(); + + float read_humidity(); + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace hdc2010 +} // namespace esphome diff --git a/esphome/components/hdc2010/sensor.py b/esphome/components/hdc2010/sensor.py new file mode 100644 index 0000000000..15e19f2cc8 --- /dev/null +++ b/esphome/components/hdc2010/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc2010_ns = cg.esphome_ns.namespace("hdc2010") +HDC2010Component = hdc2010_ns.class_( + "HDC2010Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC2010Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/tests/components/hdc2010/common.yaml b/tests/components/hdc2010/common.yaml new file mode 100644 index 0000000000..a22b3f15ce --- /dev/null +++ b/tests/components/hdc2010/common.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hdc2010 + i2c_id: i2c_bus + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hdc2010/test.esp32-c3-idf.yaml b/tests/components/hdc2010/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9990d96d29 --- /dev/null +++ b/tests/components/hdc2010/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2010/test.esp32-idf.yaml b/tests/components/hdc2010/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/hdc2010/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2010/test.esp8266-ard.yaml b/tests/components/hdc2010/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/hdc2010/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2010/test.rp2040-ard.yaml b/tests/components/hdc2010/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/hdc2010/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From d23e25f0991cb20900c0de5576218c3623d7a59e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 16:31:51 -1000 Subject: [PATCH 07/12] [api] Fix clang-tidy modernize-use-emplace warning for light effects (#11490) --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7c135946f8..f76080253d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -486,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c if (light->supports_effects()) { msg.effects.emplace_back("None"); for (auto *effect : light->get_effects()) { - msg.effects.push_back(effect->get_name()); + msg.effects.emplace_back(effect->get_name()); } } return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, From 6c2ce5cacff9bf151642ee8c2185349de697c420 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:36:30 -1000 Subject: [PATCH 08/12] Bump bleak from 1.0.1 to 1.1.1 (#11492) Signed-off-by: dependabot[bot] 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 6966ebe583..59592ec0a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ pillow==11.3.0 cairosvg==2.8.2 freetype-py==2.5.1 jinja2==3.1.6 -bleak==1.0.1 +bleak==1.1.1 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From 5b023f9369e3ce84e4f8b5717928bd9bbc1e735c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 16:37:50 -1000 Subject: [PATCH 09/12] [ethernet] Add RMII GPIO pin conflict validation (#11488) --- esphome/components/ethernet/__init__.py | 84 ++++++++++++++++++- tests/components/ethernet/common-dp83848.yaml | 4 +- tests/components/ethernet/common-ip101.yaml | 4 +- tests/components/ethernet/common-jl1101.yaml | 4 +- tests/components/ethernet/common-ksz8081.yaml | 4 +- .../ethernet/common-ksz8081rna.yaml | 4 +- tests/components/ethernet/common-lan8670.yaml | 4 +- tests/components/ethernet/common-lan8720.yaml | 4 +- tests/components/ethernet/common-rtl8201.yaml | 4 +- tests/components/ethernet_info/common.yaml | 4 +- 10 files changed, 98 insertions(+), 22 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 7384bb26d3..77f70a3630 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -32,6 +32,7 @@ from esphome.const import ( CONF_MISO_PIN, CONF_MODE, CONF_MOSI_PIN, + CONF_NUMBER, CONF_PAGE_ID, CONF_PIN, CONF_POLLING_INTERVAL, @@ -52,12 +53,36 @@ from esphome.core import ( coroutine_with_priority, ) import esphome.final_validate as fv +from esphome.types import ConfigType CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] LOGGER = logging.getLogger(__name__) +# RMII pins that are hardcoded on ESP32 classic and cannot be changed +# These pins are used by the internal Ethernet MAC when using RMII PHYs +ESP32_RMII_FIXED_PINS = { + 19: "EMAC_TXD0", + 21: "EMAC_TX_EN", + 22: "EMAC_TXD1", + 25: "EMAC_RXD0", + 26: "EMAC_RXD1", + 27: "EMAC_RX_CRS_DV", +} + +# RMII default pins for ESP32-P4 +# These are the default pins used by ESP-IDF and are configurable in principle, +# but ESPHome's ethernet component currently has no way to change them +ESP32P4_RMII_DEFAULT_PINS = { + 34: "EMAC_TXD0", + 35: "EMAC_TXD1", + 28: "EMAC_RX_CRS_DV", + 29: "EMAC_RXD0", + 30: "EMAC_RXD1", + 49: "EMAC_TX_EN", +} + ethernet_ns = cg.esphome_ns.namespace("ethernet") PHYRegister = ethernet_ns.struct("PHYRegister") CONF_PHY_ADDR = "phy_addr" @@ -273,7 +298,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config): +def _final_validate_spi(config): if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return if spi_configs := fv.full_config.get().get(CONF_SPI): @@ -292,9 +317,6 @@ def _final_validate(config): ) -FINAL_VALIDATE_SCHEMA = _final_validate - - def manual_ip(config): return cg.StructInitializer( ManualIP, @@ -383,3 +405,57 @@ async def to_code(config): if CORE.using_arduino: cg.add_library("WiFi", None) + + +def _final_validate_rmii_pins(config: ConfigType) -> None: + """Validate that RMII pins are not used by other components.""" + # Only validate for RMII-based PHYs on ESP32/ESP32P4 + if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH": + return # SPI and OPENETH don't use RMII + + variant = get_esp32_variant() + if variant == VARIANT_ESP32: + rmii_pins = ESP32_RMII_FIXED_PINS + is_configurable = False + elif variant == VARIANT_ESP32P4: + rmii_pins = ESP32P4_RMII_DEFAULT_PINS + is_configurable = True + else: + return # No RMII validation needed for other variants + + # Check all used pins against RMII reserved pins + for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values(): + for pin_path, _, pin_config in pin_list: + pin_num = pin_config.get(CONF_NUMBER) + if pin_num not in rmii_pins: + continue + # Found a conflict - show helpful error message + pin_function = rmii_pins[pin_num] + component_path = ".".join(str(p) for p in pin_path) + if is_configurable: + error_msg = ( + f"GPIO{pin_num} is used by Ethernet RMII " + f"({pin_function}) with the current default " + f"configuration. This conflicts with '{component_path}'. " + f"Please choose a different GPIO pin for " + f"'{component_path}'." + ) + else: + error_msg = ( + f"GPIO{pin_num} is reserved for Ethernet RMII " + f"({pin_function}) and cannot be used. This pin is " + f"hardcoded by ESP-IDF and cannot be changed when using " + f"RMII Ethernet PHYs. Please choose a different GPIO pin " + f"for '{component_path}'." + ) + raise cv.Invalid(error_msg, path=pin_path) + + +def _final_validate(config: ConfigType) -> ConfigType: + """Final validation for Ethernet component.""" + _final_validate_spi(config) + _final_validate_rmii_pins(config) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/tests/components/ethernet/common-dp83848.yaml b/tests/components/ethernet/common-dp83848.yaml index 7cedfeaf08..f9069c5fb9 100644 --- a/tests/components/ethernet/common-dp83848.yaml +++ b/tests/components/ethernet/common-dp83848.yaml @@ -1,12 +1,12 @@ ethernet: type: DP83848 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-ip101.yaml b/tests/components/ethernet/common-ip101.yaml index 2dece15171..cea7a5cc35 100644 --- a/tests/components/ethernet/common-ip101.yaml +++ b/tests/components/ethernet/common-ip101.yaml @@ -1,12 +1,12 @@ ethernet: type: IP101 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-jl1101.yaml b/tests/components/ethernet/common-jl1101.yaml index b6ea884102..7b0a2dfdc4 100644 --- a/tests/components/ethernet/common-jl1101.yaml +++ b/tests/components/ethernet/common-jl1101.yaml @@ -1,12 +1,12 @@ ethernet: type: JL1101 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-ksz8081.yaml b/tests/components/ethernet/common-ksz8081.yaml index f70d42319e..65541832c2 100644 --- a/tests/components/ethernet/common-ksz8081.yaml +++ b/tests/components/ethernet/common-ksz8081.yaml @@ -1,12 +1,12 @@ ethernet: type: KSZ8081 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-ksz8081rna.yaml b/tests/components/ethernet/common-ksz8081rna.yaml index 18efdae0e1..f04cba15b2 100644 --- a/tests/components/ethernet/common-ksz8081rna.yaml +++ b/tests/components/ethernet/common-ksz8081rna.yaml @@ -1,12 +1,12 @@ ethernet: type: KSZ8081RNA mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-lan8670.yaml b/tests/components/ethernet/common-lan8670.yaml index ec2f24273d..fb751ebd23 100644 --- a/tests/components/ethernet/common-lan8670.yaml +++ b/tests/components/ethernet/common-lan8670.yaml @@ -1,12 +1,12 @@ ethernet: type: LAN8670 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-lan8720.yaml b/tests/components/ethernet/common-lan8720.yaml index 204c1d9210..838d57df28 100644 --- a/tests/components/ethernet/common-lan8720.yaml +++ b/tests/components/ethernet/common-lan8720.yaml @@ -1,12 +1,12 @@ ethernet: type: LAN8720 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-rtl8201.yaml b/tests/components/ethernet/common-rtl8201.yaml index 8b9f2b86f2..0e7cbe73c6 100644 --- a/tests/components/ethernet/common-rtl8201.yaml +++ b/tests/components/ethernet/common-rtl8201.yaml @@ -1,12 +1,12 @@ ethernet: type: RTL8201 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet_info/common.yaml b/tests/components/ethernet_info/common.yaml index f45f345316..b720521d10 100644 --- a/tests/components/ethernet_info/common.yaml +++ b/tests/components/ethernet_info/common.yaml @@ -1,12 +1,12 @@ ethernet: type: LAN8720 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 From 3d21adecd335e683b695ccfc00175da442214893 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 02:58:09 +0000 Subject: [PATCH 10/12] Bump aioesphomeapi from 42.2.0 to 42.3.0 (#11493) 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 59592ec0a2..351143591a 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==42.2.0 +aioesphomeapi==42.3.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From 917deac7cb7d6747f06a3c9546be85b3433e29b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 18:02:19 -1000 Subject: [PATCH 11/12] [scheduler] Remove unused include after defer queue optimization (#11491) --- esphome/core/scheduler.h | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ad0ec0284e..df0be0e4ce 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -4,7 +4,6 @@ #include #include #include -#include #ifdef ESPHOME_THREAD_MULTI_ATOMICS #include #endif From f9b08491cc54d8fe24f3f5ce1a25ee257f5d0d09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 06:50:24 -0600 Subject: [PATCH 12/12] [tests] Fix millis() ambiguity in component tests with gps component --- tests/components/absolute_humidity/common.yaml | 4 ++-- tests/components/analog_threshold/common.yaml | 2 +- tests/components/bang_bang/common.yaml | 2 +- tests/components/binary_sensor_map/common.yaml | 6 +++--- tests/components/combination/common.yaml | 4 ++-- tests/components/duty_time/common.yaml | 2 +- tests/components/endstop/common.yaml | 2 +- tests/components/lock/common.yaml | 2 +- tests/components/pid/common.yaml | 2 +- tests/components/prometheus/common.yaml | 12 ++++++------ 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/components/absolute_humidity/common.yaml b/tests/components/absolute_humidity/common.yaml index 026f88654f..f6b1c02886 100644 --- a/tests/components/absolute_humidity/common.yaml +++ b/tests/components/absolute_humidity/common.yaml @@ -6,14 +6,14 @@ sensor: - platform: template id: template_humidity lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 0.6; } return 0.0; - platform: template id: template_temperature lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } return 0.0; diff --git a/tests/components/analog_threshold/common.yaml b/tests/components/analog_threshold/common.yaml index 26c401b92a..7d9dc4bc9b 100644 --- a/tests/components/analog_threshold/common.yaml +++ b/tests/components/analog_threshold/common.yaml @@ -3,7 +3,7 @@ sensor: id: template_sensor name: Template Sensor lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } return 0.0; diff --git a/tests/components/bang_bang/common.yaml b/tests/components/bang_bang/common.yaml index 5882025191..dc7798e2f2 100644 --- a/tests/components/bang_bang/common.yaml +++ b/tests/components/bang_bang/common.yaml @@ -10,7 +10,7 @@ sensor: - platform: template id: template_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } else { return 0.0; diff --git a/tests/components/binary_sensor_map/common.yaml b/tests/components/binary_sensor_map/common.yaml index c054022583..71f4c0158e 100644 --- a/tests/components/binary_sensor_map/common.yaml +++ b/tests/components/binary_sensor_map/common.yaml @@ -2,21 +2,21 @@ binary_sensor: - platform: template id: bin1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; - platform: template id: bin2 lambda: |- - if (millis() > 20000) { + if (esphome::millis() > 20000) { return true; } return false; - platform: template id: bin3 lambda: |- - if (millis() > 30000) { + if (esphome::millis() > 30000) { return true; } return false; diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index 0e5d512d08..bb05fa375b 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -2,14 +2,14 @@ sensor: - platform: template id: template_temperature1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 0.6; } return 0.0; - platform: template id: template_temperature2 lambda: |- - if (millis() > 20000) { + if (esphome::millis() > 20000) { return 0.8; } return 0.0; diff --git a/tests/components/duty_time/common.yaml b/tests/components/duty_time/common.yaml index 761d10f16a..ffe62ec7fc 100644 --- a/tests/components/duty_time/common.yaml +++ b/tests/components/duty_time/common.yaml @@ -2,7 +2,7 @@ binary_sensor: - platform: template id: bin1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; diff --git a/tests/components/endstop/common.yaml b/tests/components/endstop/common.yaml index b92b1e13b9..317b31b1cb 100644 --- a/tests/components/endstop/common.yaml +++ b/tests/components/endstop/common.yaml @@ -2,7 +2,7 @@ binary_sensor: - platform: template id: bin1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; diff --git a/tests/components/lock/common.yaml b/tests/components/lock/common.yaml index 9ba7f34857..35907fe679 100644 --- a/tests/components/lock/common.yaml +++ b/tests/components/lock/common.yaml @@ -15,7 +15,7 @@ lock: id: test_lock1 name: Template Lock lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return LOCK_STATE_LOCKED; } return LOCK_STATE_UNLOCKED; diff --git a/tests/components/pid/common.yaml b/tests/components/pid/common.yaml index 262e75591e..e9478103f6 100644 --- a/tests/components/pid/common.yaml +++ b/tests/components/pid/common.yaml @@ -25,7 +25,7 @@ sensor: - platform: template id: template_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } return 0.0; diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index cf46e882a7..f9bd471ce7 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -33,7 +33,7 @@ sensor: - platform: template id: template_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } return 0.0; @@ -46,7 +46,7 @@ text_sensor: - platform: template id: template_text_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return {"Hello World"}; } return {"Goodbye (cruel) World"}; @@ -56,7 +56,7 @@ binary_sensor: - platform: template id: template_binary_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; @@ -65,7 +65,7 @@ switch: - platform: template id: template_switch1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; @@ -79,7 +79,7 @@ cover: - platform: template id: template_cover1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return COVER_OPEN; } return COVER_CLOSED; @@ -88,7 +88,7 @@ lock: - platform: template id: template_lock1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return LOCK_STATE_LOCKED; } return LOCK_STATE_UNLOCKED;