[radio_frequency] Add on_control trigger; ir_rf_proxy driver-agnostic (#16368)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Keith Burzinski
2026-05-12 22:13:29 -05:00
committed by GitHub
parent 1dfd3fe9c2
commit 480c23012c
9 changed files with 105 additions and 9 deletions
@@ -106,7 +106,6 @@ void RfProxy::setup() {
void RfProxy::dump_config() { void RfProxy::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"RF Proxy '%s'\n" "RF Proxy '%s'\n"
" Backend: remote_transmitter/receiver\n"
" Supports Transmitter: %s\n" " Supports Transmitter: %s\n"
" Supports Receiver: %s", " Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()), 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) { 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); transmit_raw_timings(this->transmitter_, 0, call);
} }
+4 -1
View File
@@ -43,7 +43,10 @@ class IrRfProxy : public infrared::Infrared {
#endif // USE_IR_RF #endif // USE_IR_RF
#ifdef USE_RADIO_FREQUENCY #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 { class RfProxy : public radio_frequency::RadioFrequency {
public: public:
RfProxy() = default; RfProxy() = default;
@@ -35,17 +35,19 @@ def _final_validate(config: ConfigType) -> None:
if CONF_REMOTE_TRANSMITTER_ID not in config: if CONF_REMOTE_TRANSMITTER_ID not in config:
return return
transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID]
full_config = fv.full_config.get() 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) transmitter_config = full_config.get_config_for_path(transmitter_path)
duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT) duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT)
if duty_percent is not None and duty_percent != 100: if duty_percent is not None and duty_percent != 100:
raise cv.Invalid( raise cv.Invalid(
f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' " f"Transmitter '{config[CONF_REMOTE_TRANSMITTER_ID]}' must have "
"set to 100% for RF transmission. Dedicated RF hardware handles modulation; " f"'{CONF_CARRIER_DUTY_PERCENT}' set to 100% for RF transmission. "
"applying a carrier duty cycle would corrupt the signal" "Dedicated RF hardware handles modulation; applying a carrier duty cycle "
"would corrupt the signal"
) )
@@ -8,9 +8,10 @@ breaking changes policy. Use at your own risk.
Once the API is considered stable, this warning will be removed. Once the API is considered stable, this warning will be removed.
""" """
from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv 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 import CORE, coroutine_with_priority
from esphome.core.entity_helpers import queue_entity_register, setup_entity from esphome.core.entity_helpers import queue_entity_register, setup_entity
from esphome.coroutine import CoroPriority from esphome.coroutine import CoroPriority
@@ -42,6 +43,7 @@ def radio_frequency_schema(class_: type[cg.MockObjClass]) -> cv.Schema:
return entity_schema.extend( return entity_schema.extend(
{ {
cv.GenerateID(): cv.declare_id(class_), 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) await setup_radio_frequency_core_(var, config)
CORE.register_platform_component("radio_frequency", var) 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: async def new_radio_frequency(config: ConfigType, *args) -> cg.Pvariable:
"""Create a new RadioFrequency instance. """Create a new RadioFrequency instance.
@@ -54,6 +54,10 @@ RadioFrequencyCall &RadioFrequencyCall::set_repeat_count(uint32_t count) {
void RadioFrequencyCall::perform() { void RadioFrequencyCall::perform() {
if (this->parent_ != nullptr) { 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); this->parent_->control(*this);
} }
} }
@@ -170,6 +170,15 @@ class RadioFrequency : public Component, public EntityBase, public remote_base::
this->receive_callback_.add(std::forward<F>(callback)); this->receive_callback_.add(std::forward<F>(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<typename F> void add_on_control_callback(F &&callback) {
this->control_callback_.add(std::forward<F>(callback));
}
protected: protected:
friend class RadioFrequencyCall; 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) // Callback manager for receive events (lazy: saves memory when no callbacks registered)
LazyCallbackManager<void(remote_base::RemoteReceiveData)> receive_callback_; LazyCallbackManager<void(remote_base::RemoteReceiveData)> receive_callback_;
// Callback manager for on_control trigger (lazy: same memory savings)
LazyCallbackManager<void(const RadioFrequencyCall &)> control_callback_;
}; };
} // namespace esphome::radio_frequency } // namespace esphome::radio_frequency
@@ -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
@@ -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
@@ -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