[api] Reply when an infrared/RF raw timings transmit has completed (#19086)

This commit is contained in:
J. Nick Koston
2026-10-07 08:25:44 -10:00
committed by GitHub
parent 678975b6b2
commit 13a8efd694
43 changed files with 1130 additions and 593 deletions
+1
View File
@@ -287,6 +287,7 @@ esphome/components/inkplate/* @jesserockz @JosipKuci
esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core
esphome/components/ir_rf_base/* @bdraco @kbx81
esphome/components/ir_rf_proxy/* @kbx81
esphome/components/it8951/* @koosoli @limengdu @Passific
esphome/components/jsn_sr04t/* @Mafus1
+24 -2
View File
@@ -2692,7 +2692,7 @@ message ListEntitiesInfraredResponse {
message InfraredRFTransmitRawTimingsRequest {
option (id) = 136;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
option (ifdef) = "USE_IR_RF";
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
@@ -2706,7 +2706,7 @@ message InfraredRFTransmitRawTimingsRequest {
message InfraredRFReceiveEvent {
option (id) = 137;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
option (ifdef) = "USE_IR_RF";
option (no_delay) = true;
option (speed_optimized) = true;
@@ -2715,6 +2715,28 @@ message InfraredRFReceiveEvent {
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
// Sent only to the client that issued an InfraredRFTransmitRawTimingsRequest, once the
// transmitter reports that the transmission (all repeats) has finished, or immediately with
// success=false if it was refused before reaching the transmitter (unknown key, no transmitter,
// no or invalid timings) or the transmitter could not send it (not set up, hardware error).
// success=false also answers a request superseded by a newer request on the same entity, from
// any client, and a transmit that reported nothing within 30 s past its expected air time, so a
// client should not retry on it blindly. The device serializes transmits per transmitter and
// several entities may share one, so a client should keep at most one transmit outstanding per
// device, not per entity. A reply for an unknown key or a refused request is sent once and can be lost
// when the device's send buffer is full, so a client should also stop waiting on its own after the
// expected air time plus a margin. Lets clients pace requests instead of estimating durations (since API 1.18)
message InfraredRFTransmitCompleteResponse {
option (id) = 153;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_IR_RF";
option (no_delay) = true;
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2 [(force) = true]; // Key of the transmitter entity from the request
bool success = 3; // false if the request was refused, not sent, superseded, or never reported
}
// ==================== RADIO FREQUENCY ====================
// Lists available radio frequency entity instances
+43 -3
View File
@@ -198,6 +198,17 @@ APIConnection::~APIConnection() {
proxy->serial_proxy_request(this, enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
}
}
#endif
// entities holding a transmit reply for this client must not answer into a freed connection
#ifdef USE_INFRARED
for (auto *infrared : App.get_infrareds()) {
infrared->on_api_connection_closed(this);
}
#endif
#ifdef USE_RADIO_FREQUENCY
for (auto *radio_frequency : App.get_radio_frequencies()) {
radio_frequency->on_api_connection_closed(this);
}
#endif
}
@@ -1517,8 +1528,10 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
// Clients on API 1.18+ are told when the frame has left the transmitter; the entity owns that reply
const bool want_reply = this->client_supports_api_version(1, 18);
// Dispatch by key: infrared entities are checked first, then radio frequency entities.
// The key is unique across all entity instances on a device, so at most one lookup will succeed.
#ifdef USE_INFRARED
@@ -1528,6 +1541,7 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF
call.set_carrier_frequency(msg.carrier_frequency);
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
call.set_repeat_count(msg.repeat_count);
call.set_api_connection(want_reply ? this : nullptr);
call.perform();
return;
}
@@ -1540,13 +1554,38 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF
call.set_modulation(static_cast<radio_frequency::RadioFrequencyModulation>(msg.modulation));
call.set_repeat_count(msg.repeat_count);
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
call.set_api_connection(want_reply ? this : nullptr);
call.perform();
return;
}
#endif
ESP_LOGW(TAG, "IR/RF transmit for unknown key %" PRIu32, msg.key);
if (want_reply) {
// nothing will ever report for an unknown key, so answer as not started right away
#ifdef USE_DEVICES
const uint32_t device_id = msg.device_id;
#else
const uint32_t device_id = 0;
#endif
if (!this->send_infrared_rf_transmit_complete(device_id, msg.key, false)) {
API_LOG_MSG_DROPPED(TAG, "IR/RF reply");
}
}
}
bool APIConnection::send_infrared_rf_transmit_complete([[maybe_unused]] uint32_t device_id, uint32_t key,
bool success) {
InfraredRFTransmitCompleteResponse resp{};
#ifdef USE_DEVICES
resp.device_id = device_id;
#endif
resp.key = key;
resp.success = success;
return this->send_message(resp);
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) {
if (!this->send_message(msg)) {
// V: fires per decoded frame with no subscription gate, so a warning
@@ -1554,6 +1593,7 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent
ESP_LOGV(TAG, "IR/RF event dropped, TCP buffer full");
}
}
#endif
#ifdef USE_SERIAL_PROXY
@@ -1816,7 +1856,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 17;
resp.api_version_minor = 18;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.server_info = ESPHOME_VERSION_REF;
resp.name = StringRef(App.get_name());
+4 -1
View File
@@ -233,9 +233,12 @@ class APIConnection final : public APIServerConnectionBase {
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg);
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg);
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
// Reply to an InfraredRFTransmitRawTimingsRequest (API 1.18+); false when the TCP buffer is
// full, the entity that owns the reply retries it then
[[nodiscard]] bool send_infrared_rf_transmit_complete(uint32_t device_id, uint32_t key, bool success);
#endif
#ifdef USE_SERIAL_PROXY
+23 -2
View File
@@ -3934,7 +3934,7 @@ uint32_t ListEntitiesInfraredResponse::calc_size_msg(const void *self) {
return size;
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void InfraredRFTransmitRawTimingsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
proto_varint_value_t scalar) {
auto &msg = *static_cast<InfraredRFTransmitRawTimingsRequest *>(self);
@@ -3994,6 +3994,27 @@ InfraredRFReceiveEvent::calc_size_msg(const void *self) {
}
return size;
}
uint8_t *InfraredRFTransmitCompleteResponse::encode_msg(const void *self,
ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const InfraredRFTransmitCompleteResponse *>(self);
uint8_t *__restrict__ pos = buffer.get_pos();
#ifdef USE_DEVICES
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.device_id);
#endif
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, msg.key);
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.success);
return pos;
}
uint32_t InfraredRFTransmitCompleteResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const InfraredRFTransmitCompleteResponse *>(self);
uint32_t size = 0;
#ifdef USE_DEVICES
size += ProtoSize::calc_uint32(1, msg.device_id);
#endif
size += 5;
size += ProtoSize::calc_bool(1, msg.success);
return size;
}
#endif
#ifdef USE_RADIO_FREQUENCY
uint8_t *ListEntitiesRadioFrequencyResponse::encode_msg(const void *self,
@@ -4329,7 +4350,7 @@ static_assert(!std::is_polymorphic_v<UpdateCommandRequest>, "decodable messages
static_assert(!std::is_polymorphic_v<ZWaveProxyFrame>, "decodable messages carry no vtable");
static_assert(!std::is_polymorphic_v<ZWaveProxyRequest>, "decodable messages carry no vtable");
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
static_assert(!std::is_polymorphic_v<InfraredRFTransmitRawTimingsRequest>, "decodable messages carry no vtable");
#endif
#ifdef USE_SERIAL_PROXY
+25 -1
View File
@@ -3681,7 +3681,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
protected:
};
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 136;
@@ -3733,6 +3733,30 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
protected:
};
class InfraredRFTransmitCompleteResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 153;
static constexpr uint8_t ESTIMATED_SIZE = 11;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_complete_response"); }
#endif
#ifdef USE_DEVICES
uint32_t device_id{0};
#endif
uint32_t key{0};
bool success{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_RADIO_FREQUENCY
class ListEntitiesRadioFrequencyResponse final : public InfoResponseProtoMessage {
+10 -1
View File
@@ -2720,7 +2720,7 @@ const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest"));
#ifdef USE_DEVICES
@@ -2749,6 +2749,15 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
}
return out.c_str();
}
const char *InfraredRFTransmitCompleteResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitCompleteResponse"));
#ifdef USE_DEVICES
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("key"), this->key);
dump_field(out, ESPHOME_PSTR("success"), this->success);
return out.c_str();
}
#endif
#ifdef USE_RADIO_FREQUENCY
const char *ListEntitiesRadioFrequencyResponse::dump_to(DumpBuffer &out) const {
+1 -1
View File
@@ -628,7 +628,7 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
break;
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: {
InfraredRFTransmitRawTimingsRequest msg;
msg.decode(msg_data, msg_size);
+1 -1
View File
@@ -213,7 +213,7 @@ class APIServerConnectionBase {
void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
#endif
+2 -1
View File
@@ -504,7 +504,7 @@ void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
const std::vector<int32_t> *timings) {
InfraredRFReceiveEvent resp{};
@@ -517,6 +517,7 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
for (auto &c : this->active_clients())
c->send_infrared_rf_receive_event(resp);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
+1 -1
View File
@@ -203,7 +203,7 @@ class APIServer final : public Component
#ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
+6 -8
View File
@@ -9,20 +9,21 @@ Once the API is considered stable, this warning will be removed.
"""
import esphome.codegen as cg
from esphome.components import ir_rf_base
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import queue_entity_register, setup_entity
from esphome.core import coroutine_with_priority
from esphome.core.entity_helpers import setup_entity
from esphome.coroutine import CoroPriority
from esphome.types import ConfigType, SafeExpType
CODEOWNERS = ["@kbx81"]
AUTO_LOAD = ["remote_base"]
AUTO_LOAD = ["ir_rf_base"]
IS_PLATFORM_COMPONENT = True
infrared_ns = cg.esphome_ns.namespace("infrared")
Infrared = infrared_ns.class_("Infrared", cg.EntityBase, cg.Component)
Infrared = infrared_ns.class_("Infrared", ir_rf_base.IrRfEntity)
InfraredCall = infrared_ns.class_("InfraredCall")
InfraredTraits = infrared_ns.class_("InfraredTraits")
@@ -52,11 +53,8 @@ async def setup_infrared_core_(var: cg.MockObj, config: ConfigType) -> None:
async def register_infrared(var: cg.MockObj, config: ConfigType) -> None:
"""Register an infrared device with the core."""
cg.add_define("USE_IR_RF")
await cg.register_component(var, config)
queue_entity_register("infrared", config)
await ir_rf_base.register_ir_rf_entity(var, config, "infrared")
await setup_infrared_core_(var, config)
CORE.register_platform_component("infrared", var)
async def new_infrared(config: ConfigType, *args: SafeExpType) -> cg.MockObj:
+2 -145
View File
@@ -1,161 +1,18 @@
#include "infrared.h"
#include <cinttypes>
#include "esphome/core/log.h"
#ifdef USE_API
#include "esphome/components/api/api_server.h"
#endif
namespace esphome::infrared {
static const char *const TAG = "infrared";
// ========== InfraredCall ==========
InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
this->carrier_frequency_ = frequency;
return *this;
}
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
this->raw_timings_ = &timings;
this->packed_data_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this;
}
InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) {
this->base64url_ptr_ = &base64url;
this->raw_timings_ = nullptr;
this->packed_data_ = nullptr;
return *this;
}
InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count) {
this->packed_data_ = data;
this->packed_length_ = length;
this->packed_count_ = count;
this->raw_timings_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this;
}
InfraredCall &InfraredCall::set_repeat_count(uint32_t count) {
this->repeat_count_ = count;
return *this;
}
void InfraredCall::perform() {
if (this->parent_ != nullptr) {
this->parent_->control(*this);
}
}
// ========== Infrared ==========
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());
}
void Infrared::dump_config() {
ESP_LOGCONFIG(TAG,
"Infrared '%s'\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
YESNO(this->traits_.get_supports_receiver()));
}
void Infrared::control(const InfraredCall &call) {
if (this->transmitter_ == nullptr) {
ESP_LOGW(TAG, "No transmitter configured");
return;
}
if (!call.has_raw_timings()) {
ESP_LOGE(TAG, "No raw timings provided");
return;
}
// Create transmit data object
auto transmit_call = this->transmitter_->transmit();
auto *transmit_data = transmit_call.get_data();
// Set carrier frequency
auto freq = call.get_carrier_frequency();
if (freq.has_value()) {
transmit_data->set_carrier_frequency(*freq);
}
// Set timings based on format
if (call.is_packed()) {
// Zero-copy from packed protobuf data
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
call.get_repeat_count());
} else if (call.is_base64url()) {
// Decode base64url (URL-safe) into transmit buffer
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
}
// Sanity check: validate timing values are within reasonable bounds
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
call.get_repeat_count());
} else {
// From vector (lambdas/automations)
transmit_data->set_data(call.get_raw_timings());
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
call.get_repeat_count());
}
// Set repeat count
if (call.get_repeat_count() > 0) {
transmit_call.set_send_times(call.get_repeat_count());
}
// Perform transmission
transmit_call.perform();
}
uint32_t Infrared::get_capability_flags() const {
uint32_t flags = 0;
// Add transmit/receive capability based on traits
if (this->traits_.get_supports_transmitter())
flags |= InfraredCapability::CAPABILITY_TRANSMITTER;
if (this->traits_.get_supports_receiver())
flags |= InfraredCapability::CAPABILITY_RECEIVER;
return flags;
}
bool Infrared::on_receive(remote_base::RemoteReceiveData data) {
// Forward received IR data to API server
#if defined(USE_API) && defined(USE_IR_RF)
if (api::global_api_server != nullptr) {
#ifdef USE_DEVICES
uint32_t device_id = this->get_device_id();
#else
uint32_t device_id = 0;
#endif
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
}
#endif
return false; // Don't consume the event, allow other listeners to process it
this->get_name().c_str(), YESNO(this->get_supports_transmitter()),
YESNO(this->get_supports_receiver()));
}
} // namespace esphome::infrared
+18 -106
View File
@@ -4,131 +4,48 @@
// without following the normal breaking changes policy. Use at your own risk.
// Once the API is considered stable, this warning will be removed.
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/components/remote_base/remote_base.h"
#include <vector>
#include "esphome/components/ir_rf_base/ir_rf_base.h"
namespace esphome::infrared {
/// Capability flags for individual infrared instances
enum InfraredCapability : uint32_t {
CAPABILITY_TRANSMITTER = 1 << 0, // Can transmit signals
CAPABILITY_RECEIVER = 1 << 1, // Can receive signals
};
using ir_rf_base::CAPABILITY_RECEIVER;
using ir_rf_base::CAPABILITY_TRANSMITTER;
/// Forward declarations
class Infrared;
/// InfraredCall - Builder pattern for transmitting infrared signals
class InfraredCall {
class InfraredCall : public ir_rf_base::IrRfCall<InfraredCall, Infrared> {
public:
explicit InfraredCall(Infrared *parent) : parent_(parent) {}
explicit InfraredCall(Infrared *parent) : IrRfCall(parent) {}
/// Set the carrier frequency in Hz
InfraredCall &set_carrier_frequency(uint32_t frequency);
// ===== Raw Timings Methods =====
// All set_raw_timings_* methods store pointers/references to external data.
// The referenced data must remain valid until perform() completes.
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
/// Set the raw timings from a vector (positive = mark, negative = space)
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
/// Set the raw timings from base64url-encoded little-endian int32 data
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
/// @note Decoding happens at perform() time, directly into the transmit buffer.
InfraredCall &set_raw_timings_base64url(const std::string &base64url);
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
/// @note Usage: For API component where data comes directly from the protobuf message.
InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
InfraredCall &set_repeat_count(uint32_t count);
/// Perform the transmission
void perform();
InfraredCall &set_carrier_frequency(uint32_t frequency) {
this->carrier_frequency_ = frequency;
return *this;
}
/// Get the carrier frequency
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
/// Get the raw timings (only valid if set via set_raw_timings)
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
/// Check if raw timings have been set (any format)
bool has_raw_timings() const {
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
}
/// Check if using packed data format
bool is_packed() const { return this->packed_data_ != nullptr; }
/// Check if using base64url data format
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
/// Get the base64url data string
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
/// Get packed data (only valid if set via set_raw_timings_packed)
const uint8_t *get_packed_data() const { return this->packed_data_; }
uint16_t get_packed_length() const { return this->packed_length_; }
uint16_t get_packed_count() const { return this->packed_count_; }
/// Get the repeat count
uint32_t get_repeat_count() const { return this->repeat_count_; }
protected:
uint32_t repeat_count_{1};
Infrared *parent_;
optional<uint32_t> carrier_frequency_;
// Pointer to vector-based timings (caller-owned, must outlive perform())
const std::vector<int32_t> *raw_timings_{nullptr};
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
const std::string *base64url_ptr_{nullptr};
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
const uint8_t *packed_data_{nullptr};
uint16_t packed_length_{0};
uint16_t packed_count_{0};
};
/// InfraredTraits - Describes the capabilities of an infrared implementation
class InfraredTraits {
public:
bool get_supports_transmitter() const { return this->supports_transmitter_; }
void set_supports_transmitter(bool supports) { this->supports_transmitter_ = supports; }
bool get_supports_receiver() const { return this->supports_receiver_; }
void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; }
uint32_t get_receiver_frequency_hz() const { return this->receiver_frequency_hz_; }
void set_receiver_frequency_hz(uint32_t freq) { this->receiver_frequency_hz_ = freq; }
protected:
bool supports_transmitter_{false};
bool supports_receiver_{false};
uint32_t receiver_frequency_hz_{0}; // Demodulation frequency of the IR receiver in Hz (0 = unspecified)
};
/// Infrared - Base class for infrared remote control implementations
class Infrared : public Component, public EntityBase, public remote_base::RemoteReceiverListener {
class Infrared : public ir_rf_base::IrRfEntity {
public:
Infrared() = default;
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
/// 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; }
/// Check if this infrared has a transmitter configured
bool has_transmitter() const { return this->transmitter_ != nullptr; }
/// Check if this infrared has a receiver configured
bool has_receiver() const { return this->receiver_ != nullptr; }
/// Get the traits for this infrared implementation
InfraredTraits &get_traits() { return this->traits_; }
@@ -137,21 +54,16 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote
/// Create a call object for transmitting
InfraredCall make_call() { return InfraredCall(this); }
/// Get capability flags for this infrared instance
uint32_t get_capability_flags() const;
/// Called when IR data is received (from RemoteReceiverListener)
bool on_receive(remote_base::RemoteReceiveData data) override;
protected:
friend class InfraredCall;
friend class ir_rf_base::IrRfCall<InfraredCall, Infrared>;
/// Perform the actual transmission (called by InfraredCall)
virtual void control(const InfraredCall &call);
// Underlying hardware components
remote_base::RemoteReceiverBase *receiver_{nullptr};
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
void on_call_(const InfraredCall &) {}
/// Perform the actual transmission (called by InfraredCall); false only when no frame was handed
/// to the transmitter, in which case no completion follows
/// Without a remote_base transmitter, call api_transmit_done_() once the frame is out
virtual bool control(const InfraredCall &call) {
return this->transmit_raw_(call, call.get_carrier_frequency().value_or(0));
}
// Traits describing capabilities
InfraredTraits traits_;
+38
View File
@@ -0,0 +1,38 @@
"""Shared base for the infrared and radio_frequency entity components."""
import esphome.codegen as cg
from esphome.components import remote_base
from esphome.const import CONF_API
from esphome.core import CORE
from esphome.core.entity_helpers import queue_entity_register
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81", "@bdraco"]
AUTO_LOAD = ["remote_base"]
ir_rf_base_ns = cg.esphome_ns.namespace("ir_rf_base")
IrRfEntity = ir_rf_base_ns.class_("IrRfEntity", cg.EntityBase, cg.Component)
async def attach_transmitter(var: cg.MockObj, config: ConfigType, key: str) -> None:
"""Link the configured transmitter to an entity.
With the API configured this also compiles in the transmit completion
tracking that answers API transmit requests; the transmitter platform has
to report completion through notify_complete_(), as remote_transmitter
does. Without this call anywhere in the build a request is answered as
soon as the frame is handed over.
"""
await remote_base.register_transmittable(var, config, key)
if CONF_API in CORE.config:
cg.add_define("USE_IR_RF_TRANSMIT_COMPLETE")
async def register_ir_rf_entity(
var: cg.MockObj, config: ConfigType, domain: str
) -> None:
"""Register an infrared or radio_frequency entity; USE_IR_RF covers both kinds."""
cg.add_define("USE_IR_RF")
await cg.register_component(var, config)
queue_entity_register(domain, config)
CORE.register_platform_component(domain, var)
@@ -0,0 +1,237 @@
#include "ir_rf_base.h"
#include <algorithm>
#include <cinttypes>
#include "esphome/core/log.h"
#ifdef USE_API
#include "esphome/components/api/api_connection.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/application.h"
#endif
namespace esphome::ir_rf_base {
static const char *const TAG = "ir_rf";
#if defined(USE_API) && defined(USE_IR_RF)
// A missing completion is answered as failed this long after the frame should have left the
// wire, and an owed reply the client never reads is dropped this long after the first refusal
static constexpr uint32_t API_REPLY_TIMEOUT_MS = 30000;
#endif
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
// Longest air time added to the deadline, in 16 ms ticks (8 min); with the 30 s above the
// deadline stays within the signed 16 bit tick window loop() compares against
static constexpr uint16_t API_REPLY_MAX_AIR_TICKS = 30000;
#endif
#if defined(USE_API) && defined(USE_IR_RF)
static uint16_t api_reply_ticks_from_now() {
return static_cast<uint16_t>((App.get_loop_component_start_time() >> 4) + (API_REPLY_TIMEOUT_MS >> 4));
}
#endif
void IrRfEntity::setup() {
// merged, not assigned: a platform may have set a flag for its own hardware before setup()
if (this->has_transmitter())
this->supports_transmitter_ = true;
if (this->has_receiver())
this->supports_receiver_ = true;
}
bool IrRfEntity::transmit_raw_(const IrRfCallData &call, uint32_t carrier_frequency_hz) {
if (this->transmitter_ == nullptr) {
ESP_LOGW(TAG, "No transmitter configured");
return false;
}
if (!call.has_raw_timings()) {
ESP_LOGE(TAG, "No raw timings provided");
return false;
}
auto transmit_call = this->transmitter_->transmit();
auto *transmit_data = transmit_call.get_data();
transmit_data->set_carrier_frequency(carrier_frequency_hz);
if (call.is_packed()) {
// Zero-copy from packed protobuf data
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
call.get_repeat_count());
} else if (call.is_base64url()) {
// Decode base64url (URL-safe) into transmit buffer
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return false;
}
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
return false;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
call.get_repeat_count());
} else {
// From vector (lambdas/automations)
transmit_data->set_data(call.get_raw_timings());
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
call.get_repeat_count());
}
// one answer for every backend: a frame that decoded to nothing is refused here
if (transmit_data->get_data().empty()) {
ESP_LOGE(TAG, "No raw timings provided");
return false;
}
if (call.get_repeat_count() > 0) {
transmit_call.set_send_times(call.get_repeat_count());
}
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
// only the API frame expect_api_reply_() armed claims the seq and extends its deadline
if (call.wants_api_reply() && this->api_reply_ == ApiReply::API_REPLY_WAITING) {
this->inflight_seq_ = transmit_call.get_seq();
// a long frame must not be answered as failed while still on the wire: the 30 s safety net
// starts after this frame's own air time (capped so the tick comparison cannot wrap)
uint64_t frame_us = 0;
for (const int64_t timing : transmit_data->get_data())
frame_us += timing < 0 ? -timing : timing;
const uint64_t air_ticks = (frame_us * std::max<uint32_t>(call.get_repeat_count(), 1) / 1000) >> 4;
this->api_reply_deadline_ += static_cast<uint16_t>(std::min<uint64_t>(air_ticks, API_REPLY_MAX_AIR_TICKS));
}
#endif
transmit_call.perform();
return true;
}
bool IrRfEntity::on_receive(remote_base::RemoteReceiveData data) {
#if defined(USE_API) && defined(USE_IR_RF)
if (api::global_api_server != nullptr) {
#ifdef USE_DEVICES
uint32_t device_id = this->get_device_id();
#else
uint32_t device_id = 0;
#endif
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
}
#endif
return false; // Don't consume the event, allow other listeners to process it
}
#if defined(USE_API) && defined(USE_IR_RF)
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
void IrRfEntity::on_transmit_complete(remote_base::RemoteTransmitterBase *transmitter, uint16_t seq, bool sent) {
// only the frame this entity submitted; YAML automations and other entities share the transmitter
if (transmitter == this->transmitter_ && seq == this->inflight_seq_ &&
this->api_reply_ == ApiReply::API_REPLY_WAITING)
this->finish_api_reply_(sent);
}
#endif
bool IrRfEntity::expect_api_reply_(api::APIConnection *conn) {
// only an unpaced client gets here: its earlier request is answered as not started, and a reply
// the buffer still owes gets one last try; if that fails too the slot stays with it and the
// new request is refused rather than answered to nobody
if (this->api_reply_ == ApiReply::API_REPLY_WAITING)
this->finish_api_reply_(false);
if (this->api_reply_ != ApiReply::API_REPLY_NONE && !this->send_api_reply_()) {
ESP_LOGW(TAG, "'%s': transmit %s", this->get_name().c_str(), LOG_STR_LITERAL("refused, reply still owed"));
this->refuse_api_call_(conn);
return false;
}
this->api_reply_connection_ = conn;
this->api_reply_deadline_ = api_reply_ticks_from_now();
this->api_reply_ = ApiReply::API_REPLY_WAITING;
return true;
}
void IrRfEntity::refuse_api_call_(api::APIConnection *conn) {
uint32_t device_id = 0;
#ifdef USE_DEVICES
device_id = this->get_device_id();
#endif
// no slot to retry from; a drop here is covered by the client's own timeout
if (!conn->send_infrared_rf_transmit_complete(device_id, this->get_object_id_hash(), false)) {
API_LOG_MSG_DROPPED(TAG, "IR/RF reply");
}
}
void IrRfEntity::finish_api_reply_(bool success) {
this->api_reply_ = success ? ApiReply::API_REPLY_OWED_OK : ApiReply::API_REPLY_OWED_FAILED;
if (!this->send_api_reply_()) {
// the retry gets its own window
this->api_reply_deadline_ = api_reply_ticks_from_now();
}
}
bool IrRfEntity::send_api_reply_() {
uint32_t device_id = 0;
#ifdef USE_DEVICES
device_id = this->get_device_id();
#endif
// Refused by a full TCP buffer: the reply stays owed and loop() retries it, since a lost
// reply would stall the client's pacing for good (same shape as bluetooth_proxy)
if (!this->api_reply_connection_->send_infrared_rf_transmit_complete(device_id, this->get_object_id_hash(),
this->api_reply_ == ApiReply::API_REPLY_OWED_OK))
return false;
this->clear_api_reply_();
return true;
}
// Only runs while an API reply is pending: retries an owed one until it goes out or the client is
// gone, and expires a transmit that never reported (a platform overriding control() without wiring
// its transmitter's completion)
void IrRfEntity::loop() {
if (this->api_reply_ == ApiReply::API_REPLY_NONE) {
this->disable_loop();
return;
}
const auto remaining = static_cast<int16_t>(this->api_reply_deadline_ - (App.get_loop_component_start_time() >> 4));
if (this->api_reply_ != ApiReply::API_REPLY_WAITING) {
if (this->send_api_reply_() || remaining > 0)
return;
ESP_LOGW(TAG, "'%s': transmit %s", this->get_name().c_str(), LOG_STR_LITERAL("reply dropped, client not reading"));
this->clear_api_reply_();
return;
}
if (remaining > 0)
return;
ESP_LOGW(TAG, "'%s': transmit %s", this->get_name().c_str(), LOG_STR_LITERAL("never reported completion"));
this->finish_api_reply_(false);
}
void IrRfEntity::on_api_connection_closed(api::APIConnection *conn) {
if (this->api_reply_connection_ == conn) {
this->clear_api_reply_();
}
}
#endif
} // namespace esphome::ir_rf_base
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
namespace esphome::remote_base {
void ir_rf_transmit_complete(RemoteTransmitterBase *transmitter, uint16_t seq, bool sent) {
#ifdef USE_INFRARED
for (auto *entity : App.get_infrareds()) {
entity->on_transmit_complete(transmitter, seq, sent);
}
#endif
#ifdef USE_RADIO_FREQUENCY
for (auto *entity : App.get_radio_frequencies()) {
entity->on_transmit_complete(transmitter, seq, sent);
}
#endif
}
} // namespace esphome::remote_base
#endif
+273
View File
@@ -0,0 +1,273 @@
#pragma once
// WARNING: This component is EXPERIMENTAL. The API may change at any time
// without following the normal breaking changes policy. Use at your own risk.
// Once the API is considered stable, this warning will be removed.
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/components/remote_base/remote_base.h"
#include <string>
#include <vector>
#if defined(USE_API) && defined(USE_IR_RF)
namespace esphome::api {
class APIConnection;
} // namespace esphome::api
#endif
namespace esphome::ir_rf_base {
/// Capability flags reported by infrared and radio frequency entities
enum IrRfCapability : uint32_t {
CAPABILITY_TRANSMITTER = 1 << 0, // Can transmit signals
CAPABILITY_RECEIVER = 1 << 1, // Can receive signals
};
/// Raw timings of a transmit call, in one of three caller-owned forms
class IrRfCallData {
public:
/// Get the raw timings (only valid if set via set_raw_timings)
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
/// Check if raw timings have been set (any format)
bool has_raw_timings() const {
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
}
/// Check if using packed data format
bool is_packed() const { return this->packed_data_ != nullptr; }
/// Check if using base64url data format
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
/// Get the base64url data string
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
/// Get packed data (only valid if set via set_raw_timings_packed)
const uint8_t *get_packed_data() const { return this->packed_data_; }
uint16_t get_packed_length() const { return this->packed_length_; }
uint16_t get_packed_count() const { return this->packed_count_; }
/// Get the repeat count
uint32_t get_repeat_count() const { return this->repeat_count_; }
#if defined(USE_API) && defined(USE_IR_RF)
/// True for a frame an API client is waiting on
bool wants_api_reply() const { return this->api_connection_ != nullptr; }
#endif
protected:
uint32_t repeat_count_{1};
#if defined(USE_API) && defined(USE_IR_RF)
api::APIConnection *api_connection_{nullptr};
#endif
// Pointer to vector-based timings (caller-owned, must outlive perform())
const std::vector<int32_t> *raw_timings_{nullptr};
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
const std::string *base64url_ptr_{nullptr};
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
const uint8_t *packed_data_{nullptr};
uint16_t packed_length_{0};
uint16_t packed_count_{0};
};
template<typename Call, typename Entity> class IrRfCall;
/// Everything an infrared or radio frequency entity does that does not depend on the medium:
/// the remote_base transport, forwarding received frames to the API, and answering the API
/// once a transmit it started has left the transmitter.
class IrRfEntity : public Component, public EntityBase, public remote_base::RemoteReceiverListener {
public:
/// Reports the configured transports, listens on the receiver and hooks the transmitter's
/// completion; a platform with its own setup() calls it first
void setup() override;
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
/// 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; }
bool has_transmitter() const { return this->transmitter_ != nullptr; }
bool has_receiver() const { return this->receiver_ != nullptr; }
/// What the entity can do; platforms with their own hardware set these from their own
/// setup(), remote_base ones get them from IrRfEntity::setup()
bool get_supports_transmitter() const { return this->supports_transmitter_; }
void set_supports_transmitter(bool supports) { this->supports_transmitter_ = supports; }
bool get_supports_receiver() const { return this->supports_receiver_; }
void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; }
/// Capability flags as reported to the API and web server
uint32_t get_capability_flags() const {
uint32_t flags = 0;
if (this->supports_transmitter_)
flags |= CAPABILITY_TRANSMITTER;
if (this->supports_receiver_)
flags |= CAPABILITY_RECEIVER;
return flags;
}
/// Forwards a received frame to the API; never consumes it, so other listeners still run
bool on_receive(remote_base::RemoteReceiveData data) override;
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
/// Called for every finished frame on every transmitter; answers the API request when the
/// frame is the one this entity submitted for it, with sent as the outcome
void on_transmit_complete(remote_base::RemoteTransmitterBase *transmitter, uint16_t seq, bool sent);
#endif
#if defined(USE_API) && defined(USE_IR_RF)
void loop() override;
/// The API server calls this when a client disconnects, so no reply goes to a stale pointer
void on_api_connection_closed(api::APIConnection *conn);
#endif
protected:
template<typename, typename> friend class IrRfCall;
/// Hands the call's timings to the transmitter; the default transmit path of both entity types
bool transmit_raw_(const IrRfCallData &call, uint32_t carrier_frequency_hz);
#if defined(USE_API) && defined(USE_IR_RF)
// One reply slot: a pacing client has at most one transmit outstanding, and a second request
// from an unpaced client displaces the first. Retried and expired from loop(), which only
// runs while a reply is pending; polling the expiry there costs nothing while idle, where a
// scheduler timeout would allocate per frame.
enum class ApiReply : uint8_t {
API_REPLY_NONE,
API_REPLY_WAITING, // frame handed to the transmitter, completion not reported yet
API_REPLY_OWED_OK, // reply refused by a full TCP buffer; loop() retries it
API_REPLY_OWED_FAILED // same, for a transmit that did not start
};
/// Claims the reply slot for conn; false when it still owes a reply that cannot be sent
bool expect_api_reply_(api::APIConnection *conn);
void refuse_api_call_(api::APIConnection *conn);
/// After control(): a false start is answered now, and the loop only runs once something is
/// pending, since a blocking transmitter has already answered inside control()
void settle_api_reply_(bool started) {
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
if (!started)
this->finish_api_reply_(false);
#else
// no transmitter in this build reports completion, so the hand-over is the answer
this->finish_api_reply_(started);
#endif
if (this->api_reply_ != ApiReply::API_REPLY_NONE)
this->enable_loop();
}
void finish_api_reply_(bool success);
/// Answers the API request for a control() override that transmits without a remote_base transmitter
void api_transmit_done_(bool sent) {
if (this->api_reply_ == ApiReply::API_REPLY_WAITING)
this->finish_api_reply_(sent);
}
bool send_api_reply_();
void clear_api_reply_() {
this->api_reply_ = ApiReply::API_REPLY_NONE;
this->api_reply_connection_ = nullptr;
this->disable_loop();
}
api::APIConnection *api_reply_connection_{nullptr};
#endif
remote_base::RemoteReceiverBase *receiver_{nullptr};
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
#if defined(USE_API) && defined(USE_IR_RF)
// 16 ms ticks: 30 s for completion (plus capped air time) or for delivering an owed reply
uint16_t api_reply_deadline_{0};
#endif
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
uint16_t inflight_seq_{0}; // seq of the API frame this entity submitted last
#endif
// short members last, so the derived traits start on the next word without a gap
bool supports_transmitter_{false};
bool supports_receiver_{false};
#if defined(USE_API) && defined(USE_IR_RF)
ApiReply api_reply_{ApiReply::API_REPLY_NONE};
#endif
};
/// Builder for a transmit; Call is the concrete call type and Entity its entity, so the fluent
/// setters return the concrete type and control() sees the medium specific fields
template<typename Call, typename Entity> class IrRfCall : public IrRfCallData {
// only the concrete call may construct the CRTP base
friend Call;
explicit IrRfCall(Entity *parent) : parent_(parent) {}
public:
// ===== Raw Timings Methods =====
// All set_raw_timings_* methods store pointers/references to external data.
// The referenced data must remain valid until perform() completes.
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
/// Set the raw timings from a vector (positive = mark, negative = space)
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
Call &set_raw_timings(const std::vector<int32_t> &timings) {
this->raw_timings_ = &timings;
this->packed_data_ = nullptr;
this->base64url_ptr_ = nullptr;
return this->self_();
}
/// Set the raw timings from base64url-encoded little-endian int32 data
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
/// @note Decoding happens at perform() time, directly into the transmit buffer.
Call &set_raw_timings_base64url(const std::string &base64url) {
this->base64url_ptr_ = &base64url;
this->raw_timings_ = nullptr;
this->packed_data_ = nullptr;
return this->self_();
}
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
/// @note Usage: For API component where data comes directly from the protobuf message.
Call &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count) {
this->packed_data_ = data;
this->packed_length_ = length;
this->packed_count_ = count;
this->raw_timings_ = nullptr;
this->base64url_ptr_ = nullptr;
return this->self_();
}
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
Call &set_repeat_count(uint32_t count) {
this->repeat_count_ = count;
return this->self_();
}
#if defined(USE_API) && defined(USE_IR_RF)
/// Reply to this API client once the frame has left the transmitter (API 1.18+)
Call &set_api_connection(api::APIConnection *conn) {
this->api_connection_ = conn;
return this->self_();
}
#endif
/// Perform the transmission; returns true if a frame was handed to the transmitter
bool perform() {
// make_call() always sets the parent
Entity *parent = this->parent_;
if (parent == nullptr)
return false;
parent->on_call_(this->self_());
#if defined(USE_API) && defined(USE_IR_RF)
// Before control(): blocking transmitters report completion from inside it, and the
// non-blocking RMT path flushes the previous frame's completion there
if (this->api_connection_ != nullptr && !parent->expect_api_reply_(this->api_connection_))
return false;
#endif
const bool started = parent->control(this->self_());
#if defined(USE_API) && defined(USE_IR_RF)
if (this->api_connection_ != nullptr)
parent->settle_api_reply_(started);
#endif
return started;
}
protected:
Call &self_() { return static_cast<Call &>(*this); }
Entity *parent_;
};
} // namespace esphome::ir_rf_base
+3 -9
View File
@@ -3,12 +3,7 @@
from typing import Any
import esphome.codegen as cg
from esphome.components import (
infrared,
remote_base,
remote_receiver,
remote_transmitter,
)
from esphome.components import infrared, ir_rf_base, remote_base, remote_receiver
from esphome.components.const import CONF_RECEIVER_FREQUENCY
import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
@@ -30,7 +25,7 @@ CONFIG_SCHEMA = cv.All(
remote_receiver.RemoteReceiverComponent
),
cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
remote_base.RemoteTransmitterBase
),
}
),
@@ -82,8 +77,7 @@ async def to_code(config: dict[str, Any]) -> None:
# Link transmitter if specified
if CONF_REMOTE_TRANSMITTER_ID in config:
transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter))
await ir_rf_base.attach_transmitter(var, config, CONF_REMOTE_TRANSMITTER_ID)
# Link receiver if specified
if CONF_REMOTE_RECEIVER_ID in config:
+9 -69
View File
@@ -8,70 +8,17 @@ namespace esphome::ir_rf_proxy {
static const char *const TAG = "ir_rf_proxy";
// ========== Shared transmit helper ==========
// Static template: all instantiations occur in this translation unit.
template<typename CallT>
static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency,
const CallT &call) {
if (transmitter == nullptr) {
ESP_LOGW(TAG, "No transmitter configured");
return;
}
if (!call.has_raw_timings()) {
ESP_LOGE(TAG, "No raw timings provided");
return;
}
auto transmit_call = transmitter->transmit();
auto *transmit_data = transmit_call.get_data();
transmit_data->set_carrier_frequency(carrier_frequency);
if (call.is_packed()) {
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
call.get_repeat_count());
} else if (call.is_base64url()) {
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
}
constexpr int32_t max_timing_us = 500000;
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
call.get_repeat_count());
} else {
transmit_data->set_data(call.get_raw_timings());
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
call.get_repeat_count());
}
if (call.get_repeat_count() > 0) {
transmit_call.set_send_times(call.get_repeat_count());
}
transmit_call.perform();
}
// ========== IrRfProxy (Infrared platform) ==========
#ifdef USE_IR_RF
#ifdef USE_INFRARED
void IrRfProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"IR Proxy '%s'\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
YESNO(this->traits_.get_supports_receiver()));
this->get_name().c_str(), YESNO(this->get_supports_transmitter()),
YESNO(this->get_supports_receiver()));
if (this->is_rf()) {
ESP_LOGCONFIG(TAG, " Hardware Type: RF (%.3f MHz)", this->frequency_khz_ / 1e3f);
@@ -80,21 +27,14 @@ void IrRfProxy::dump_config() {
}
}
void IrRfProxy::control(const infrared::InfraredCall &call) {
uint32_t carrier = call.get_carrier_frequency().value_or(0);
transmit_raw_timings(this->transmitter_, carrier, call);
}
#endif // USE_IR_RF
#endif // USE_INFRARED
// ========== RfProxy (Radio Frequency platform) ==========
#ifdef USE_RADIO_FREQUENCY
void RfProxy::setup() {
this->traits_.set_supports_transmitter(this->transmitter_ != nullptr);
this->traits_.set_supports_receiver(this->receiver_ != nullptr);
ir_rf_base::IrRfEntity::setup();
// remote_transmitter/receiver always uses OOK (on-off keying)
this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK);
}
@@ -104,8 +44,8 @@ void RfProxy::dump_config() {
"RF Proxy '%s'\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
YESNO(this->traits_.get_supports_receiver()));
this->get_name().c_str(), YESNO(this->get_supports_transmitter()),
YESNO(this->get_supports_receiver()));
const auto &traits = this->traits_;
if (traits.get_frequency_min_hz() > 0) {
@@ -118,11 +58,11 @@ void RfProxy::dump_config() {
}
}
void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
bool RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
// 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);
return this->transmit_raw_(call, 0);
}
#endif // USE_RADIO_FREQUENCY
+4 -15
View File
@@ -6,7 +6,7 @@
#include "esphome/components/remote_base/remote_base.h"
#ifdef USE_IR_RF
#ifdef USE_INFRARED
#include "esphome/components/infrared/infrared.h"
#endif
@@ -16,7 +16,7 @@
namespace esphome::ir_rf_proxy {
#ifdef USE_IR_RF
#ifdef USE_INFRARED
/// IrRfProxy - Infrared platform implementation using remote_transmitter/receiver as backend
class IrRfProxy final : public infrared::Infrared {
public:
@@ -35,12 +35,10 @@ class IrRfProxy final : public infrared::Infrared {
void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); }
protected:
void control(const infrared::InfraredCall &call) override;
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
uint32_t frequency_khz_{0};
};
#endif // USE_IR_RF
#endif // USE_INFRARED
#ifdef USE_RADIO_FREQUENCY
/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend.
@@ -54,20 +52,11 @@ class RfProxy final : public radio_frequency::RadioFrequency {
void setup() override;
void dump_config() override;
/// Set the remote transmitter component
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
/// 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)
void set_frequency_hz(uint32_t freq_hz) { this->traits_.set_fixed_frequency_hz(freq_hz); }
protected:
void control(const radio_frequency::RadioFrequencyCall &call) override;
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
remote_base::RemoteReceiverBase *receiver_{nullptr};
bool control(const radio_frequency::RadioFrequencyCall &call) override;
};
#endif // USE_RADIO_FREQUENCY
@@ -1,12 +1,7 @@
"""Radio Frequency platform implementation using remote_base (remote_transmitter/receiver)."""
import esphome.codegen as cg
from esphome.components import (
radio_frequency,
remote_base,
remote_receiver,
remote_transmitter,
)
from esphome.components import ir_rf_base, radio_frequency, remote_base, remote_receiver
import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
import esphome.final_validate as fv
@@ -27,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
remote_receiver.RemoteReceiverComponent
),
cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
remote_base.RemoteTransmitterBase
),
}
),
@@ -67,8 +62,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_frequency_hz(int(config[CONF_FREQUENCY])))
if CONF_REMOTE_TRANSMITTER_ID in config:
transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter))
await ir_rf_base.attach_transmitter(var, config, CONF_REMOTE_TRANSMITTER_ID)
if CONF_REMOTE_RECEIVER_ID in config:
await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID)
+6 -10
View File
@@ -10,22 +10,21 @@ Once the API is considered stable, this warning will be removed.
from esphome import automation
import esphome.codegen as cg
from esphome.components import ir_rf_base
import esphome.config_validation as cv
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.core import coroutine_with_priority
from esphome.core.entity_helpers import setup_entity
from esphome.coroutine import CoroPriority
from esphome.types import ConfigType, SafeExpType
CODEOWNERS = ["@kbx81"]
AUTO_LOAD = ["remote_base"]
AUTO_LOAD = ["ir_rf_base"]
IS_PLATFORM_COMPONENT = True
radio_frequency_ns = cg.esphome_ns.namespace("radio_frequency")
RadioFrequency = radio_frequency_ns.class_(
"RadioFrequency", cg.EntityBase, cg.Component
)
RadioFrequency = radio_frequency_ns.class_("RadioFrequency", ir_rf_base.IrRfEntity)
RadioFrequencyCall = radio_frequency_ns.class_("RadioFrequencyCall")
RadioFrequencyTraits = radio_frequency_ns.class_("RadioFrequencyTraits")
RadioFrequencyModulation = radio_frequency_ns.enum("RadioFrequencyModulation")
@@ -55,11 +54,8 @@ async def setup_radio_frequency_core_(var: cg.MockObj, config: ConfigType) -> No
async def register_radio_frequency(var: cg.MockObj, config: ConfigType) -> None:
"""Register a radio frequency device with the core."""
cg.add_define("USE_RADIO_FREQUENCY")
await cg.register_component(var, config)
queue_entity_register("radio_frequency", config)
await ir_rf_base.register_ir_rf_entity(var, config, "radio_frequency")
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(
@@ -4,73 +4,17 @@
#include "esphome/core/log.h"
#ifdef USE_API
#include "esphome/components/api/api_server.h"
#endif
namespace esphome::radio_frequency {
static const char *const TAG = "radio_frequency";
// ========== RadioFrequencyCall ==========
RadioFrequencyCall &RadioFrequencyCall::set_frequency(uint32_t frequency_hz) {
this->frequency_hz_ = frequency_hz;
return *this;
}
RadioFrequencyCall &RadioFrequencyCall::set_modulation(RadioFrequencyModulation modulation) {
this->modulation_ = modulation;
return *this;
}
RadioFrequencyCall &RadioFrequencyCall::set_raw_timings(const std::vector<int32_t> &timings) {
this->raw_timings_ = &timings;
this->packed_data_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this;
}
RadioFrequencyCall &RadioFrequencyCall::set_raw_timings_base64url(const std::string &base64url) {
this->base64url_ptr_ = &base64url;
this->raw_timings_ = nullptr;
this->packed_data_ = nullptr;
return *this;
}
RadioFrequencyCall &RadioFrequencyCall::set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count) {
this->packed_data_ = data;
this->packed_length_ = length;
this->packed_count_ = count;
this->raw_timings_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this;
}
RadioFrequencyCall &RadioFrequencyCall::set_repeat_count(uint32_t count) {
this->repeat_count_ = count;
return *this;
}
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);
}
}
// ========== RadioFrequency ==========
void RadioFrequency::dump_config() {
ESP_LOGCONFIG(TAG,
"Radio Frequency '%s'\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
YESNO(this->traits_.get_supports_receiver()));
this->get_name().c_str(), YESNO(this->get_supports_transmitter()),
YESNO(this->get_supports_receiver()));
if (this->traits_.get_frequency_min_hz() > 0) {
if (this->traits_.get_frequency_min_hz() == this->traits_.get_frequency_max_hz()) {
ESP_LOGCONFIG(TAG, " Frequency: %" PRIu32 " Hz (fixed)", this->traits_.get_frequency_min_hz());
@@ -81,31 +25,9 @@ void RadioFrequency::dump_config() {
}
}
uint32_t RadioFrequency::get_capability_flags() const {
uint32_t flags = 0;
if (this->traits_.get_supports_transmitter())
flags |= RadioFrequencyCapability::CAPABILITY_TRANSMITTER;
if (this->traits_.get_supports_receiver())
flags |= RadioFrequencyCapability::CAPABILITY_RECEIVER;
return flags;
}
bool RadioFrequency::on_receive(remote_base::RemoteReceiveData data) {
// Invoke local callbacks
this->receive_callback_.call(data);
// Forward received RF data to API server
#if defined(USE_API) && defined(USE_RADIO_FREQUENCY)
if (api::global_api_server != nullptr) {
#ifdef USE_DEVICES
uint32_t device_id = this->get_device_id();
#else
uint32_t device_id = 0;
#endif
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
}
#endif
return false; // Don't consume the event, allow other listeners to process it
return IrRfEntity::on_receive(data);
}
} // namespace esphome::radio_frequency
@@ -4,20 +4,12 @@
// without following the normal breaking changes policy. Use at your own risk.
// Once the API is considered stable, this warning will be removed.
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/components/remote_base/remote_base.h"
#include <vector>
#include "esphome/components/ir_rf_base/ir_rf_base.h"
namespace esphome::radio_frequency {
/// Capability flags for individual radio frequency instances
enum RadioFrequencyCapability : uint32_t {
CAPABILITY_TRANSMITTER = 1 << 0, // Can transmit signals
CAPABILITY_RECEIVER = 1 << 1, // Can receive signals
};
using ir_rf_base::CAPABILITY_RECEIVER;
using ir_rf_base::CAPABILITY_TRANSMITTER;
/// Modulation types supported by radio frequency implementations
enum RadioFrequencyModulation : uint8_t {
@@ -25,95 +17,36 @@ enum RadioFrequencyModulation : uint8_t {
// Future: RADIO_FREQUENCY_MODULATION_FSK, RADIO_FREQUENCY_MODULATION_GFSK, etc.
};
/// Forward declarations
class RadioFrequency;
/// RadioFrequencyCall - Builder pattern for transmitting radio frequency signals
class RadioFrequencyCall {
class RadioFrequencyCall : public ir_rf_base::IrRfCall<RadioFrequencyCall, RadioFrequency> {
public:
explicit RadioFrequencyCall(RadioFrequency *parent) : parent_(parent) {}
explicit RadioFrequencyCall(RadioFrequency *parent) : IrRfCall(parent) {}
/// Set the carrier frequency in Hz (e.g. 433920000 for 433.92 MHz)
RadioFrequencyCall &set_frequency(uint32_t frequency_hz);
RadioFrequencyCall &set_frequency(uint32_t frequency_hz) {
this->frequency_hz_ = frequency_hz;
return *this;
}
/// Set the modulation type (defaults to OOK)
RadioFrequencyCall &set_modulation(RadioFrequencyModulation modulation);
// ===== Raw Timings Methods =====
// All set_raw_timings_* methods store pointers/references to external data.
// The referenced data must remain valid until perform() completes.
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
/// Set the raw timings from a vector (positive = mark, negative = space)
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
RadioFrequencyCall &set_raw_timings(const std::vector<int32_t> &timings);
/// Set the raw timings from base64url-encoded little-endian int32 data
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
/// @note Decoding happens at perform() time, directly into the transmit buffer.
RadioFrequencyCall &set_raw_timings_base64url(const std::string &base64url);
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
/// @note Usage: For API component where data comes directly from the protobuf message.
RadioFrequencyCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
RadioFrequencyCall &set_repeat_count(uint32_t count);
/// Perform the transmission
void perform();
RadioFrequencyCall &set_modulation(RadioFrequencyModulation modulation) {
this->modulation_ = modulation;
return *this;
}
/// Get the frequency in Hz
const optional<uint32_t> &get_frequency() const { return this->frequency_hz_; }
/// Get the modulation type
RadioFrequencyModulation get_modulation() const { return this->modulation_; }
/// Get the raw timings (only valid if set via set_raw_timings)
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
/// Check if raw timings have been set (any format)
bool has_raw_timings() const {
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
}
/// Check if using packed data format
bool is_packed() const { return this->packed_data_ != nullptr; }
/// Check if using base64url data format
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
/// Get the base64url data string
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
/// Get packed data (only valid if set via set_raw_timings_packed)
const uint8_t *get_packed_data() const { return this->packed_data_; }
uint16_t get_packed_length() const { return this->packed_length_; }
uint16_t get_packed_count() const { return this->packed_count_; }
/// Get the repeat count
uint32_t get_repeat_count() const { return this->repeat_count_; }
protected:
optional<uint32_t> frequency_hz_{};
uint32_t repeat_count_{1};
RadioFrequency *parent_;
// Pointer to vector-based timings (caller-owned, must outlive perform())
const std::vector<int32_t> *raw_timings_{nullptr};
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
const std::string *base64url_ptr_{nullptr};
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
const uint8_t *packed_data_{nullptr};
uint16_t packed_length_{0};
uint16_t packed_count_{0};
RadioFrequencyModulation modulation_{RADIO_FREQUENCY_MODULATION_OOK};
};
/// RadioFrequencyTraits - Describes the capabilities of a radio frequency implementation
class RadioFrequencyTraits {
public:
bool get_supports_transmitter() const { return this->supports_transmitter_; }
void set_supports_transmitter(bool supports) { this->supports_transmitter_ = supports; }
bool get_supports_receiver() const { return this->supports_receiver_; }
void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; }
/// Hardware-supported tunable frequency range in Hz.
/// If min == max (and both non-zero): fixed-frequency hardware.
/// If both 0: range unspecified.
@@ -140,17 +73,14 @@ class RadioFrequencyTraits {
uint32_t frequency_min_hz_{0}; // Minimum tunable frequency in Hz (0 = unspecified)
uint32_t frequency_max_hz_{0}; // Maximum tunable frequency in Hz (0 = unspecified)
uint32_t supported_modulations_{0}; // Bitmask of supported RadioFrequencyModulation values
bool supports_transmitter_{false};
bool supports_receiver_{false};
};
/// RadioFrequency - Base class for radio frequency implementations
class RadioFrequency : public Component, public EntityBase, public remote_base::RemoteReceiverListener {
class RadioFrequency : public ir_rf_base::IrRfEntity {
public:
RadioFrequency() = default;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
/// Get the traits for this radio frequency implementation
RadioFrequencyTraits &get_traits() { return this->traits_; }
@@ -159,9 +89,6 @@ class RadioFrequency : public Component, public EntityBase, public remote_base::
/// Create a call object for transmitting
RadioFrequencyCall make_call() { return RadioFrequencyCall(this); }
/// Get capability flags for this radio frequency instance
uint32_t get_capability_flags() const;
/// Called when RF data is received (from RemoteReceiverListener)
bool on_receive(remote_base::RemoteReceiveData data) override;
@@ -180,11 +107,16 @@ class RadioFrequency : public Component, public EntityBase, public remote_base::
}
protected:
friend class RadioFrequencyCall;
friend class ir_rf_base::IrRfCall<RadioFrequencyCall, RadioFrequency>;
/// Fires the on_control hooks before the platform-specific control() runs
void on_call_(const RadioFrequencyCall &call) { this->control_callback_.call(call); }
/// Perform the actual transmission (called by RadioFrequencyCall::perform())
/// Platforms must override this to implement hardware-specific transmission.
virtual void control(const RadioFrequencyCall &call) = 0;
/// Returns false only when no frame was handed to the transmitter, in which case no
/// completion follows.
/// Without a remote_base transmitter, call api_transmit_done_() once the frame is out
virtual bool control(const RadioFrequencyCall &call) = 0;
// Traits describing capabilities
RadioFrequencyTraits traits_;
+2 -2
View File
@@ -136,8 +136,8 @@ async def attach_receiver(
add_listener(receiver, var)
async def register_transmittable(var, config):
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
async def register_transmittable(var, config, key: str = CONF_TRANSMITTER_ID):
transmitter_ = await cg.get_variable(config[key])
cg.add(var.set_transmitter(transmitter_))
@@ -20,7 +20,7 @@ void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) {
// Typically a DISH device needs to get a command a total of
// at least 4 times to accept it.
for (uint i = 0; i < 4; i++) {
for (uint32_t i = 0; i < 4; i++) {
// COMMAND (function, in MSB)
for (uint8_t mask = 1UL << 5; mask; mask >>= 1) {
if (data.command & mask) {
@@ -39,7 +39,7 @@ void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) {
}
}
// PADDING
for (uint j = 0; j < 6; j++)
for (uint32_t j = 0; j < 6; j++)
dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US);
// FOOTER
@@ -73,7 +73,7 @@ optional<DishData> DishProtocol::decode(RemoteReceiveData src) {
return {};
}
}
for (uint j = 0; j < 6; j++) {
for (uint32_t j = 0; j < 6; j++) {
if (!src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) {
return {};
}
@@ -182,7 +182,7 @@ bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) {
/* RemoteTransmitterBase */
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait, [[maybe_unused]] uint16_t seq) {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
const auto &vec = this->temp_.get_data();
char buffer[256];
@@ -213,6 +213,10 @@ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
if (pos != 0) {
ESP_LOGVV(TAG, "%s", buffer);
}
#endif
this->flush_pending_completion();
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
this->current_seq_ = seq;
#endif
this->send_internal(send_times, send_wait);
}
+32 -5
View File
@@ -143,6 +143,14 @@ class RemoteRMTChannel {
#endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32
class RemoteTransmitterBase;
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
/// Defined by ir_rf_base: answers the API request waiting on the entity that submitted seq;
/// sent is false when the platform never put the frame on the wire.
/// One function for the whole build instead of a callback list on every transmitter.
void ir_rf_transmit_complete(RemoteTransmitterBase *transmitter, uint16_t seq, bool sent);
#endif
// 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.
@@ -164,21 +172,24 @@ class RemoteTransmitterBase : public RemoteComponentBase {
RemoteTransmitterBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {}
class TransmitCall {
public:
explicit TransmitCall(RemoteTransmitterBase *parent) : parent_(parent) {}
TransmitCall(RemoteTransmitterBase *parent, uint16_t seq) : parent_(parent), seq_(seq) {}
RemoteTransmitData *get_data() { return &this->parent_->temp_; }
void set_send_times(uint32_t send_times) { send_times_ = send_times; }
void set_send_wait(uint32_t send_wait) { send_wait_ = send_wait; }
void perform() { this->parent_->send_(this->send_times_, this->send_wait_); }
/// Identifies this transmission in the completion hook
uint16_t get_seq() const { return this->seq_; }
void perform() { this->parent_->send_(this->send_times_, this->send_wait_, this->seq_); }
protected:
RemoteTransmitterBase *parent_;
uint32_t send_times_{1};
uint32_t send_wait_{0};
uint16_t seq_;
};
TransmitCall transmit() {
this->temp_.reset();
return TransmitCall(this);
return TransmitCall(this, this->take_seq_());
}
template<RemoteProtocolEncoder Protocol>
void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
@@ -190,9 +201,25 @@ class RemoteTransmitterBase : public RemoteComponentBase {
}
protected:
void send_(uint32_t send_times, uint32_t send_wait);
void send_(uint32_t send_times, uint32_t send_wait, uint16_t seq);
virtual void send_internal(uint32_t send_times, uint32_t send_wait) = 0;
void send_single_() { this->send_(1, 0); }
/// Platforms that report completion later wait out the previous frame here, before send_()
/// assigns the next seq, so that completion carries the seq it belongs to
virtual void flush_pending_completion() {}
void send_single_() { this->send_(1, 0, this->take_seq_()); }
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
/// Reports the frame handed to the platform last, after its final repeat and before the
/// on_complete trigger; a seq only has to be unique within the 30 s reply window
void notify_complete_(bool sent) { ir_rf_transmit_complete(this, this->current_seq_, sent); }
uint16_t take_seq_() { return ++this->next_seq_; }
uint16_t next_seq_{0};
uint16_t current_seq_{0};
#else
// seq tracking only exists for the API completion reply
void notify_complete_(bool /*sent*/) {}
static uint16_t take_seq_() { return 0; }
#endif
/// Use same vector for all transmits, avoids many allocations
RemoteTransmitData temp_;
@@ -114,7 +114,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
}
}
}
this->complete_trigger_.trigger();
this->fire_complete_(send_times != 0); // same answer as the ISR backend for a frame sent zero times
}
} // namespace esphome::remote_transmitter
@@ -82,6 +82,16 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
protected:
void send_internal(uint32_t send_times, uint32_t send_wait) override;
// the API reply is answered before the user's on_complete automation; sent is false on a
// bail-out that never put the frame on the wire, on_complete fires either way
void fire_complete_(bool sent = true) {
this->notify_complete_(sent);
this->complete_trigger_.trigger();
}
#if (defined(USE_ESP32) && SOC_RMT_SUPPORTED) || defined(USE_LIBRETINY_VARIANT_RTL8720C) || \
defined(REMOTE_TRANSMITTER_BK_PWM)
void flush_pending_completion() override;
#endif
#if defined(USE_ESP8266) || \
(defined(USE_LIBRETINY) && !defined(USE_LIBRETINY_VARIANT_RTL8720C) && !defined(REMOTE_TRANSMITTER_BK_PWM)) || \
defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
@@ -110,7 +110,7 @@ void RemoteTransmitterComponent::deliver_completion_() {
if (!this->stall_aborted_)
this->status_clear_warning();
this->complete_pending_ = false;
this->complete_trigger_.trigger();
this->fire_complete_(!this->stall_aborted_);
}
// Waits until no chain is in flight, delivering any deferred completions; a completion
@@ -152,20 +152,21 @@ void RemoteTransmitterComponent::arm_chain_(uint32_t send_times, uint32_t send_w
this->start_isr_item_(0);
}
void RemoteTransmitterComponent::flush_pending_completion() { this->wait_until_idle_(); }
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (!this->envelope_ready_()) {
// both triggers still fire, so an on_complete-sequenced automation does not stall
ESP_LOGW(TAG, "Cannot send: PWM not initialized");
this->transmit_trigger_.trigger();
this->deliver_completion_();
this->fire_complete_(false);
return;
}
this->wait_until_idle_();
if (send_times == 0) {
// parity with the loop-based implementations: transmit nothing, but both triggers
// still fire so an on_complete-sequenced automation does not stall
this->transmit_trigger_.trigger();
this->deliver_completion_();
this->fire_complete_(false);
return;
}
ESP_LOGD(TAG, "Sending remote code");
@@ -175,7 +176,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (this->isr_data_.empty()) {
ESP_LOGW(TAG, "Empty data");
this->transmit_trigger_.trigger();
this->deliver_completion_();
this->fire_complete_(false);
return;
}
// trigger first: the deadline computed in arm_chain_ must not be charged for user code
@@ -200,25 +200,35 @@ void RemoteTransmitterComponent::wait_for_rmt_() {
this->status_set_warning();
}
this->complete_trigger_.trigger();
this->fire_complete_(error == ESP_OK);
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
void RemoteTransmitterComponent::flush_pending_completion() {
// a frame still on the wire is waited out, and its completion reported, before the next one
if (this->non_blocking_ && this->cancel_timeout("complete")) {
this->wait_for_rmt_();
}
}
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
uint64_t total_duration = 0;
if (this->is_failed()) {
// both triggers still fire, so a paced API client or on_complete automation is not left waiting
this->transmit_trigger_.trigger();
this->fire_complete_(false);
return;
}
// if the timeout was cancelled, block until the tx is complete
if (this->non_blocking_ && this->cancel_timeout("complete")) {
this->wait_for_rmt_();
}
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
this->configure_rmt_();
if (this->is_failed()) { // the carrier change failed, there is no channel to send on
this->transmit_trigger_.trigger();
this->fire_complete_(false);
return;
}
}
this->rmt_temp_.clear();
@@ -258,6 +268,8 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) {
ESP_LOGE(TAG, "Empty data");
this->transmit_trigger_.trigger();
this->fire_complete_(false);
return;
}
@@ -273,9 +285,11 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
this->status_set_warning();
} else {
this->status_clear_warning();
// nothing was queued, so there is no frame to wait for
this->fire_complete_(false);
return;
}
this->status_clear_warning();
if (this->non_blocking_) {
this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); });
@@ -284,13 +298,23 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
}
}
#else
void RemoteTransmitterComponent::flush_pending_completion() {}
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->is_failed())
if (this->is_failed()) {
this->transmit_trigger_.trigger();
this->fire_complete_(false);
return;
}
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
this->configure_rmt_();
if (this->is_failed()) { // the carrier change failed, there is no channel to send on
this->transmit_trigger_.trigger();
this->fire_complete_(false);
return;
}
}
this->rmt_temp_.clear();
@@ -328,9 +352,12 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.empty()) {
ESP_LOGE(TAG, "Empty data");
this->transmit_trigger_.trigger();
this->fire_complete_(false);
return;
}
this->transmit_trigger_.trigger();
bool sent = send_times != 0; // same answer as the ISR backend for a frame sent zero times
for (uint32_t i = 0; i < send_times; i++) {
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
@@ -340,18 +367,21 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
this->status_set_warning();
} else {
this->status_clear_warning();
sent = false;
}
error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
sent = false;
}
if (i + 1 < send_times)
delayMicroseconds(send_wait);
}
this->complete_trigger_.trigger();
// a later repeat must not clear the warning a failed one raised
if (sent)
this->status_clear_warning();
this->fire_complete_(sent);
}
#endif
@@ -146,6 +146,8 @@ void RemoteTransmitterComponent::await_target_time_() {
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->pwm_ == nullptr) {
ESP_LOGW(TAG, "Cannot send: PWM not initialized");
this->transmit_trigger_.trigger();
this->fire_complete_(false);
return;
}
ESP_LOGD(TAG, "Sending remote code");
@@ -194,7 +196,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
}
}
}
this->complete_trigger_.trigger();
this->fire_complete_();
}
#endif // USE_LIBRETINY_VARIANT_RTL8720C
+2 -4
View File
@@ -2080,10 +2080,8 @@ void WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config,
set_json_icon_state_value(root, obj, "infrared", "", 0, start_config);
auto traits = obj->get_traits();
root[ESPHOME_F("supports_transmitter")] = traits.get_supports_transmitter();
root[ESPHOME_F("supports_receiver")] = traits.get_supports_receiver();
root[ESPHOME_F("supports_transmitter")] = obj->get_supports_transmitter();
root[ESPHOME_F("supports_receiver")] = obj->get_supports_receiver();
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
+1
View File
@@ -86,6 +86,7 @@
#define USE_IMPROV_BLE_STATE_CALLBACK
#define USE_INFRARED
#define USE_IR_RF
#define USE_IR_RF_TRANSMIT_COMPLETE
#define USE_JSON
#define USE_JSON_ARENA
#define USE_RADIO_FREQUENCY
@@ -149,7 +149,7 @@ BENCHMARK(Decode_SerialProxyWriteRequest);
// --- InfraredRFReceiveEvent encode (100 sint32 timings) +
// InfraredRFTransmitRawTimingsRequest decode (hand-built wire bytes) ---
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
// Mark/space pairs simulating a typical RC-5 / NEC capture (100 timings).
static std::vector<int32_t> make_ir_timings_100() {
@@ -275,6 +275,6 @@ static void Decode_InfraredRFTransmitRawTimingsRequest(benchmark::State &state)
}
BENCHMARK(Decode_InfraredRFTransmitRawTimingsRequest);
#endif // USE_IR_RF || USE_RADIO_FREQUENCY
#endif // USE_IR_RF
} // namespace esphome::api::benchmarks
@@ -19,7 +19,8 @@ class InfraredCall {
return *this;
}
InfraredCall &set_repeat_count(uint32_t /*count*/) { return *this; }
void perform() {}
template<typename T> InfraredCall &set_api_connection(T * /*conn*/) { return *this; }
bool perform() { return false; }
protected:
Infrared *parent_;
@@ -37,6 +38,7 @@ class Infrared : public Component, public EntityBase {
const InfraredTraits &get_traits() const { return this->traits_; }
InfraredCall make_call() { return InfraredCall(this); }
uint32_t get_capability_flags() const { return 0; }
template<typename T> void on_api_connection_closed(T * /*conn*/) {}
protected:
InfraredTraits traits_;
@@ -23,7 +23,8 @@ class RadioFrequencyCall {
RadioFrequencyCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) {
return *this;
}
void perform() {}
template<typename T> RadioFrequencyCall &set_api_connection(T * /*conn*/) { return *this; }
bool perform() { return false; }
protected:
RadioFrequency *parent_;
@@ -43,6 +44,7 @@ class RadioFrequency : public Component, public EntityBase {
const RadioFrequencyTraits &get_traits() const { return this->traits_; }
RadioFrequencyCall make_call() { return RadioFrequencyCall(this); }
uint32_t get_capability_flags() const { return 0; }
template<typename T> void on_api_connection_closed(T * /*conn*/) {}
protected:
RadioFrequencyTraits traits_;
@@ -0,0 +1,25 @@
"""Host-only stand-in for remote_transmitter used by the IR/RF integration tests."""
import esphome.codegen as cg
from esphome.components import remote_base
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/tests"]
MULTI_CONF = True
AUTO_LOAD = ["remote_base"]
remote_transmitter_mock_ns = cg.esphome_ns.namespace("remote_transmitter_mock")
MockRemoteTransmitter = remote_transmitter_mock_ns.class_(
"MockRemoteTransmitter", remote_base.RemoteTransmitterBase, cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{cv.GenerateID(): cv.declare_id(MockRemoteTransmitter)}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -0,0 +1,40 @@
#include "remote_transmitter_mock.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome::remote_transmitter_mock {
static const char *const TAG = "remote_transmitter_mock";
void MockRemoteTransmitter::dump_config() { ESP_LOGCONFIG(TAG, "Mock Remote Transmitter"); }
void MockRemoteTransmitter::flush_pending_completion() {
if (!this->busy_)
return;
// The RMT backend blocks here until the hardware is idle; the mock only reports the overlap
ESP_LOGW(TAG, "Overlap: a frame was requested while seq=%" PRIu16 " was in flight", this->current_seq_);
this->cancel_timeout("complete");
this->finish_();
}
void MockRemoteTransmitter::send_internal(uint32_t send_times, uint32_t send_wait) {
uint64_t total_us = static_cast<uint64_t>(send_wait) * (send_times - 1);
for (int32_t value : this->temp_.get_data()) {
total_us += static_cast<uint64_t>(value < 0 ? -value : value) * send_times;
}
const auto duration_ms = static_cast<uint32_t>(total_us / 1000);
this->busy_ = true;
ESP_LOGI(TAG, "TX seq=%" PRIu16 " timings=%zu repeat=%" PRIu32 " duration=%" PRIu32 "ms", this->current_seq_,
this->temp_.get_data().size(), send_times, duration_ms);
this->set_timeout("complete", duration_ms, [this]() { this->finish_(); });
}
void MockRemoteTransmitter::finish_() {
this->busy_ = false;
ESP_LOGI(TAG, "Complete seq=%" PRIu16, this->current_seq_);
this->notify_complete_(true);
}
} // namespace esphome::remote_transmitter_mock
@@ -0,0 +1,31 @@
#pragma once
// ============================================================================
// HOST-ONLY TEST COMPONENT — DO NOT COPY TO PRODUCTION CODE
//
// Emulates a non-blocking remote transmitter: a frame "occupies" the
// transmitter for its real duration and the completion callback fires from a
// scheduler timeout, the same way the ESP32 RMT backend reports completion.
// A frame submitted while another is in flight is logged as an overlap; the
// real backends block the main loop in that case.
// ============================================================================
#include "esphome/core/component.h"
#include "esphome/components/remote_base/remote_base.h"
namespace esphome::remote_transmitter_mock {
class MockRemoteTransmitter : public remote_base::RemoteTransmitterBase, public Component {
public:
MockRemoteTransmitter() : remote_base::RemoteTransmitterBase(nullptr) {}
void dump_config() override;
protected:
void flush_pending_completion() override;
void send_internal(uint32_t send_times, uint32_t send_wait) override;
void finish_();
bool busy_{false};
};
} // namespace esphome::remote_transmitter_mock
@@ -0,0 +1,30 @@
esphome:
name: ir-rf-transmit-complete-test
host:
api:
logger:
level: DEBUG
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
remote_transmitter_mock:
- id: rf_tx
- id: ir_tx
# two entities on one transmitter: each must get only its own completion
radio_frequency:
- platform: ir_rf_proxy
name: RF Transmitter A
remote_transmitter_id: rf_tx
- platform: ir_rf_proxy
name: RF Transmitter B
remote_transmitter_id: rf_tx
infrared:
- platform: ir_rf_proxy
name: IR Transmitter
remote_transmitter_id: ir_tx
@@ -0,0 +1,131 @@
"""IR/RF transmit completion replies (API 1.18) and the client pacing built on them.
The transmitter is a host-only mock that takes a frame's real duration to
"send" it and reports completion from a scheduler timeout, like the ESP32 RMT
backend. The client is expected to hold the next frame until the device
replies, so the mock never sees an overlapping frame.
"""
from __future__ import annotations
import asyncio
import re
from aioesphomeapi import InfraredInfo, RadioFrequencyInfo
from aioesphomeapi.api_pb2 import InfraredRFTransmitCompleteResponse
import pytest
from .state_utils import find_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
FRAME_COUNT = 5
# 10 marks and 10 spaces of 500 us, sent twice: 20 ms per frame
TIMINGS = [500, -500] * 10
REPEAT = 2
MOCK_EVENT = re.compile(
r"remote_transmitter_mock[^\]]*\]: (TX|Complete|Overlap)\b.*?seq=(\d+)"
)
@pytest.mark.shared_yaml("ir_rf_transmit_complete")
@pytest.mark.asyncio
async def test_ir_rf_transmit_complete_boot(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""The host build with the mock transmitters boots and lists all entities on any client."""
async with run_compiled(yaml_config), api_client_connected() as client:
entities, _ = await client.list_entities_services()
assert find_entity(entities, "rf_transmitter_a", RadioFrequencyInfo) is not None
assert find_entity(entities, "rf_transmitter_b", RadioFrequencyInfo) is not None
assert find_entity(entities, "ir_transmitter", InfraredInfo) is not None
@pytest.mark.shared_yaml("ir_rf_transmit_complete")
@pytest.mark.asyncio
async def test_ir_rf_transmit_complete(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Frames are answered once they leave the transmitter, never overlap, and a refused
request is answered at once. Two RF entities share a transmitter and each gets its own
reply; the infrared entity on its own transmitter is answered the same way."""
loop = asyncio.get_running_loop()
events: list[tuple[str, int]] = []
all_sent = loop.create_future()
all_logged = loop.create_future()
def line_callback(line: str) -> None:
if (match := MOCK_EVENT.search(line)) is None:
return
events.append((match.group(1), int(match.group(2))))
if (
match.group(1) == "Complete"
and not all_sent.done()
and sum(kind == "Complete" for kind, _ in events) == FRAME_COUNT
):
all_sent.set_result(None)
# the log reader stops with the device, so wait for the last line before asserting on it
if len(events) == 2 * (FRAME_COUNT + 1) and not all_logged.done():
all_logged.set_result(None)
completions: list[InfraredRFTransmitCompleteResponse] = []
all_replied = loop.create_future()
ir_replied = loop.create_future()
refused = loop.create_future()
def on_complete(msg: InfraredRFTransmitCompleteResponse) -> None:
if not msg.success:
if not refused.done():
refused.set_result(msg)
return
completions.append(msg)
if len(completions) == FRAME_COUNT and not all_replied.done():
all_replied.set_result(None)
if len(completions) == FRAME_COUNT + 1 and not ir_replied.done():
ir_replied.set_result(None)
async with (
run_compiled(yaml_config, line_callback=line_callback),
api_client_connected() as client,
):
entities, _ = await client.list_entities_services()
rf = find_entity(entities, "rf_transmitter_a", RadioFrequencyInfo)
rf_b = find_entity(entities, "rf_transmitter_b", RadioFrequencyInfo)
ir = find_entity(entities, "ir_transmitter", InfraredInfo)
assert rf is not None and rf_b is not None, "RF transmitter entities not found"
assert ir is not None, "IR transmitter entity not found"
client._connection.add_message_callback(
on_complete, (InfraredRFTransmitCompleteResponse,)
)
# alternate between the two entities sharing the transmitter
keys = [rf.key if i % 2 == 0 else rf_b.key for i in range(FRAME_COUNT)]
for key in keys:
client.radio_frequency_transmit_raw_timings(
key, 433920000, TIMINGS, repeat_count=REPEAT
)
await asyncio.wait_for(all_replied, timeout=10)
await asyncio.wait_for(all_sent, timeout=10)
# the infrared entity goes through the same path on its own transmitter
client.infrared_rf_transmit_raw_timings(ir.key, 38000, TIMINGS)
await asyncio.wait_for(ir_replied, timeout=10)
await asyncio.wait_for(all_logged, timeout=10)
# A request the entity refuses is answered right away with success false;
# no timings, so the proxy rejects it before it reaches the transmitter
client.radio_frequency_transmit_raw_timings(rf.key, 433920000, [])
refused_msg = await asyncio.wait_for(refused, timeout=10)
assert [msg.key for msg in completions] == [*keys, ir.key]
assert refused_msg.key == rf.key
# The mocks saw one frame at a time: every transmit follows the previous completion
kinds = [kind for kind, _ in events]
assert kinds == ["TX", "Complete"] * (FRAME_COUNT + 1), events
seqs = [seq for _, seq in events]
assert seqs[::2] == seqs[1::2], events