[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

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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"
)

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -170,6 +170,15 @@ class RadioFrequency : public Component, public EntityBase, public remote_base::
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:
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<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

View File

@@ -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

View File

@@ -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

View File

@@ -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