[remote_base] Size the receiver lists by the busiest receiver and hoist attach_receiver

This commit is contained in:
J. Nick Koston
2026-09-11 08:41:41 -05:00
parent 798400908e
commit 03409b0818
8 changed files with 86 additions and 40 deletions
+2 -1
View File
@@ -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; }
@@ -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)
+8 -8
View File
@@ -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:
+2 -1
View File
@@ -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)
@@ -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)
+58 -7
View File
@@ -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_))
@@ -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
@@ -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