From 9acc07a97350ca578e002ffcff300f1df01f2e87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Oct 2026 01:48:02 -1000 Subject: [PATCH] [remote_base] Generate transmit_pronto and transmit_rc6 with no C++ class (#20564) --- esphome/components/remote_base/__init__.py | 53 ++++++++++++------- .../components/remote_base/pronto_protocol.h | 13 ----- esphome/components/remote_base/rc6_protocol.h | 19 ------- .../remote_transmitter/config/rc6_pronto.yaml | 28 ++++++++++ .../remote_transmitter/test_rc6_pronto.py | 41 ++++++++++++++ 5 files changed, 104 insertions(+), 50 deletions(-) create mode 100644 tests/component_tests/remote_transmitter/config/rc6_pronto.yaml create mode 100644 tests/component_tests/remote_transmitter/test_rc6_pronto.py diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index b0dc93e3b79..93e26165d6b 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -308,26 +308,38 @@ def register_action( return decorator +ProtocolField = ( + tuple[str, str, SafeExpType] | automation.ApplyField | automation.ApplyCall +) + + +def _protocol_field( + field: ProtocolField, +) -> automation.ApplyField | automation.ApplyCall: + if not isinstance(field, tuple): + return field + key, member, type_ = field + return automation.ApplyField(key, f"data.{member} = {{}}", type_) + + def register_protocol_action( name: str, protocol: str, schema: cv.Schema, - *fields: tuple[str, str, SafeExpType], + *fields: ProtocolField, validate: Callable[[ConfigType], ConfigType] | None = None, ) -> None: """Register ``transmit_`` with no C++ action class. One generated function fills ``Data`` from ``(conf_key, member, type_)`` fields - and sends it; the action is a core ``ApplyAction``. + and sends it; the action is a core ``ApplyAction``. ``ApplyField`` and ``ApplyCall`` pass through + and target the call object, whose protocol data is ``data``. """ action, schema, validator = _transmit_action(name, schema, validate) builder = automation.apply_action_builder( action, schema, - *( - automation.ApplyField(key, f"data.{member} = {{}}", type_) - for key, member, type_ in fields - ), + *(_protocol_field(field) for field in fields), automation.ApplyField((CONF_REPEAT, CONF_TIMES), "set_send_times", cg.uint32), automation.ApplyField( (CONF_REPEAT, CONF_WAIT_TIME), "set_send_wait", cg.uint32 @@ -1206,7 +1218,7 @@ register_protocol_action( ProntoData, ProntoBinarySensor, ProntoTrigger, - ProntoAction, + _, ProntoDumper, ) = declare_protocol("Pronto") PRONTO_SCHEMA = cv.Schema( @@ -1240,10 +1252,10 @@ def pronto_dumper(var, config): pass -@register_action("pronto", ProntoAction, PRONTO_SCHEMA) -async def pronto_action(var, config, args): - template_ = await cg.templatable(config[CONF_DATA], args, cg.std_string) - cg.add(var.set_data(template_)) +# delta only matters when matching a received code +register_protocol_action( + "pronto", "Pronto", PRONTO_SCHEMA, (CONF_DATA, "data", cg.std_string) +) # Gobox @@ -1640,7 +1652,7 @@ register_protocol_action( # RC6 -RC6Data, RC6BinarySensor, RC6Trigger, RC6Action, RC6Dumper = declare_protocol("RC6") +RC6Data, RC6BinarySensor, RC6Trigger, _, RC6Dumper = declare_protocol("RC6") RC6_SCHEMA = cv.Schema( { cv.Required(CONF_ADDRESS): cv.hex_uint8_t, @@ -1674,12 +1686,17 @@ def rc6_dumper(var, config): pass -@register_action("rc6", RC6Action, RC6_SCHEMA) -async def rc6_action(var, config, args): - template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) - cg.add(var.set_address(template_)) - template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) - cg.add(var.set_command(template_)) +register_protocol_action( + "rc6", + "RC6", + RC6_SCHEMA, + (CONF_ADDRESS, "address", cg.uint8), + (CONF_COMMAND, "command", cg.uint8), + # Each action flips its own toggle bit, starting at 0: the lambda's static is per action + automation.ApplyCall( + "data.toggle = []() {{ static uint8_t toggle; return toggle++; }}()" + ), +) # RC Switch Raw diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h index bfd04c5cd9a..4119b87f286 100644 --- a/esphome/components/remote_base/pronto_protocol.h +++ b/esphome/components/remote_base/pronto_protocol.h @@ -37,17 +37,4 @@ class ProntoProtocol : public RemoteProtocol { DECLARE_REMOTE_PROTOCOL(Pronto) -template class ProntoAction : public RemoteTransmitterActionBase { - public: - TEMPLATABLE_VALUE(std::string, data) - TEMPLATABLE_VALUE(int, delta) - - void encode(RemoteTransmitData *dst, Ts... x) override { - ProntoData data{}; - data.data = this->data_.value(x...); - data.delta = this->delta_.value(x...); - ProntoProtocol().encode(dst, data); - } -}; - } // namespace esphome::remote_base diff --git a/esphome/components/remote_base/rc6_protocol.h b/esphome/components/remote_base/rc6_protocol.h index c4a2e8529bb..cdeb8dfec76 100644 --- a/esphome/components/remote_base/rc6_protocol.h +++ b/esphome/components/remote_base/rc6_protocol.h @@ -22,23 +22,4 @@ class RC6Protocol : public RemoteProtocol { DECLARE_REMOTE_PROTOCOL(RC6) -template class RC6Action : public RemoteTransmitterActionBase { - public: - TEMPLATABLE_VALUE(uint8_t, address) - TEMPLATABLE_VALUE(uint8_t, command) - - void encode(RemoteTransmitData *dst, Ts... x) { - RC6Data data{}; - data.mode = 0; - data.toggle = this->toggle_; - data.address = this->address_.value(x...); - data.command = this->command_.value(x...); - RC6Protocol().encode(dst, data); - this->toggle_ = !this->toggle_; - } - - protected: - uint8_t toggle_{0}; -}; - } // namespace esphome::remote_base diff --git a/tests/component_tests/remote_transmitter/config/rc6_pronto.yaml b/tests/component_tests/remote_transmitter/config/rc6_pronto.yaml new file mode 100644 index 00000000000..a91040ebaba --- /dev/null +++ b/tests/component_tests/remote_transmitter/config/rc6_pronto.yaml @@ -0,0 +1,28 @@ +esphome: + name: test + +esp8266: + board: esp01_1m + +remote_transmitter: + pin: GPIO4 + carrier_duty_percent: 50% + +button: + - platform: template + name: RC6 one + on_press: + - remote_transmitter.transmit_rc6: + address: 0x00 + command: 0x0C + - platform: template + name: RC6 two + on_press: + - remote_transmitter.transmit_rc6: + address: 0x00 + command: 0x0D + - platform: template + name: Pronto + on_press: + - remote_transmitter.transmit_pronto: + data: "0000 006D 0001 0000 0015 0016" diff --git a/tests/component_tests/remote_transmitter/test_rc6_pronto.py b/tests/component_tests/remote_transmitter/test_rc6_pronto.py new file mode 100644 index 00000000000..ba68f5a48bf --- /dev/null +++ b/tests/component_tests/remote_transmitter/test_rc6_pronto.py @@ -0,0 +1,41 @@ +"""transmit_rc6 keeps a toggle bit per action, and transmit_pronto sends its string.""" + +from collections.abc import Callable +from pathlib import Path + +from esphome import automation +import esphome.codegen as cg +from esphome.components.remote_base import _protocol_field +from esphome.const import CONF_TEMPERATURE +from tests.component_tests.remote_transmitter import protocol_call_bodies + + +def test_rc6_toggle_and_pronto( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("rc6_pronto.yaml")) + bodies = protocol_call_bodies(main_cpp) + assert len(bodies) == 3 + toggle = ( + "apply_call.data.toggle = []() { static uint8_t toggle; return toggle++; }();" + ) + # Each RC6 action has its own generated function, so its own toggle static. + assert toggle in bodies[0] + assert toggle in bodies[1] + assert ( + 'apply_call.data.data = progmem_string(ESPHOME_F("0000 006D 0001 0000 0015 0016"));' + in bodies[2] + ) + assert "delta" not in bodies[2] + + +def test_apply_objects_pass_through() -> None: + call = automation.ApplyCall("stop()") + assert _protocol_field(call) is call + field = automation.ApplyField( + CONF_TEMPERATURE, "data.set_temperature({})", cg.float_ + ) + assert _protocol_field(field) is field + plain = _protocol_field((CONF_TEMPERATURE, "temperature", cg.float_)) + assert plain.target == "data.temperature = {}"