diff --git a/AGENTS.md b/AGENTS.md index 98bdd58ec52..8db3cd3d624 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -629,6 +629,9 @@ file does, and it is the authority when they disagree. The most useful starting _request_listener_slot() cg.add(hub.register_listener(var)) ``` + When several instances each own a list declared at the same size (one per hub of a + `MULTI_CONF` component), pass the owning object as the key, `_request_listener_slot(str(hub))`; + the define is then the largest count any one key requested instead of the total. ```cpp #ifdef MY_COMPONENT_LISTENER_COUNT void register_listener(MyComponentListener *listener); diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 3eb8dbe2f41..fcca8b89dba 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import climate_ir +from esphome.components import climate_ir, remote_base from esphome.types import ConfigType AUTO_LOAD = ["climate_ir"] @@ -12,4 +12,5 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate) async def to_code(config: ConfigType) -> None: + remote_base.request_protocol("coolix") # used from C++ await climate_ir.new_climate_ir(config) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 5a909738c6f..83039a5a9bf 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -59,11 +59,6 @@ void Infrared::setup() { // Set up traits based on configuration this->traits_.set_supports_transmitter(this->has_transmitter()); this->traits_.set_supports_receiver(this->has_receiver()); - - // Register as listener for received IR data - if (this->receiver_ != nullptr) { - this->receiver_->register_listener(this); - } } void Infrared::dump_config() { diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index b6863e37ce5..afbde57be2f 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/infrared.py b/esphome/components/ir_rf_proxy/infrared.py index 3218889721c..288bd916738 100644 --- a/esphome/components/ir_rf_proxy/infrared.py +++ b/esphome/components/ir_rf_proxy/infrared.py @@ -3,7 +3,12 @@ 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 @@ -82,8 +87,7 @@ async def to_code(config: dict[str, Any]) -> None: # Link receiver if specified if CONF_REMOTE_RECEIVER_ID in config: - receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) - cg.add(var.set_receiver(receiver)) + 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.cpp b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp index c13c6198cb6..ceb4c9a67c3 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp @@ -97,10 +97,6 @@ void RfProxy::setup() { // remote_transmitter/receiver always uses OOK (on-off keying) this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK); - - if (this->receiver_ != nullptr) { - this->receiver_->register_listener(this); - } } void RfProxy::dump_config() { diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index 5fc683354ba..1aa4394fe84 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 a243909837f..28b8fd5953a 100644 --- a/esphome/components/ir_rf_proxy/radio_frequency.py +++ b/esphome/components/ir_rf_proxy/radio_frequency.py @@ -1,7 +1,12 @@ """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 @@ -66,5 +71,4 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_transmitter(transmitter)) if CONF_REMOTE_RECEIVER_ID in config: - receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) - cg.add(var.set_receiver(receiver)) + await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID) diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 0e03bca2336..07ad02d3afc 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import climate, remote_transmitter, sensor, uart +from esphome.components import climate, remote_base, remote_transmitter, sensor, uart from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode from esphome.components.remote_base import CONF_TRANSMITTER_ID import esphome.config_validation as cv @@ -280,6 +280,7 @@ async def to_code(config): cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds)) cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS])) if CONF_TRANSMITTER_ID in config: + remote_base.request_protocol("midea") # ir_transmitter.h uses it from C++ cg.add_define("USE_REMOTE_TRANSMITTER") transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) cg.add(var.set_transmitter(transmitter_)) diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py index 84bfeab0d46..e1b2b56ada4 100644 --- a/esphome/components/midea_ir/climate.py +++ b/esphome/components/midea_ir/climate.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import climate_ir +from esphome.components import climate_ir, remote_base import esphome.config_validation as cv from esphome.const import CONF_USE_FAHRENHEIT from esphome.types import ConfigType @@ -19,5 +19,9 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend( async def to_code(config: ConfigType) -> None: + # midea_ir uses MideaProtocol from C++ and auto-loads coolix, whose coolix.cpp uses + # CoolixProtocol even when no coolix climate is configured + remote_base.request_protocol("midea") + remote_base.request_protocol("coolix") var = await climate_ir.new_climate_ir(config) cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 19b8549f75a..27b6eb9fc82 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1,6 +1,11 @@ +from collections.abc import Callable +from pathlib import Path +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.components import binary_sensor +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -40,11 +45,14 @@ from esphome.const import ( CONF_ZERO, ) from esphome.core import ID, coroutine +from esphome.cpp_generator import MockObj from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome.types import ConfigType from esphome.util import Registry, SimpleRegistry AUTO_LOAD = ["binary_sensor"] + CONF_RECEIVER_ID = "receiver_id" CONF_TRANSMITTER_ID = "transmitter_id" CONF_FIRST = "first" @@ -90,9 +98,42 @@ REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema( ) -async def register_listener(var, config): +# 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 slots are keyed by receiver and the define is the largest count any one receiver needs. +LISTENER_COUNT_DEFINE = "REMOTE_BASE_LISTENER_COUNT" +DUMPER_COUNT_DEFINE = "REMOTE_BASE_DUMPER_COUNT" + + +_request_listener_slot = cg.slot_counter(LISTENER_COUNT_DEFINE) +_request_dumper_slot = cg.slot_counter(DUMPER_COUNT_DEFINE) + + +def add_listener(receiver: MockObj, listener: MockObj) -> None: + _request_listener_slot(str(receiver)) + cg.add(receiver.register_listener(listener)) + + +def add_dumper(receiver: MockObj, dumper: MockObj) -> None: + _request_dumper_slot(str(receiver)) + cg.add(receiver.register_dumper(dumper)) + + +async def register_listener(var: MockObj, config: ConfigType) -> None: receiver = await cg.get_variable(config[CONF_RECEIVER_ID]) - cg.add(receiver.register_listener(var)) + 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): @@ -100,8 +141,53 @@ async def register_transmittable(var, config): cg.add(var.set_transmitter(transmitter_)) -def register_binary_sensor(name, type, schema): - return BINARY_SENSOR_REGISTRY.register(name, type, schema) +# Registry names that share a protocol source file +def _protocol_stem(name: str) -> str: + if name.startswith("rc_switch"): + return "rc_switch" + if name == "canalsatld": + return "canalsat" + return name + + +def protocol_define(name: str) -> str: + return f"USE_REMOTE_PROTOCOL_{_protocol_stem(name).upper()}" + + +_PROTOCOL_STEMS = sorted( + path.name.removesuffix("_protocol.cpp") + for path in Path(__file__).parent.glob("*_protocol.cpp") +) + + +def request_protocol(name: str) -> None: + """Keep a protocol's source file in the build; components using it from C++ must call this.""" + if _protocol_stem(name) not in _PROTOCOL_STEMS: + raise ValueError( + f"Unknown remote protocol {name!r}; expected one of {', '.join(_PROTOCOL_STEMS)}" + ) + cg.add_define(protocol_define(name)) + + +# Only the protocol sources a configuration uses are compiled +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {f"{stem}_protocol.cpp": protocol_define(stem) for stem in _PROTOCOL_STEMS} +) + + +def register_binary_sensor( + name: str, type: MockObj, schema: cv.Schema | dict +) -> Callable[[Callable[[MockObj, ConfigType], Any]], Callable]: + registerer = BINARY_SENSOR_REGISTRY.register(name, type, schema) + + def decorator(func: Callable[[MockObj, ConfigType], Any]) -> Callable: + async def new_func(var: MockObj, config: ConfigType) -> None: + request_protocol(name) + await coroutine(func)(var, config) + + return registerer(new_func) + + return decorator def register_trigger(name, type, data_type): @@ -114,6 +200,7 @@ def register_trigger(name, type, data_type): def decorator(func): async def new_func(config): + request_protocol(name) var = cg.new_Pvariable(config[CONF_TRIGGER_ID]) await coroutine(func)(var, config) await automation.build_automation(var, [(data_type, "x")], config) @@ -131,6 +218,7 @@ def register_dumper(name, type, schema=None): def decorator(func): async def new_func(config, dumper_id): + request_protocol(name) var = cg.new_Pvariable(dumper_id) await coroutine(func)(var, config) return var @@ -171,6 +259,7 @@ def register_action(name, type_, schema): def decorator(func): async def new_func(config, action_id, template_arg, args): + request_protocol(name) var = cg.new_Pvariable(action_id, template_arg) await register_transmittable(var, config) if CONF_REPEAT in config: @@ -213,7 +302,13 @@ DUMPER_REGISTRY = Registry() def validate_dumpers(value): if isinstance(value, str) and value.lower() == "all": return validate_dumpers(list(DUMPER_REGISTRY.keys())) - return cv.validate_registry("dumper", DUMPER_REGISTRY)(value) + entries = cv.validate_registry("dumper", DUMPER_REGISTRY)(value) + # a dumper listed twice would register twice; the receiver holds one secondary dumper + return list( + { + next(k for k in entry if k in DUMPER_REGISTRY): entry for entry in entries + }.values() + ) def validate_triggers(base_schema): @@ -1439,7 +1534,7 @@ def validate_rc_switch_raw_code(value): def build_rc_switch_protocol(config): if isinstance(config, int): - return rc_switch_protocols[config] + return rc_switch_protocol(config) pl = config[CONF_PULSE_LENGTH] return RCSwitchBase( config[CONF_SYNC][0] * pl, @@ -1526,7 +1621,7 @@ RC_SWITCH_TRANSMITTER = cv.Schema( } ) -rc_switch_protocols = ns.RC_SWITCH_PROTOCOLS +rc_switch_protocol = ns.rc_switch_protocol RCSwitchData = ns.struct("RCSwitchData") RCSwitchBase = ns.class_("RCSwitchBase") RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger) diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 7ff32923bef..a309c124eed 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -191,9 +191,9 @@ class ABBWelcomeData { class ABBWelcomeProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const ABBWelcomeData &src) override; - optional decode(RemoteReceiveData src) override; - void dump(const ABBWelcomeData &data) override; + void encode(RemoteTransmitData *dst, const ABBWelcomeData &src); + optional decode(RemoteReceiveData src); + void dump(const ABBWelcomeData &data); protected: void encode_byte_(RemoteTransmitData *dst, uint8_t data) const; diff --git a/esphome/components/remote_base/aeha_protocol.h b/esphome/components/remote_base/aeha_protocol.h index 3f4e98bd438..98a55011552 100644 --- a/esphome/components/remote_base/aeha_protocol.h +++ b/esphome/components/remote_base/aeha_protocol.h @@ -15,9 +15,9 @@ struct AEHAData { class AEHAProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const AEHAData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const AEHAData &data) override; + void encode(RemoteTransmitData *dst, const AEHAData &data); + optional decode(RemoteReceiveData src); + void dump(const AEHAData &data); private: std::string format_data_(const std::vector &data); diff --git a/esphome/components/remote_base/beo4_protocol.h b/esphome/components/remote_base/beo4_protocol.h index 30b99dbeb77..ed9d6aa6712 100644 --- a/esphome/components/remote_base/beo4_protocol.h +++ b/esphome/components/remote_base/beo4_protocol.h @@ -16,9 +16,9 @@ struct Beo4Data { class Beo4Protocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const Beo4Data &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const Beo4Data &data) override; + void encode(RemoteTransmitData *dst, const Beo4Data &data); + optional decode(RemoteReceiveData src); + void dump(const Beo4Data &data); }; DECLARE_REMOTE_PROTOCOL(Beo4) diff --git a/esphome/components/remote_base/brennenstuhl_protocol.h b/esphome/components/remote_base/brennenstuhl_protocol.h index 1d5b6217147..bfea463b7d4 100644 --- a/esphome/components/remote_base/brennenstuhl_protocol.h +++ b/esphome/components/remote_base/brennenstuhl_protocol.h @@ -13,9 +13,9 @@ struct BrennenstuhlData { class BrennenstuhlProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const BrennenstuhlData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const BrennenstuhlData &data) override; + void encode(RemoteTransmitData *dst, const BrennenstuhlData &data); + optional decode(RemoteReceiveData src); + void dump(const BrennenstuhlData &data); }; DECLARE_REMOTE_PROTOCOL(Brennenstuhl) diff --git a/esphome/components/remote_base/byronsx_protocol.h b/esphome/components/remote_base/byronsx_protocol.h index 674fa99ea10..c71390c267c 100644 --- a/esphome/components/remote_base/byronsx_protocol.h +++ b/esphome/components/remote_base/byronsx_protocol.h @@ -21,9 +21,9 @@ struct ByronSXData { class ByronSXProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const ByronSXData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const ByronSXData &data) override; + void encode(RemoteTransmitData *dst, const ByronSXData &data); + optional decode(RemoteReceiveData src); + void dump(const ByronSXData &data); }; DECLARE_REMOTE_PROTOCOL(ByronSX) diff --git a/esphome/components/remote_base/canalsat_protocol.h b/esphome/components/remote_base/canalsat_protocol.h index 5ba9115ea86..09bead18b3c 100644 --- a/esphome/components/remote_base/canalsat_protocol.h +++ b/esphome/components/remote_base/canalsat_protocol.h @@ -19,9 +19,9 @@ struct CanalSatLDData : public CanalSatData {}; class CanalSatBaseProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const CanalSatData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const CanalSatData &data) override; + void encode(RemoteTransmitData *dst, const CanalSatData &data); + optional decode(RemoteReceiveData src); + void dump(const CanalSatData &data); protected: uint16_t frequency_; diff --git a/esphome/components/remote_base/coolix_protocol.h b/esphome/components/remote_base/coolix_protocol.h index d9441e84178..29a306ce291 100644 --- a/esphome/components/remote_base/coolix_protocol.h +++ b/esphome/components/remote_base/coolix_protocol.h @@ -21,9 +21,9 @@ struct CoolixData { class CoolixProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const CoolixData &data) override; - optional decode(RemoteReceiveData data) override; - void dump(const CoolixData &data) override; + void encode(RemoteTransmitData *dst, const CoolixData &data); + optional decode(RemoteReceiveData data); + void dump(const CoolixData &data); }; DECLARE_REMOTE_PROTOCOL(Coolix) diff --git a/esphome/components/remote_base/dish_protocol.h b/esphome/components/remote_base/dish_protocol.h index c89f4e78e11..f319b55f432 100644 --- a/esphome/components/remote_base/dish_protocol.h +++ b/esphome/components/remote_base/dish_protocol.h @@ -13,9 +13,9 @@ struct DishData { class DishProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const DishData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const DishData &data) override; + void encode(RemoteTransmitData *dst, const DishData &data); + optional decode(RemoteReceiveData src); + void dump(const DishData &data); }; DECLARE_REMOTE_PROTOCOL(Dish) diff --git a/esphome/components/remote_base/dooya_protocol.h b/esphome/components/remote_base/dooya_protocol.h index 148c7c17bc8..954c3cf1d38 100644 --- a/esphome/components/remote_base/dooya_protocol.h +++ b/esphome/components/remote_base/dooya_protocol.h @@ -20,9 +20,9 @@ struct DooyaData { class DooyaProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const DooyaData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const DooyaData &data) override; + void encode(RemoteTransmitData *dst, const DooyaData &data); + optional decode(RemoteReceiveData src); + void dump(const DooyaData &data); }; DECLARE_REMOTE_PROTOCOL(Dooya) diff --git a/esphome/components/remote_base/drayton_protocol.h b/esphome/components/remote_base/drayton_protocol.h index 693a1bbe85b..4e879f0f75b 100644 --- a/esphome/components/remote_base/drayton_protocol.h +++ b/esphome/components/remote_base/drayton_protocol.h @@ -19,9 +19,9 @@ struct DraytonData { class DraytonProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const DraytonData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const DraytonData &data) override; + void encode(RemoteTransmitData *dst, const DraytonData &data); + optional decode(RemoteReceiveData src); + void dump(const DraytonData &data); }; DECLARE_REMOTE_PROTOCOL(Drayton) diff --git a/esphome/components/remote_base/dyson_protocol.h b/esphome/components/remote_base/dyson_protocol.h index 3473a489b2c..663e50fb4b5 100644 --- a/esphome/components/remote_base/dyson_protocol.h +++ b/esphome/components/remote_base/dyson_protocol.h @@ -21,9 +21,9 @@ struct DysonData { class DysonProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const DysonData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const DysonData &data) override; + void encode(RemoteTransmitData *dst, const DysonData &data); + optional decode(RemoteReceiveData src); + void dump(const DysonData &data); }; DECLARE_REMOTE_PROTOCOL(Dyson) diff --git a/esphome/components/remote_base/gobox_protocol.h b/esphome/components/remote_base/gobox_protocol.h index f6b278771e0..0c8797af70c 100644 --- a/esphome/components/remote_base/gobox_protocol.h +++ b/esphome/components/remote_base/gobox_protocol.h @@ -31,9 +31,9 @@ class GoboxProtocol : public RemoteProtocol { void dump_timings_(const RawTimings &timings) const; public: - void encode(RemoteTransmitData *dst, const GoboxData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const GoboxData &data) override; + void encode(RemoteTransmitData *dst, const GoboxData &data); + optional decode(RemoteReceiveData src); + void dump(const GoboxData &data); }; DECLARE_REMOTE_PROTOCOL(Gobox) diff --git a/esphome/components/remote_base/haier_protocol.h b/esphome/components/remote_base/haier_protocol.h index 9c45ba1a635..e1fd60411fc 100644 --- a/esphome/components/remote_base/haier_protocol.h +++ b/esphome/components/remote_base/haier_protocol.h @@ -13,9 +13,9 @@ struct HaierData { class HaierProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const HaierData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const HaierData &data) override; + void encode(RemoteTransmitData *dst, const HaierData &data); + optional decode(RemoteReceiveData src); + void dump(const HaierData &data); protected: void encode_byte_(RemoteTransmitData *dst, uint8_t item); diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index f6e2548dead..5911664fc39 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -14,9 +14,9 @@ struct JVCData { class JVCProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const JVCData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const JVCData &data) override; + void encode(RemoteTransmitData *dst, const JVCData &data); + optional decode(RemoteReceiveData src); + void dump(const JVCData &data); }; DECLARE_REMOTE_PROTOCOL(JVC) diff --git a/esphome/components/remote_base/keeloq_protocol.h b/esphome/components/remote_base/keeloq_protocol.h index 432313b87b2..335fbd164b1 100644 --- a/esphome/components/remote_base/keeloq_protocol.h +++ b/esphome/components/remote_base/keeloq_protocol.h @@ -24,9 +24,9 @@ struct KeeloqData { class KeeloqProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const KeeloqData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const KeeloqData &data) override; + void encode(RemoteTransmitData *dst, const KeeloqData &data); + optional decode(RemoteReceiveData src); + void dump(const KeeloqData &data); }; DECLARE_REMOTE_PROTOCOL(Keeloq) diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index 97159749956..91dfbadb0c2 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -16,9 +16,9 @@ struct LGData { class LGProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const LGData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const LGData &data) override; + void encode(RemoteTransmitData *dst, const LGData &data); + optional decode(RemoteReceiveData src); + void dump(const LGData &data); }; DECLARE_REMOTE_PROTOCOL(LG) diff --git a/esphome/components/remote_base/magiquest_protocol.h b/esphome/components/remote_base/magiquest_protocol.h index 18662ec7598..f0d2410fe27 100644 --- a/esphome/components/remote_base/magiquest_protocol.h +++ b/esphome/components/remote_base/magiquest_protocol.h @@ -27,9 +27,9 @@ struct MagiQuestData { class MagiQuestProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const MagiQuestData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const MagiQuestData &data) override; + void encode(RemoteTransmitData *dst, const MagiQuestData &data); + optional decode(RemoteReceiveData src); + void dump(const MagiQuestData &data); }; DECLARE_REMOTE_PROTOCOL(MagiQuest) diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 47bad6826fc..85bbef1cb1f 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -67,9 +67,9 @@ class MideaData { class MideaProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const MideaData &src) override; - optional decode(RemoteReceiveData src) override; - void dump(const MideaData &data) override; + void encode(RemoteTransmitData *dst, const MideaData &src); + optional decode(RemoteReceiveData src); + void dump(const MideaData &data); }; DECLARE_REMOTE_PROTOCOL(Midea) diff --git a/esphome/components/remote_base/mirage_protocol.h b/esphome/components/remote_base/mirage_protocol.h index c967e72f134..a37fb93f4fd 100644 --- a/esphome/components/remote_base/mirage_protocol.h +++ b/esphome/components/remote_base/mirage_protocol.h @@ -13,9 +13,9 @@ struct MirageData { class MirageProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const MirageData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const MirageData &data) override; + void encode(RemoteTransmitData *dst, const MirageData &data); + optional decode(RemoteReceiveData src); + void dump(const MirageData &data); protected: void encode_byte_(RemoteTransmitData *dst, uint8_t item); diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index 7b310e8ba5b..1337f7a8b32 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -14,9 +14,9 @@ struct NECData { class NECProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const NECData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const NECData &data) override; + void encode(RemoteTransmitData *dst, const NECData &data); + optional decode(RemoteReceiveData src); + void dump(const NECData &data); }; DECLARE_REMOTE_PROTOCOL(NEC) diff --git a/esphome/components/remote_base/nexa_protocol.h b/esphome/components/remote_base/nexa_protocol.h index ebcd2a2c113..ebf85387b04 100644 --- a/esphome/components/remote_base/nexa_protocol.h +++ b/esphome/components/remote_base/nexa_protocol.h @@ -24,9 +24,9 @@ class NexaProtocol : public RemoteProtocol { void zero(RemoteTransmitData *dst) const; void sync(RemoteTransmitData *dst) const; - void encode(RemoteTransmitData *dst, const NexaData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const NexaData &data) override; + void encode(RemoteTransmitData *dst, const NexaData &data); + optional decode(RemoteReceiveData src); + void dump(const NexaData &data); }; DECLARE_REMOTE_PROTOCOL(Nexa) diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index d13c0f27985..84df3c08b72 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -16,9 +16,9 @@ struct PanasonicData { class PanasonicProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const PanasonicData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const PanasonicData &data) override; + void encode(RemoteTransmitData *dst, const PanasonicData &data); + optional decode(RemoteReceiveData src); + void dump(const PanasonicData &data); }; DECLARE_REMOTE_PROTOCOL(Panasonic) diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h index 514ab675016..d02bd3451f2 100644 --- a/esphome/components/remote_base/pioneer_protocol.h +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -13,9 +13,9 @@ struct PioneerData { class PioneerProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const PioneerData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const PioneerData &data) override; + void encode(RemoteTransmitData *dst, const PioneerData &data); + optional decode(RemoteReceiveData src); + void dump(const PioneerData &data); }; DECLARE_REMOTE_PROTOCOL(Pioneer) diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h index f4f6b2144d9..bfd04c5cd9a 100644 --- a/esphome/components/remote_base/pronto_protocol.h +++ b/esphome/components/remote_base/pronto_protocol.h @@ -30,9 +30,9 @@ class ProntoProtocol : public RemoteProtocol { std::string compensate_and_dump_sequence_(const RawTimings &data, uint16_t timebase); public: - void encode(RemoteTransmitData *dst, const ProntoData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const ProntoData &data) override; + void encode(RemoteTransmitData *dst, const ProntoData &data); + optional decode(RemoteReceiveData src); + void dump(const ProntoData &data); }; DECLARE_REMOTE_PROTOCOL(Pronto) diff --git a/esphome/components/remote_base/rc5_protocol.h b/esphome/components/remote_base/rc5_protocol.h index dbb89e41c60..f6f0f33c6e2 100644 --- a/esphome/components/remote_base/rc5_protocol.h +++ b/esphome/components/remote_base/rc5_protocol.h @@ -14,9 +14,9 @@ struct RC5Data { class RC5Protocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const RC5Data &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const RC5Data &data) override; + void encode(RemoteTransmitData *dst, const RC5Data &data); + optional decode(RemoteReceiveData src); + void dump(const RC5Data &data); }; DECLARE_REMOTE_PROTOCOL(RC5) diff --git a/esphome/components/remote_base/rc6_protocol.h b/esphome/components/remote_base/rc6_protocol.h index fda9d98ecbb..c4a2e8529bb 100644 --- a/esphome/components/remote_base/rc6_protocol.h +++ b/esphome/components/remote_base/rc6_protocol.h @@ -15,9 +15,9 @@ struct RC6Data { class RC6Protocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const RC6Data &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const RC6Data &data) override; + void encode(RemoteTransmitData *dst, const RC6Data &data); + optional decode(RemoteReceiveData src); + void dump(const RC6Data &data); }; DECLARE_REMOTE_PROTOCOL(RC6) diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 612558ca1c9..de16c55cb02 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -1,29 +1,21 @@ #include "rc_switch_protocol.h" + +#include +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome::remote_base { static const char *const TAG = "remote.rc_switch"; -const RCSwitchBase RC_SWITCH_PROTOCOLS[9] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false), - RCSwitchBase(350, 10850, 350, 1050, 1050, 350, false), - RCSwitchBase(650, 6500, 650, 1300, 1300, 650, false), - RCSwitchBase(3000, 7100, 400, 1100, 900, 600, false), - RCSwitchBase(380, 2280, 380, 1140, 1140, 380, false), - RCSwitchBase(3000, 7000, 500, 1000, 1000, 500, false), - RCSwitchBase(10350, 450, 450, 900, 900, 450, true), - RCSwitchBase(300, 9300, 150, 900, 900, 150, false), - RCSwitchBase(250, 2500, 250, 1250, 250, 250, false)}; - -RCSwitchBase::RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, - uint32_t one_high, uint32_t one_low, bool inverted) - : sync_high_(sync_high), - sync_low_(sync_low), - zero_high_(zero_high), - zero_low_(zero_low), - one_high_(one_high), - one_low_(one_low), - inverted_(inverted) {} +RCSwitchBase rc_switch_protocol(uint8_t index) { + RCSwitchBase protocol; + // entry 0 is the all-zero protocol, so an out of range index from a lambda transmits nothing + if (index >= std::size(RC_SWITCH_PROTOCOLS)) + index = 0; + progmem_memcpy(&protocol, &RC_SWITCH_PROTOCOLS[index], sizeof(protocol)); + return protocol; +} void RCSwitchBase::one(RemoteTransmitData *dst) const { if (!this->inverted_) { @@ -133,11 +125,11 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *o optional RCSwitchBase::decode(RemoteReceiveData &src) const { RCSwitchData out; uint8_t out_nbits; - for (uint8_t i = 1; i <= 8; i++) { + for (size_t i = 1; i < std::size(RC_SWITCH_PROTOCOLS); i++) { src.reset(); const RCSwitchBase *protocol = &RC_SWITCH_PROTOCOLS[i]; if (protocol->decode(src, &out.code, &out_nbits) && out_nbits >= 3) { - out.protocol = i; + out.protocol = static_cast(i); return out; } } @@ -246,7 +238,7 @@ bool RCSwitchRawReceiver::matches(RemoteReceiveData src) { return decoded_nbits == this->nbits_ && (decoded_code & this->mask_) == (this->code_ & this->mask_); } bool RCSwitchDumper::dump(RemoteReceiveData src) { - for (uint8_t i = 1; i <= 8; i++) { + for (size_t i = 1; i < std::size(RC_SWITCH_PROTOCOLS); i++) { src.reset(); uint64_t out_data; uint8_t out_nbits; @@ -257,7 +249,7 @@ bool RCSwitchDumper::dump(RemoteReceiveData src) { buffer[j] = (out_data & ((uint64_t) 1 << (out_nbits - j - 1))) ? '1' : '0'; buffer[out_nbits] = '\0'; - ESP_LOGI(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", i, buffer); + ESP_LOGI(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", static_cast(i), buffer); // only send first decoded protocol return true; diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 3224c04fb29..9ccea4d15a5 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -16,9 +16,16 @@ class RCSwitchBase { public: using ProtocolData = RCSwitchData; - RCSwitchBase() = default; - RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high, - uint32_t one_low, bool inverted); + constexpr RCSwitchBase() = default; + constexpr RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, + uint32_t one_high, uint32_t one_low, bool inverted) + : sync_high_(sync_high), + sync_low_(sync_low), + zero_high_(zero_high), + zero_low_(zero_low), + one_high_(one_high), + one_low_(one_low), + inverted_(inverted) {} void one(RemoteTransmitData *dst) const; @@ -58,10 +65,28 @@ class RCSwitchBase { uint32_t zero_low_{}; uint32_t one_high_{}; uint32_t one_low_{}; - bool inverted_{}; + uint32_t inverted_{}; // bool widened so every field is a word: the table is read from flash }; -extern const RCSwitchBase RC_SWITCH_PROTOCOLS[9]; +// Constant-initialized and kept in flash on every platform. The decoder reads entries in place +// through a pointer, which ESP8266 only allows while every field is a whole word; copies out of +// the table go through rc_switch_protocol() +static_assert(sizeof(RCSwitchBase) == 7 * sizeof(uint32_t), "RCSwitchBase must stay word-only for flash reads"); +inline constexpr RCSwitchBase RC_SWITCH_PROTOCOLS[] PROGMEM = { + {0, 0, 0, 0, 0, 0, false}, + {350, 10850, 350, 1050, 1050, 350, false}, + {650, 6500, 650, 1300, 1300, 650, false}, + {3000, 7100, 400, 1100, 900, 600, false}, + {380, 2280, 380, 1140, 1140, 380, false}, + {3000, 7000, 500, 1000, 1000, 500, false}, + {10350, 450, 450, 900, 900, 450, true}, + {300, 9300, 150, 900, 900, 150, false}, + {250, 2500, 250, 1250, 250, 250, false}, +}; + +/// RAM copy of RC_SWITCH_PROTOCOLS[index] (0 when out of range) for the transmit actions and the dumper, made with +/// progmem_memcpy so no byte load ever touches the flash table on ESP8266 +RCSwitchBase rc_switch_protocol(uint8_t index); uint64_t decode_binary_string(const std::string &data); diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 4d9bc55f216..5d1bba16b61 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -99,29 +99,48 @@ bool RemoteReceiverBinarySensorBase::on_receive(RemoteReceiveData src) { /* RemoteReceiverBase */ +// Slots are counted at code generation; a registration from C++ setup() has none +#ifdef REMOTE_BASE_LISTENER_COUNT +void RemoteReceiverBase::register_listener(RemoteReceiverListener *listener) { + if (this->listeners_.size() == REMOTE_BASE_LISTENER_COUNT) { + ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("listener"), + LOG_STR_LITERAL("listener")); + return; + } + this->listeners_.push_back(listener); +} +#endif + +#ifdef REMOTE_BASE_DUMPER_COUNT void RemoteReceiverBase::register_dumper(RemoteReceiverDumperBase *dumper) { if (dumper->is_secondary()) { - this->secondary_dumpers_.push_back(dumper); - } else { + if (this->secondary_dumper_ == nullptr) { + this->secondary_dumper_ = dumper; + return; + } + } else if (this->dumpers_.size() != REMOTE_BASE_DUMPER_COUNT) { this->dumpers_.push_back(dumper); + return; } + ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("dumper"), + LOG_STR_LITERAL("dumper")); } +#endif -void RemoteReceiverBase::call_listeners_() { +void RemoteReceiverBase::call_listeners_dumpers_() { +#ifdef REMOTE_BASE_LISTENER_COUNT for (auto *listener : this->listeners_) listener->on_receive(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)); -} - -void RemoteReceiverBase::call_dumpers_() { +#endif +#ifdef REMOTE_BASE_DUMPER_COUNT bool success = false; for (auto *dumper : this->dumpers_) { if (dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_))) success = true; } - if (!success) { - for (auto *dumper : this->secondary_dumpers_) - dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)); - } + if (!success && this->secondary_dumper_ != nullptr) + this->secondary_dumper_->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)); +#endif } void RemoteReceiverBinarySensorBase::dump_config() { LOG_BINARY_SENSOR("", "Remote Receiver Binary Sensor", this); } diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 4e2ed4b71cb..67e5799bcaf 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -1,12 +1,14 @@ +#pragma once + +#include #include #include -#pragma once - #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" namespace esphome::remote_base { @@ -141,6 +143,22 @@ class RemoteRMTChannel { #endif // SOC_RMT_SUPPORTED #endif // USE_ESP32 +// Protocol shapes, checked where a protocol is used so a missing method fails at the use site +// instead of deep inside a template body. Receive-only protocols such as RCSwitchBase decode +// without encoding. +template +concept RemoteProtocolDecoder = requires(T proto, RemoteReceiveData src) { + { proto.decode(src) } -> std::same_as>; +}; +template +concept RemoteProtocolDumper = RemoteProtocolDecoder && requires(T proto, const typename T::ProtocolData &data) { + proto.dump(data); +}; +template +concept RemoteProtocolEncoder = requires(T proto, RemoteTransmitData *dst, const typename T::ProtocolData &data) { + proto.encode(dst, data); +}; + class RemoteTransmitterBase : public RemoteComponentBase { public: RemoteTransmitterBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {} @@ -162,8 +180,8 @@ class RemoteTransmitterBase : public RemoteComponentBase { this->temp_.reset(); return TransmitCall(this); } - template - void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + template + void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { auto call = this->transmit(); Protocol().encode(call.get_data(), data); call.set_send_times(send_times); @@ -194,24 +212,37 @@ class RemoteReceiverDumperBase { class RemoteReceiverBase : public RemoteComponentBase { public: RemoteReceiverBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {} - void register_listener(RemoteReceiverListener *listener) { this->listeners_.push_back(listener); } + // Slots are counted at code generation; without one the call fails at compile time with the same message + // the runtime check logs +#ifdef REMOTE_BASE_LISTENER_COUNT + void register_listener(RemoteReceiverListener *listener); +#else + template void register_listener(T *) { + static_assert(sizeof(T) == 0, "No listener slot: register it from to_code() with remote_base.add_listener"); + } +#endif +#ifdef REMOTE_BASE_DUMPER_COUNT void register_dumper(RemoteReceiverDumperBase *dumper); +#else + template void register_dumper(T *) { + static_assert(sizeof(T) == 0, "No dumper slot: register it from to_code() with remote_base.add_dumper"); + } +#endif void set_tolerance(uint32_t tolerance, ToleranceMode tolerance_mode) { this->tolerance_ = tolerance; this->tolerance_mode_ = tolerance_mode; } protected: - void call_listeners_(); - void call_dumpers_(); - void call_listeners_dumpers_() { - this->call_listeners_(); - this->call_dumpers_(); - } + void call_listeners_dumpers_(); - std::vector listeners_; - std::vector dumpers_; - std::vector secondary_dumpers_; +#ifdef REMOTE_BASE_LISTENER_COUNT + StaticVector listeners_; +#endif +#ifdef REMOTE_BASE_DUMPER_COUNT + StaticVector dumpers_; + RemoteReceiverDumperBase *secondary_dumper_{nullptr}; // runs only when no primary dumper matched +#endif RawTimings temp_; uint32_t tolerance_{25}; ToleranceMode tolerance_mode_{TOLERANCE_MODE_PERCENTAGE}; @@ -229,15 +260,14 @@ class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitial /* TEMPLATES */ +// Protocols are used only through their concrete type (see the RemoteProtocol* concepts); encode/decode/dump +// stay non-virtual so unused ones link out template class RemoteProtocol { public: using ProtocolData = T; - virtual void encode(RemoteTransmitData *dst, const ProtocolData &data) = 0; - virtual optional decode(RemoteReceiveData src) = 0; - virtual void dump(const ProtocolData &data) = 0; }; -template class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase { +template class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase { public: RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {} @@ -255,7 +285,7 @@ template class RemoteReceiverBinarySensor : public RemoteReceiverBin T::ProtocolData data_; }; -template +template class RemoteReceiverTrigger final : public Trigger, public RemoteReceiverListener { protected: bool on_receive(RemoteReceiveData src) override { @@ -276,8 +306,8 @@ class RemoteTransmittable { void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } protected: - template - void transmit_(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + template + void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { this->transmitter_->transmit(data, send_times, send_wait); } RemoteTransmitterBase *transmitter_; @@ -298,7 +328,7 @@ template class RemoteTransmitterActionBase : public RemoteTransm virtual void encode(RemoteTransmitData *dst, Ts... x) = 0; }; -template class RemoteReceiverDumper : public RemoteReceiverDumperBase { +template class RemoteReceiverDumper : public RemoteReceiverDumperBase { public: bool dump(RemoteReceiveData src) override { auto proto = T(); diff --git a/esphome/components/remote_base/roomba_protocol.h b/esphome/components/remote_base/roomba_protocol.h index 3582dac398b..8db025f812a 100644 --- a/esphome/components/remote_base/roomba_protocol.h +++ b/esphome/components/remote_base/roomba_protocol.h @@ -12,9 +12,9 @@ struct RoombaData { class RoombaProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const RoombaData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const RoombaData &data) override; + void encode(RemoteTransmitData *dst, const RoombaData &data); + optional decode(RemoteReceiveData src); + void dump(const RoombaData &data); }; DECLARE_REMOTE_PROTOCOL(Roomba) diff --git a/esphome/components/remote_base/samsung36_protocol.h b/esphome/components/remote_base/samsung36_protocol.h index 4f15d906e76..df4e1af8d8c 100644 --- a/esphome/components/remote_base/samsung36_protocol.h +++ b/esphome/components/remote_base/samsung36_protocol.h @@ -16,9 +16,9 @@ struct Samsung36Data { class Samsung36Protocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const Samsung36Data &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const Samsung36Data &data) override; + void encode(RemoteTransmitData *dst, const Samsung36Data &data); + optional decode(RemoteReceiveData src); + void dump(const Samsung36Data &data); }; DECLARE_REMOTE_PROTOCOL(Samsung36) diff --git a/esphome/components/remote_base/samsung_protocol.h b/esphome/components/remote_base/samsung_protocol.h index bb234d681de..dfa22ff85ce 100644 --- a/esphome/components/remote_base/samsung_protocol.h +++ b/esphome/components/remote_base/samsung_protocol.h @@ -14,9 +14,9 @@ struct SamsungData { class SamsungProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const SamsungData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const SamsungData &data) override; + void encode(RemoteTransmitData *dst, const SamsungData &data); + optional decode(RemoteReceiveData src); + void dump(const SamsungData &data); }; DECLARE_REMOTE_PROTOCOL(Samsung) diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index eb873e8b7dc..f83b2908b61 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -16,9 +16,9 @@ struct SonyData { class SonyProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const SonyData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const SonyData &data) override; + void encode(RemoteTransmitData *dst, const SonyData &data); + optional decode(RemoteReceiveData src); + void dump(const SonyData &data); }; DECLARE_REMOTE_PROTOCOL(Sony) diff --git a/esphome/components/remote_base/symphony_protocol.h b/esphome/components/remote_base/symphony_protocol.h index 7caf5eab867..40a5c2daec9 100644 --- a/esphome/components/remote_base/symphony_protocol.h +++ b/esphome/components/remote_base/symphony_protocol.h @@ -17,9 +17,9 @@ struct SymphonyData { class SymphonyProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const SymphonyData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const SymphonyData &data) override; + void encode(RemoteTransmitData *dst, const SymphonyData &data); + optional decode(RemoteReceiveData src); + void dump(const SymphonyData &data); }; DECLARE_REMOTE_PROTOCOL(Symphony) diff --git a/esphome/components/remote_base/toshiba_ac_protocol.h b/esphome/components/remote_base/toshiba_ac_protocol.h index 8a853005acb..35d5af314cb 100644 --- a/esphome/components/remote_base/toshiba_ac_protocol.h +++ b/esphome/components/remote_base/toshiba_ac_protocol.h @@ -14,9 +14,9 @@ struct ToshibaAcData { class ToshibaAcProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const ToshibaAcData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const ToshibaAcData &data) override; + void encode(RemoteTransmitData *dst, const ToshibaAcData &data); + optional decode(RemoteReceiveData src); + void dump(const ToshibaAcData &data); }; DECLARE_REMOTE_PROTOCOL(ToshibaAc) diff --git a/esphome/components/remote_base/toto_protocol.h b/esphome/components/remote_base/toto_protocol.h index 285c9f21257..8e965a5c739 100644 --- a/esphome/components/remote_base/toto_protocol.h +++ b/esphome/components/remote_base/toto_protocol.h @@ -16,9 +16,9 @@ struct TotoData { class TotoProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const TotoData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const TotoData &data) override; + void encode(RemoteTransmitData *dst, const TotoData &data); + optional decode(RemoteReceiveData src); + void dump(const TotoData &data); }; DECLARE_REMOTE_PROTOCOL(Toto) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 6e8c73d331a..b2fd87165e4 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -221,11 +221,11 @@ async def to_code(config: ConfigType) -> None: dumpers = await remote_base.build_dumpers(config[CONF_DUMP]) for dumper in dumpers: - cg.add(var.register_dumper(dumper)) + remote_base.add_dumper(var, dumper) triggers = await remote_base.build_triggers(config) for trigger in triggers: - cg.add(var.register_listener(trigger)) + remote_base.add_listener(var, trigger) await cg.register_component(var, config) cg.add( diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py index 3b1e7352f98..e5f8544f2fe 100644 --- a/esphome/components/toshiba/climate.py +++ b/esphome/components/toshiba/climate.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import climate_ir +from esphome.components import climate_ir, remote_base import esphome.config_validation as cv from esphome.const import CONF_MODEL from esphome.types import ConfigType @@ -26,5 +26,6 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).exten async def to_code(config: ConfigType) -> None: + remote_base.request_protocol("toshiba_ac") # used from C++ var = await climate_ir.new_climate_ir(config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9144e65576f..6b9b9eda43f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -137,6 +137,43 @@ #define MICRONOVA_LISTENER_COUNT 1 #define USE_MICRONOVA_WRITER #define MK2PVROUTER_LISTENER_COUNT 1 +#define REMOTE_BASE_DUMPER_COUNT 1 +#define REMOTE_BASE_LISTENER_COUNT 1 +#define USE_REMOTE_PROTOCOL_ABBWELCOME +#define USE_REMOTE_PROTOCOL_AEHA +#define USE_REMOTE_PROTOCOL_BEO4 +#define USE_REMOTE_PROTOCOL_BRENNENSTUHL +#define USE_REMOTE_PROTOCOL_BYRONSX +#define USE_REMOTE_PROTOCOL_CANALSAT +#define USE_REMOTE_PROTOCOL_COOLIX +#define USE_REMOTE_PROTOCOL_DISH +#define USE_REMOTE_PROTOCOL_DOOYA +#define USE_REMOTE_PROTOCOL_DRAYTON +#define USE_REMOTE_PROTOCOL_DYSON +#define USE_REMOTE_PROTOCOL_GOBOX +#define USE_REMOTE_PROTOCOL_HAIER +#define USE_REMOTE_PROTOCOL_JVC +#define USE_REMOTE_PROTOCOL_KEELOQ +#define USE_REMOTE_PROTOCOL_LG +#define USE_REMOTE_PROTOCOL_MAGIQUEST +#define USE_REMOTE_PROTOCOL_MIDEA +#define USE_REMOTE_PROTOCOL_MIRAGE +#define USE_REMOTE_PROTOCOL_NEC +#define USE_REMOTE_PROTOCOL_NEXA +#define USE_REMOTE_PROTOCOL_PANASONIC +#define USE_REMOTE_PROTOCOL_PIONEER +#define USE_REMOTE_PROTOCOL_PRONTO +#define USE_REMOTE_PROTOCOL_RAW +#define USE_REMOTE_PROTOCOL_RC5 +#define USE_REMOTE_PROTOCOL_RC6 +#define USE_REMOTE_PROTOCOL_RC_SWITCH +#define USE_REMOTE_PROTOCOL_ROOMBA +#define USE_REMOTE_PROTOCOL_SAMSUNG +#define USE_REMOTE_PROTOCOL_SAMSUNG36 +#define USE_REMOTE_PROTOCOL_SONY +#define USE_REMOTE_PROTOCOL_SYMPHONY +#define USE_REMOTE_PROTOCOL_TOSHIBA_AC +#define USE_REMOTE_PROTOCOL_TOTO #define SERIAL_PROXY_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 53b59cb1240..fc44d27f472 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,4 +1,4 @@ -from collections.abc import Callable +from collections.abc import Callable, Hashable from dataclasses import dataclass, field import logging @@ -142,9 +142,10 @@ _SLOT_COUNTER_DOMAIN = "slot_counter" @dataclass class _SlotCounterState: - """Per-run slot counter state: requested counts and already-emitted defines.""" + """Per-run slot counter state: requested counts per define and key, and + already-emitted defines.""" - counts: dict[str, int] = field(default_factory=dict) + counts: dict[str, dict[Hashable, int]] = field(default_factory=dict) emitted: set[str] = field(default_factory=set) @@ -156,11 +157,13 @@ def _get_slot_counter_state() -> _SlotCounterState: def get_slot_count(define: str) -> int: - """Number of slots requested so far for `define`.""" - return _get_slot_counter_state().counts.get(define, 0) + """Value `define` would be emitted with so far: the largest count requested + under any one key, which is the plain request count when no key is used.""" + counts = _get_slot_counter_state().counts.get(define) + return max(counts.values()) if counts else 0 -def slot_counter(define: str) -> Callable[[], None]: +def slot_counter(define: str) -> Callable[..., None]: """Create a request_slot function for codegen-sized storage. The pattern behind a StaticVector listener array: a consumer's to_code @@ -169,6 +172,11 @@ def slot_counter(define: str) -> Callable[[], None]: emitted with the requested count. No requests, no define: the guarded storage and its registration method compile out entirely. + When several objects each declare the storage at the same size (one list + per receiver, per hub, ...) the caller passes the owning object as `key` + and the define becomes the largest count any one key requested, not the + total. Requests without a key share one count. + The counts live in a table under CORE.data, which clears between runs. A request arriving after the define was already emitted raises instead of silently undercounting: the define would keep the stale smaller value and @@ -179,10 +187,10 @@ def slot_counter(define: str) -> Callable[[], None]: async def emit_job() -> None: state = _get_slot_counter_state() state.emitted.add(define) - # Scheduled only by the first request, so the count is always >= 1 here. - add_define(define, state.counts[define]) + # Scheduled only by the first request, so there is at least one count here. + add_define(define, max(state.counts[define].values())) - def request_slot() -> None: + def request_slot(key: Hashable = None) -> None: state = _get_slot_counter_state() if define in state.emitted: raise ValueError( @@ -190,10 +198,16 @@ def slot_counter(define: str) -> Callable[[], None]: f"define was emitted; request slots from to_code, not from a " f"job running after FINAL emission" ) - counts = state.counts - counts[define] = (count := counts.get(define, 0) + 1) - if count == 1: + counts = state.counts.get(define) + if counts is None: + counts = state.counts[define] = {} CORE.add_job(emit_job) + elif (key is None) != (None in counts): + # a keyed and an unkeyed request would compare buckets instead of adding up + raise ValueError( + f"slot_counter('{define}'): every request must use a key, or none of them" + ) + counts[key] = counts.get(key, 0) + 1 return request_slot diff --git a/tests/component_tests/remote_receiver/__init__.py b/tests/component_tests/remote_receiver/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/remote_receiver/config/receiver_bare.yaml b/tests/component_tests/remote_receiver/config/receiver_bare.yaml new file mode 100644 index 00000000000..b4741948015 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_bare.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + +remote_receiver: + - id: rcvr + pin: GPIO4 diff --git a/tests/component_tests/remote_receiver/config/receiver_with_dumpers.yaml b/tests/component_tests/remote_receiver/config/receiver_with_dumpers.yaml new file mode 100644 index 00000000000..32c1b07f579 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_with_dumpers.yaml @@ -0,0 +1,24 @@ +esphome: + name: test + +esp32: + board: esp32dev + +logger: + +remote_receiver: + - id: rcvr + pin: GPIO4 + dump: + - nec + - rc_switch + on_nec: + then: + - logger.log: nec + +binary_sensor: + - platform: remote_receiver + name: Remote Input + nec: + address: 0x1234 + command: 0x5678 diff --git a/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml b/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml new file mode 100644 index 00000000000..c443a842f23 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml @@ -0,0 +1,22 @@ +esphome: + name: test + +esp32: + board: esp32dev + +remote_receiver: + - id: rcvr_ir + pin: GPIO4 + - id: rcvr_rf + pin: GPIO5 + +infrared: + - platform: ir_rf_proxy + name: IR Receiver + remote_receiver_id: rcvr_ir + +radio_frequency: + - platform: ir_rf_proxy + name: RF Receiver + frequency: 433.92MHz + 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 new file mode 100644 index 00000000000..f381a640929 --- /dev/null +++ b/tests/component_tests/remote_receiver/test_slot_counts.py @@ -0,0 +1,91 @@ +"""Listener and dumper StaticVector sizes come from codegen slot counts.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.automation import ACTION_REGISTRY +from esphome.components import remote_base +import esphome.config_validation as cv + +from ..helpers import get_define_value + + +def test_dumper_and_listener_counts( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("receiver_with_dumpers.yaml")) + # nec and rc_switch dumpers + assert get_define_value("REMOTE_BASE_DUMPER_COUNT") == "2" + # on_nec trigger plus the remote_receiver binary sensor + assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "2" + + +def test_bare_receiver_emits_no_counts( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("receiver_bare.yaml")) + assert get_define_value("REMOTE_BASE_DUMPER_COUNT") is None + assert get_define_value("REMOTE_BASE_LISTENER_COUNT") is None + + +def test_proxy_receivers_count_as_listeners( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("receiver_with_proxies.yaml")) + # 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 + + +def test_only_used_protocol_sources_are_compiled( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("receiver_with_dumpers.yaml")) + excluded = set(remote_base.FILTER_SOURCE_FILES()) + assert "nec_protocol.cpp" not in excluded + assert "rc_switch_protocol.cpp" not in excluded + assert "sony_protocol.cpp" in excluded + assert "remote_base.cpp" not in excluded + + +def test_every_registry_name_maps_to_a_protocol_source() -> None: + """A registry name must resolve to a source file or request_protocol rejects it.""" + names = ( + set(remote_base.BINARY_SENSOR_REGISTRY) + | set(remote_base.DUMPER_REGISTRY) + | {key.removeprefix("on_") for key in remote_base.TRIGGER_REGISTRY} + | { + key.removeprefix("remote_transmitter.transmit_") + for key in ACTION_REGISTRY + if key.startswith("remote_transmitter.transmit_") + } + ) + assert len(names) > 40 + for name in names: + assert remote_base._protocol_stem(name) in remote_base._PROTOCOL_STEMS, name + + +def test_request_protocol_rejects_unknown_names() -> None: + """A misspelled protocol would otherwise surface only as a link error.""" + with pytest.raises(ValueError, match="Unknown remote protocol 'toshiba'"): + remote_base.request_protocol("toshiba") + + +def test_dump_list_is_deduplicated_across_forms() -> None: + dumpers = remote_base.validate_dumpers(["raw", {"raw": None}, "nec", "nec"]) + assert [ + next(k for k in entry if k in remote_base.DUMPER_REGISTRY) for entry in dumpers + ] == ["raw", "nec"] + + +@pytest.mark.parametrize("bad", [["nec", None], [5]]) +def test_dump_list_rejects_invalid_entries_with_a_validation_error(bad: list) -> None: + with pytest.raises(cv.Invalid): + remote_base.validate_dumpers(bad) diff --git a/tests/components/remote_receiver/bare-common.yaml b/tests/components/remote_receiver/bare-common.yaml new file mode 100644 index 00000000000..c100c5c2da4 --- /dev/null +++ b/tests/components/remote_receiver/bare-common.yaml @@ -0,0 +1,6 @@ +# A receiver with no dumpers and no listeners compiles both lists out. +# Only built while remote_receiver is tested in isolation: the counts are global defines, +# so this variant cannot be merged with configs that register any. +remote_receiver: + - id: rcvr_bare + pin: ${pin} diff --git a/tests/components/remote_receiver/test-bare.esp32-idf.yaml b/tests/components/remote_receiver/test-bare.esp32-idf.yaml new file mode 100644 index 00000000000..152853b65fb --- /dev/null +++ b/tests/components/remote_receiver/test-bare.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + pin: GPIO2 + +packages: + bare: !include bare-common.yaml diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 1c0e0d0a931..725c1daebb4 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -187,6 +187,31 @@ def test_slot_counter_emits_requested_count() -> None: assert _define_value("TEST_SLOT_COUNT") == "2" +def test_slot_counter_keyed_emits_largest_count() -> None: + """Keyed requests size storage every key declares at the same capacity: + the define is the busiest key's count, not the total over all keys.""" + request = ch.slot_counter("TEST_SLOT_COUNT_KEYED") + request("rx_a") + request("rx_a") + request("rx_a") + request("rx_b") + assert ch.get_slot_count("TEST_SLOT_COUNT_KEYED") == 3 + ch.CORE.flush_tasks() + assert _define_value("TEST_SLOT_COUNT_KEYED") == "3" + + +def test_slot_counter_rejects_mixed_keyed_and_unkeyed_requests() -> None: + """A keyed and an unkeyed request for one define cannot be sized together.""" + request = ch.slot_counter("TEST_SLOT_COUNT_MIXED") + request("rx_a") + with pytest.raises(ValueError, match="TEST_SLOT_COUNT_MIXED"): + request() + unkeyed = ch.slot_counter("TEST_SLOT_COUNT_MIXED_2") + unkeyed() + with pytest.raises(ValueError, match="TEST_SLOT_COUNT_MIXED_2"): + unkeyed("rx_a") + + def test_slot_counter_without_requests_emits_nothing() -> None: """No requests, no job, no define — the guarded storage compiles out.""" ch.slot_counter("TEST_SLOT_COUNT_UNUSED")