From 03409b0818ebbeacc30e37792009e027ed4e1743 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2026 08:41:41 -0500 Subject: [PATCH] [remote_base] Size the receiver lists by the busiest receiver and hoist attach_receiver --- esphome/components/infrared/infrared.h | 3 +- esphome/components/ir_rf_proxy/__init__.py | 10 --- esphome/components/ir_rf_proxy/infrared.py | 16 ++--- esphome/components/ir_rf_proxy/ir_rf_proxy.h | 3 +- .../components/ir_rf_proxy/radio_frequency.py | 16 ++--- esphome/components/remote_base/__init__.py | 65 +++++++++++++++++-- .../config/receiver_with_proxies.yaml | 8 ++- .../remote_receiver/test_slot_counts.py | 5 +- 8 files changed, 86 insertions(+), 40 deletions(-) diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index b6863e37ce..afbde57be2 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -119,7 +119,8 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } - /// Set the remote receiver component + /// Set the remote receiver component; the listener registration happens from codegen, see + /// remote_base.attach_receiver void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; } /// Set the remote transmitter component void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } diff --git a/esphome/components/ir_rf_proxy/__init__.py b/esphome/components/ir_rf_proxy/__init__.py index 26a4f0d387..bc4079ede7 100644 --- a/esphome/components/ir_rf_proxy/__init__.py +++ b/esphome/components/ir_rf_proxy/__init__.py @@ -1,9 +1,6 @@ """IR/RF Proxy component - provides remote_base backend for infrared platform.""" import esphome.codegen as cg -from esphome.components import remote_base -from esphome.cpp_generator import MockObj -from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] @@ -12,10 +9,3 @@ ir_rf_proxy_ns = cg.esphome_ns.namespace("ir_rf_proxy") CONF_REMOTE_RECEIVER_ID = "remote_receiver_id" CONF_REMOTE_TRANSMITTER_ID = "remote_transmitter_id" - - -async def attach_receiver(var: MockObj, config: ConfigType) -> None: - """Wire the configured remote_receiver to a proxy entity and register it as a listener.""" - receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) - cg.add(var.set_receiver(receiver)) - remote_base.add_listener(receiver, var) diff --git a/esphome/components/ir_rf_proxy/infrared.py b/esphome/components/ir_rf_proxy/infrared.py index 80bc55d5e9..288bd91673 100644 --- a/esphome/components/ir_rf_proxy/infrared.py +++ b/esphome/components/ir_rf_proxy/infrared.py @@ -3,18 +3,18 @@ from typing import Any import esphome.codegen as cg -from esphome.components import infrared, remote_receiver, remote_transmitter +from esphome.components import ( + infrared, + remote_base, + remote_receiver, + remote_transmitter, +) from esphome.components.const import CONF_RECEIVER_FREQUENCY import esphome.config_validation as cv from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY import esphome.final_validate as fv -from . import ( - CONF_REMOTE_RECEIVER_ID, - CONF_REMOTE_TRANSMITTER_ID, - attach_receiver, - ir_rf_proxy_ns, -) +from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns CODEOWNERS = ["@kbx81"] DEPENDENCIES = ["infrared"] @@ -87,7 +87,7 @@ async def to_code(config: dict[str, Any]) -> None: # Link receiver if specified if CONF_REMOTE_RECEIVER_ID in config: - await attach_receiver(var, config) + await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID) # Set receiver demodulation frequency if specified (metadata only, no hardware effect) if CONF_RECEIVER_FREQUENCY in config: diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index 5fc683354b..1aa4394fe8 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.h +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -56,7 +56,8 @@ class RfProxy final : public radio_frequency::RadioFrequency { /// Set the remote transmitter component void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } - /// Set the remote receiver component + /// Set the remote receiver component; the listener registration happens from codegen, see + /// remote_base.attach_receiver void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; } /// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware) diff --git a/esphome/components/ir_rf_proxy/radio_frequency.py b/esphome/components/ir_rf_proxy/radio_frequency.py index c27100c69b..28b8fd5953 100644 --- a/esphome/components/ir_rf_proxy/radio_frequency.py +++ b/esphome/components/ir_rf_proxy/radio_frequency.py @@ -1,18 +1,18 @@ """Radio Frequency platform implementation using remote_base (remote_transmitter/receiver).""" import esphome.codegen as cg -from esphome.components import radio_frequency, remote_receiver, remote_transmitter +from esphome.components import ( + radio_frequency, + remote_base, + remote_receiver, + remote_transmitter, +) import esphome.config_validation as cv from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY import esphome.final_validate as fv from esphome.types import ConfigType -from . import ( - CONF_REMOTE_RECEIVER_ID, - CONF_REMOTE_TRANSMITTER_ID, - attach_receiver, - ir_rf_proxy_ns, -) +from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns CODEOWNERS = ["@kbx81"] DEPENDENCIES = ["radio_frequency"] @@ -71,4 +71,4 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_transmitter(transmitter)) if CONF_REMOTE_RECEIVER_ID in config: - await attach_receiver(var, config) + await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 754b8a4317..8ae2140fe8 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1,4 +1,5 @@ from collections.abc import Callable +from dataclasses import dataclass, field from pathlib import Path from typing import Any @@ -44,7 +45,8 @@ from esphome.const import ( CONF_WAND_ID, CONF_ZERO, ) -from esphome.core import ID, coroutine +from esphome.core import CORE, ID, coroutine, coroutine_with_priority +from esphome.coroutine import CoroPriority from esphome.cpp_generator import MockObj from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import ConfigType @@ -52,6 +54,8 @@ from esphome.util import Registry, SimpleRegistry AUTO_LOAD = ["binary_sensor"] +DOMAIN = "remote_base" + CONF_RECEIVER_ID = "receiver_id" CONF_TRANSMITTER_ID = "transmitter_id" CONF_FIRST = "first" @@ -97,19 +101,54 @@ REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema( ) -# Listener and dumper lists are StaticVectors sized from these counts, so every -# registration must go through add_listener / add_dumper -_request_listener_slot = cg.slot_counter("REMOTE_BASE_LISTENER_COUNT") -_request_dumper_slot = cg.slot_counter("REMOTE_BASE_DUMPER_COUNT") +# Listener and dumper lists are StaticVectors sized from these counts, so every registration +# must go through add_listener / add_dumper. Every receiver's list gets the same capacity, so +# the define is the largest count any one receiver needs, not the sum over all receivers. +LISTENER_COUNT_DEFINE = "REMOTE_BASE_LISTENER_COUNT" +DUMPER_COUNT_DEFINE = "REMOTE_BASE_DUMPER_COUNT" + + +@dataclass +class _SlotCounts: + per_receiver: dict[str, dict[str, int]] = field(default_factory=dict) + emitted: bool = False + + +def _get_slot_counts() -> _SlotCounts: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = _SlotCounts() + return CORE.data[DOMAIN] + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _emit_slot_counts() -> None: + state = _get_slot_counts() + state.emitted = True + for define, counts in state.per_receiver.items(): + cg.add_define(define, max(counts.values())) + + +def _request_slot(define: str, receiver: MockObj) -> None: + state = _get_slot_counts() + if state.emitted: + raise ValueError( + f"{define}: slot requested after the count define was emitted; " + "request slots from to_code, not from a job running after FINAL" + ) + if not state.per_receiver: + CORE.add_job(_emit_slot_counts) + counts = state.per_receiver.setdefault(define, {}) + key = str(receiver) + counts[key] = counts.get(key, 0) + 1 def add_listener(receiver: MockObj, listener: MockObj) -> None: - _request_listener_slot() + _request_slot(LISTENER_COUNT_DEFINE, receiver) cg.add(receiver.register_listener(listener)) def add_dumper(receiver: MockObj, dumper: MockObj) -> None: - _request_dumper_slot() + _request_slot(DUMPER_COUNT_DEFINE, receiver) cg.add(receiver.register_dumper(dumper)) @@ -118,6 +157,18 @@ async def register_listener(var: MockObj, config: ConfigType) -> None: add_listener(receiver, var) +async def attach_receiver( + var: MockObj, config: ConfigType, key: str = CONF_RECEIVER_ID +) -> None: + """Link the configured receiver to an entity and register the entity as its listener. + + The C++ set_receiver() no longer registers the listener; the slot for it is counted here. + """ + receiver = await cg.get_variable(config[key]) + cg.add(var.set_receiver(receiver)) + add_listener(receiver, var) + + async def register_transmittable(var, config): transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) cg.add(var.set_transmitter(transmitter_)) diff --git a/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml b/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml index 66f2d01046..c443a842f2 100644 --- a/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml +++ b/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml @@ -5,16 +5,18 @@ esp32: board: esp32dev remote_receiver: - - id: rcvr + - id: rcvr_ir pin: GPIO4 + - id: rcvr_rf + pin: GPIO5 infrared: - platform: ir_rf_proxy name: IR Receiver - remote_receiver_id: rcvr + remote_receiver_id: rcvr_ir radio_frequency: - platform: ir_rf_proxy name: RF Receiver frequency: 433.92MHz - remote_receiver_id: rcvr + remote_receiver_id: rcvr_rf diff --git a/tests/component_tests/remote_receiver/test_slot_counts.py b/tests/component_tests/remote_receiver/test_slot_counts.py index 55d963fa99..f381a64092 100644 --- a/tests/component_tests/remote_receiver/test_slot_counts.py +++ b/tests/component_tests/remote_receiver/test_slot_counts.py @@ -37,8 +37,9 @@ def test_proxy_receivers_count_as_listeners( component_config_path: Callable[[str], Path], ) -> None: generate_main(component_config_path("receiver_with_proxies.yaml")) - # infrared and radio_frequency ir_rf_proxy platforms each listen - assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "2" + # one proxy entity listens on each of the two receivers; every receiver's list gets the + # capacity of the busiest one, so this is the largest per receiver count, not the sum + assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "1" assert get_define_value("REMOTE_BASE_DUMPER_COUNT") is None