diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp index 60b0cd513b..c13c6198cb 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp @@ -106,7 +106,6 @@ void RfProxy::setup() { void RfProxy::dump_config() { ESP_LOGCONFIG(TAG, "RF Proxy '%s'\n" - " Backend: remote_transmitter/receiver\n" " Supports Transmitter: %s\n" " Supports Receiver: %s", this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()), @@ -124,7 +123,9 @@ void RfProxy::dump_config() { } void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) { - // RF: no IR carrier modulation + // RF: no IR carrier modulation. Any RF front-end coordination (state turnaround, retuning) + // happens via the radio_frequency entity's on_control trigger and remote_transmitter's + // on_transmit/on_complete triggers — wired up in user YAML. transmit_raw_timings(this->transmitter_, 0, call); } diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index 973e9e2051..d0467e822d 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.h +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -43,7 +43,10 @@ class IrRfProxy : public infrared::Infrared { #endif // USE_IR_RF #ifdef USE_RADIO_FREQUENCY -/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend +/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend. +/// Driver-agnostic: integration with specific RF front-end chips (CC1101, RFM69, etc.) is done +/// in YAML by wiring their actions to `remote_transmitter`'s on_transmit/on_complete triggers and +/// to this entity's on_control trigger (see radio_frequency component docs). class RfProxy : public radio_frequency::RadioFrequency { public: RfProxy() = default; diff --git a/esphome/components/ir_rf_proxy/radio_frequency.py b/esphome/components/ir_rf_proxy/radio_frequency.py index 9982f5e4d1..a243909837 100644 --- a/esphome/components/ir_rf_proxy/radio_frequency.py +++ b/esphome/components/ir_rf_proxy/radio_frequency.py @@ -35,17 +35,19 @@ def _final_validate(config: ConfigType) -> None: if CONF_REMOTE_TRANSMITTER_ID not in config: return - transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID] full_config = fv.full_config.get() - transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1] + transmitter_path = full_config.get_path_for_id(config[CONF_REMOTE_TRANSMITTER_ID])[ + :-1 + ] transmitter_config = full_config.get_config_for_path(transmitter_path) duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT) if duty_percent is not None and duty_percent != 100: raise cv.Invalid( - f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' " - "set to 100% for RF transmission. Dedicated RF hardware handles modulation; " - "applying a carrier duty cycle would corrupt the signal" + f"Transmitter '{config[CONF_REMOTE_TRANSMITTER_ID]}' must have " + f"'{CONF_CARRIER_DUTY_PERCENT}' set to 100% for RF transmission. " + "Dedicated RF hardware handles modulation; applying a carrier duty cycle " + "would corrupt the signal" ) diff --git a/esphome/components/radio_frequency/__init__.py b/esphome/components/radio_frequency/__init__.py index a54ab6e249..9fdafe428a 100644 --- a/esphome/components/radio_frequency/__init__.py +++ b/esphome/components/radio_frequency/__init__.py @@ -8,9 +8,10 @@ breaking changes policy. Use at your own risk. Once the API is considered stable, this warning will be removed. """ +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_ON_CONTROL from esphome.core import CORE, coroutine_with_priority from esphome.core.entity_helpers import queue_entity_register, setup_entity from esphome.coroutine import CoroPriority @@ -42,6 +43,7 @@ def radio_frequency_schema(class_: type[cg.MockObjClass]) -> cv.Schema: return entity_schema.extend( { cv.GenerateID(): cv.declare_id(class_), + cv.Optional(CONF_ON_CONTROL): automation.validate_automation({}), } ) @@ -59,6 +61,11 @@ async def register_radio_frequency(var: cg.Pvariable, config: ConfigType) -> Non await setup_radio_frequency_core_(var, config) CORE.register_platform_component("radio_frequency", var) + for conf in config.get(CONF_ON_CONTROL, []): + await automation.build_callback_automation( + var, "add_on_control_callback", [(RadioFrequencyCall, "x")], conf + ) + async def new_radio_frequency(config: ConfigType, *args) -> cg.Pvariable: """Create a new RadioFrequency instance. diff --git a/esphome/components/radio_frequency/radio_frequency.cpp b/esphome/components/radio_frequency/radio_frequency.cpp index 3c000ae1ca..3e0a905737 100644 --- a/esphome/components/radio_frequency/radio_frequency.cpp +++ b/esphome/components/radio_frequency/radio_frequency.cpp @@ -54,6 +54,10 @@ RadioFrequencyCall &RadioFrequencyCall::set_repeat_count(uint32_t count) { void RadioFrequencyCall::perform() { if (this->parent_ != nullptr) { + // Fire any on_control hooks (user-wired automations) before handing off to + // the platform-specific control() — gives users a chance to react to call + // parameters (e.g. retune an external RF front-end based on call.get_frequency()). + this->parent_->control_callback_.call(*this); this->parent_->control(*this); } } diff --git a/esphome/components/radio_frequency/radio_frequency.h b/esphome/components/radio_frequency/radio_frequency.h index db73a844ed..7dfd2dd77e 100644 --- a/esphome/components/radio_frequency/radio_frequency.h +++ b/esphome/components/radio_frequency/radio_frequency.h @@ -170,6 +170,15 @@ class RadioFrequency : public Component, public EntityBase, public remote_base:: this->receive_callback_.add(std::forward(callback)); } + /// Add a callback to invoke when a transmit call is made on this entity. + /// Fires before the platform-specific control() runs, with the call object + /// (containing frequency, modulation, repeat count, etc.). Used by the + /// `on_control` YAML trigger so users can wire any RF front-end driver + /// (CC1101, RFM69, custom) to react to per-call parameters. + template void add_on_control_callback(F &&callback) { + this->control_callback_.add(std::forward(callback)); + } + protected: friend class RadioFrequencyCall; @@ -182,6 +191,8 @@ class RadioFrequency : public Component, public EntityBase, public remote_base:: // Callback manager for receive events (lazy: saves memory when no callbacks registered) LazyCallbackManager receive_callback_; + // Callback manager for on_control trigger (lazy: same memory savings) + LazyCallbackManager control_callback_; }; } // namespace esphome::radio_frequency diff --git a/tests/components/ir_rf_proxy/common-cc1101.yaml b/tests/components/ir_rf_proxy/common-cc1101.yaml new file mode 100644 index 0000000000..392e6db22e --- /dev/null +++ b/tests/components/ir_rf_proxy/common-cc1101.yaml @@ -0,0 +1,50 @@ +cc1101: + id: cc1101_radio + cs_pin: ${cs_pin} + frequency: 433.92MHz + modulation_type: ASK/OOK + output_power: 10 + +# Dual-pin wiring (recommended by the CC1101 docs): +# CC1101 GDO0 → ${gdo0_pin} (remote_transmitter) +# CC1101 GDO2 → ${gdo2_pin} (remote_receiver) +remote_transmitter: + id: rf_tx + pin: ${gdo0_pin} + carrier_duty_percent: 100% + # Switch the chip into TX state for the duration of each transmission and back to RX + # afterwards. Driver-agnostic: any RF front-end with begin_tx/begin_rx-style actions + # can be wired this way. + on_transmit: + then: + - cc1101.begin_tx: cc1101_radio + on_complete: + then: + - cc1101.begin_rx: cc1101_radio + +remote_receiver: + id: rf_rx + pin: ${gdo2_pin} + +radio_frequency: + - platform: ir_rf_proxy + id: rf_proxy_cc1101_tx + name: "CC1101 RF Transmitter" + frequency: 433.92MHz + remote_transmitter_id: rf_tx + # Optional: retune the CC1101 per-transmit when the API request specifies a + # different carrier frequency. Demonstrates the on_control trigger. + on_control: + then: + - if: + condition: + lambda: "return x.get_frequency().has_value() && *x.get_frequency() > 0;" + then: + - cc1101.set_frequency: + id: cc1101_radio + value: !lambda "return *x.get_frequency();" + - platform: ir_rf_proxy + id: rf_proxy_cc1101_rx + name: "CC1101 RF Receiver" + frequency: 433.92MHz + remote_receiver_id: rf_rx diff --git a/tests/components/ir_rf_proxy/test-cc1101.esp32-idf.yaml b/tests/components/ir_rf_proxy/test-cc1101.esp32-idf.yaml new file mode 100644 index 0000000000..bf6f3d9815 --- /dev/null +++ b/tests/components/ir_rf_proxy/test-cc1101.esp32-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + cs_pin: GPIO5 + gdo0_pin: GPIO4 + gdo2_pin: GPIO16 + +packages: + common: !include common.yaml + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + cc1101: !include common-cc1101.yaml diff --git a/tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml b/tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml new file mode 100644 index 0000000000..e25c47ab23 --- /dev/null +++ b/tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml @@ -0,0 +1,9 @@ +substitutions: + cs_pin: GPIO5 + gdo0_pin: GPIO4 + gdo2_pin: GPIO16 + +packages: + common: !include common.yaml + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + cc1101: !include common-cc1101.yaml