From cdc87a44456fc813a4f195410b125ea86723245d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Oct 2025 22:46:45 -1000 Subject: [PATCH 1/4] [mdns] Restore mdns_txt_record() public API for external components --- esphome/components/mdns/__init__.py | 83 +++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 6e148092fe..25c004ac5d 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -11,7 +11,7 @@ from esphome.const import ( CONF_SERVICES, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, Lambda, coroutine_with_priority from esphome.coroutine import CoroPriority CODEOWNERS = ["@esphome/core"] @@ -58,9 +58,64 @@ CONFIG_SCHEMA = cv.All( ) +def mdns_txt_record(key: str, value: str) -> cg.RawExpression: + """Create a mDNS TXT record. + + Public API for external components. Do not remove. + + Args: + key: The TXT record key + value: The TXT record value (static string only) + + Returns: + A RawExpression representing a MDNSTXTRecord struct + """ + return cg.RawExpression( + f"{{MDNS_STR({cg.safe_exp(key)}), MDNS_STR({cg.safe_exp(value)})}}" + ) + + +async def _mdns_txt_record_templated( + mdns_comp: cg.Pvariable, key: str, value: Lambda | str +) -> cg.RawExpression: + """Create a mDNS TXT record with support for templated values. + + Internal helper function. + + Args: + mdns_comp: The MDNSComponent instance (from cg.get_variable()) + key: The TXT record key + value: The TXT record value (can be a static string or a lambda template) + + Returns: + A RawExpression representing a MDNSTXTRecord struct + """ + if not cg.is_template(value): + # It's a static string - use directly in flash, no need to store in vector + return mdns_txt_record(key, value) + # It's a lambda - evaluate and store using helper + templated_value = await cg.templatable(value, [], cg.std_string) + safe_key = cg.safe_exp(key) + dynamic_call = f"{mdns_comp}->add_dynamic_txt_value(({templated_value})())" + return cg.RawExpression(f"{{MDNS_STR({safe_key}), MDNS_STR({dynamic_call})}}") + + def mdns_service( service: str, proto: str, port: int, txt_records: list[dict[str, str]] -): +) -> cg.StructInitializer: + """Create a mDNS service. + + Public API for external components. Do not remove. + + Args: + service: Service name (e.g., "_http") + proto: Protocol (e.g., "_tcp" or "_udp") + port: Port number + txt_records: List of MDNSTXTRecord expressions + + Returns: + A StructInitializer representing a MDNSService struct + """ return cg.StructInitializer( MDNSService, ("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")), @@ -120,26 +175,10 @@ async def to_code(config): await cg.register_component(var, config) for service in config[CONF_SERVICES]: - # Build the txt records list for the service - txt_records = [] - for txt_key, txt_value in service[CONF_TXT].items(): - if cg.is_template(txt_value): - # It's a lambda - evaluate and store using helper - templated_value = await cg.templatable(txt_value, [], cg.std_string) - safe_key = cg.safe_exp(txt_key) - dynamic_call = f"{var}->add_dynamic_txt_value(({templated_value})())" - txt_records.append( - cg.RawExpression( - f"{{MDNS_STR({safe_key}), MDNS_STR({dynamic_call})}}" - ) - ) - else: - # It's a static string - use directly in flash, no need to store in vector - txt_records.append( - cg.RawExpression( - f"{{MDNS_STR({cg.safe_exp(txt_key)}), MDNS_STR({cg.safe_exp(txt_value)})}}" - ) - ) + txt_records = [ + await _mdns_txt_record_templated(var, txt_key, txt_value) + for txt_key, txt_value in service[CONF_TXT].items() + ] exp = mdns_service( service[CONF_SERVICE], From 3112c06f1d0d2fd812a61e4b021a0b2efaee53b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 16:07:46 -1000 Subject: [PATCH 2/4] handle p4 --- esphome/components/ethernet/__init__.py | 47 +++++++++++++++++++------ 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index af9938678a..cbd2f07cae 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -60,7 +60,7 @@ DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] LOGGER = logging.getLogger(__name__) -# RMII pins that are hardcoded on ESP32 and cannot be changed +# 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", @@ -71,6 +71,18 @@ ESP32_RMII_FIXED_PINS = { 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" @@ -402,24 +414,37 @@ def _final_validate_rmii_pins(config: ConfigType) -> None: return # SPI and OPENETH don't use RMII variant = get_esp32_variant() - if variant not in (VARIANT_ESP32, VARIANT_ESP32P4): - return # Only ESP32 classic and P4 have RMII + 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 ESP32_RMII_FIXED_PINS: + if pin_num not in rmii_pins: continue # Found a conflict - show helpful error message - pin_function = ESP32_RMII_FIXED_PINS[pin_num] + pin_function = rmii_pins[pin_num] component_path = ".".join(str(p) for p in pin_path) - raise cv.Invalid( - f"GPIO{pin_num} is reserved for Ethernet RMII ({pin_function}) and cannot be used. " - f"This pin is hardcoded by ESP-IDF and cannot be changed when using RMII Ethernet PHYs. " - f"Please choose a different GPIO pin for '{component_path}'.", - path=pin_path, - ) + if is_configurable: + error_msg = ( + f"GPIO{pin_num} is used by Ethernet RMII ({pin_function}) with the current default configuration. " + f"This conflicts with '{component_path}'. " + f"Please choose a different GPIO pin for '{component_path}'." + ) + else: + error_msg = ( + f"GPIO{pin_num} is reserved for Ethernet RMII ({pin_function}) and cannot be used. " + f"This pin is hardcoded by ESP-IDF and cannot be changed when using RMII Ethernet PHYs. " + f"Please choose a different GPIO pin for '{component_path}'." + ) + raise cv.Invalid(error_msg, path=pin_path) def _final_validate(config: ConfigType) -> ConfigType: From f5b995a454d8138b725d2fe24c05518b8781034b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 16:11:37 -1000 Subject: [PATCH 3/4] preen --- esphome/components/ethernet/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index cbd2f07cae..77f70a3630 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -434,15 +434,19 @@ def _final_validate_rmii_pins(config: ConfigType) -> None: component_path = ".".join(str(p) for p in pin_path) if is_configurable: error_msg = ( - f"GPIO{pin_num} is used by Ethernet RMII ({pin_function}) with the current default configuration. " - f"This conflicts with '{component_path}'. " - f"Please choose a different GPIO pin for '{component_path}'." + 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 ({pin_function}) and cannot be used. " - f"This pin is hardcoded by ESP-IDF and cannot be changed when using RMII Ethernet PHYs. " - f"Please choose a different GPIO pin for '{component_path}'." + 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) From c10663d88c21cbe3246516641e0e13ab9e1e0cc0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 18:52:59 -0600 Subject: [PATCH 4/4] [core] Avoid redundant millis() calls in base_automation loop methods --- esphome/core/base_automation.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index e668a1782a..28af02a846 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -103,7 +103,7 @@ template class ForCondition : public Condition, public Co bool check_internal() { bool cond = this->condition_->check(); if (!cond) - this->last_inactive_ = millis(); + this->last_inactive_ = App.get_loop_component_start_time(); return cond; } @@ -380,7 +380,7 @@ template class WaitUntilAction : public Action, public Co if (this->num_running_ == 0) return; - auto now = millis(); + auto now = App.get_loop_component_start_time(); this->var_queue_.remove_if([&](auto &queued) { auto start = std::get(queued);