1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00

[infrared] Implement experimental API/Core/component for new component/entity type (#13129)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Keith Burzinski
2026-01-11 23:01:23 -06:00
committed by GitHub
parent 595217786c
commit 83eebdf15d
24 changed files with 750 additions and 1 deletions

View File

@@ -249,6 +249,7 @@ esphome/components/ina260/* @mreditor97
esphome/components/ina2xx_base/* @latonita esphome/components/ina2xx_base/* @latonita
esphome/components/ina2xx_i2c/* @latonita esphome/components/ina2xx_i2c/* @latonita
esphome/components/ina2xx_spi/* @latonita esphome/components/ina2xx_spi/* @latonita
esphome/components/infrared/* @kbx81
esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkbird_ibsth1_mini/* @fkirill
esphome/components/inkplate/* @jesserockz @JosipKuci esphome/components/inkplate/* @jesserockz @JosipKuci
esphome/components/integration/* @OttoWinter esphome/components/integration/* @OttoWinter

View File

@@ -66,6 +66,8 @@ service APIConnection {
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {} rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
} }
@@ -2437,3 +2439,49 @@ message ZWaveProxyRequest {
ZWaveProxyRequestType type = 1; ZWaveProxyRequestType type = 1;
bytes data = 2; bytes data = 2;
} }
// ==================== INFRARED ====================
// Note: Feature and capability flag enums are defined in
// esphome/components/infrared/infrared.h
// Listing of infrared instances
message ListEntitiesInfraredResponse {
option (id) = 135;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_INFRARED";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags
}
// Command to transmit infrared/RF data using raw timings
message InfraredRFTransmitRawTimingsRequest {
option (id) = 136;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_IR_RF";
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2; // Key identifying the transmitter instance
uint32 carrier_frequency = 3; // Carrier frequency in Hz
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off)
}
// Event message for received infrared/RF data
message InfraredRFReceiveEvent {
option (id) = 137;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_IR_RF";
option (no_delay) = true;
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}

View File

@@ -46,6 +46,9 @@
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h" #include "esphome/components/water_heater/water_heater.h"
#endif #endif
#ifdef USE_INFRARED
#include "esphome/components/infrared/infrared.h"
#endif
namespace esphome::api { namespace esphome::api {
@@ -1438,6 +1441,35 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
} }
#endif #endif
#ifdef USE_IR_RF
void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) {
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
// and dispatch to the appropriate entity type based on that field.
#ifdef USE_INFRARED
ENTITY_COMMAND_MAKE_CALL(infrared::Infrared, infrared, infrared)
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.perform();
#endif
}
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) {
this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE);
}
#endif
#ifdef USE_INFRARED
uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *infrared = static_cast<infrared::Infrared *>(entity);
ListEntitiesInfraredResponse msg;
msg.capabilities = infrared->get_capability_flags();
return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
}
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool APIConnection::send_update_state(update::UpdateEntity *update) { bool APIConnection::send_update_state(update::UpdateEntity *update) {
return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE, return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE,

View File

@@ -172,6 +172,11 @@ class APIConnection final : public APIServerConnection {
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif #endif
#ifdef USE_IR_RF
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override;
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
void send_event(event::Event *event, StringRef event_type); void send_event(event::Event *event, StringRef event_type);
#endif #endif
@@ -468,6 +473,10 @@ class APIConnection final : public APIServerConnection {
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
#endif #endif
#ifdef USE_INFRARED
static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);

View File

@@ -3347,5 +3347,98 @@ void ZWaveProxyRequest::calculate_size(ProtoSize &size) const {
size.add_length(1, this->data_len); size.add_length(1, this->data_len);
} }
#endif #endif
#ifdef USE_INFRARED
void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name);
#ifdef USE_ENTITY_ICON
buffer.encode_string(4, this->icon);
#endif
buffer.encode_bool(5, this->disabled_by_default);
buffer.encode_uint32(6, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
buffer.encode_uint32(7, this->device_id);
#endif
buffer.encode_uint32(8, this->capabilities);
}
void ListEntitiesInfraredResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id.size());
size.add_fixed32(1, this->key);
size.add_length(1, this->name.size());
#ifdef USE_ENTITY_ICON
size.add_length(1, this->icon.size());
#endif
size.add_bool(1, this->disabled_by_default);
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_uint32(1, this->capabilities);
}
#endif
#ifdef USE_IR_RF
bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
#ifdef USE_DEVICES
case 1:
this->device_id = value.as_uint32();
break;
#endif
case 3:
this->carrier_frequency = value.as_uint32();
break;
case 4:
this->repeat_count = value.as_uint32();
break;
default:
return false;
}
return true;
}
bool InfraredRFTransmitRawTimingsRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 5: {
this->timings_data_ = value.data();
this->timings_length_ = value.size();
this->timings_count_ = count_packed_varints(value.data(), value.size());
break;
}
default:
return false;
}
return true;
}
bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2:
this->key = value.as_fixed32();
break;
default:
return false;
}
return true;
}
void InfraredRFReceiveEvent::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_DEVICES
buffer.encode_uint32(1, this->device_id);
#endif
buffer.encode_fixed32(2, this->key);
for (const auto &it : *this->timings) {
buffer.encode_sint32(3, it, true);
}
}
void InfraredRFReceiveEvent::calculate_size(ProtoSize &size) const {
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_fixed32(1, this->key);
if (!this->timings->empty()) {
for (const auto &it : *this->timings) {
size.add_sint32_force(1, it);
}
}
}
#endif
} // namespace esphome::api } // namespace esphome::api

View File

@@ -3049,5 +3049,70 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage {
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
#endif #endif
#ifdef USE_INFRARED
class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 135;
static constexpr uint8_t ESTIMATED_SIZE = 44;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_infrared_response"; }
#endif
uint32_t capabilities{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
#endif
#ifdef USE_IR_RF
class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 136;
static constexpr uint8_t ESTIMATED_SIZE = 220;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "infrared_rf_transmit_raw_timings_request"; }
#endif
#ifdef USE_DEVICES
uint32_t device_id{0};
#endif
uint32_t key{0};
uint32_t carrier_frequency{0};
uint32_t repeat_count{0};
const uint8_t *timings_data_{nullptr};
uint16_t timings_length_{0};
uint16_t timings_count_{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class InfraredRFReceiveEvent final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 137;
static constexpr uint8_t ESTIMATED_SIZE = 17;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "infrared_rf_receive_event"; }
#endif
#ifdef USE_DEVICES
uint32_t device_id{0};
#endif
uint32_t key{0};
const std::vector<int32_t> *timings{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
#endif
} // namespace esphome::api } // namespace esphome::api

View File

@@ -2309,6 +2309,50 @@ void ZWaveProxyRequest::dump_to(std::string &out) const {
out.append("\n"); out.append("\n");
} }
#endif #endif
#ifdef USE_INFRARED
void ListEntitiesInfraredResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesInfraredResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "capabilities", this->capabilities);
}
#endif
#ifdef USE_IR_RF
void InfraredRFTransmitRawTimingsRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "InfraredRFTransmitRawTimingsRequest");
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "key", this->key);
dump_field(out, "carrier_frequency", this->carrier_frequency);
dump_field(out, "repeat_count", this->repeat_count);
out.append(" timings: ");
out.append("packed buffer [");
out.append(std::to_string(this->timings_count_));
out.append(" values, ");
out.append(std::to_string(this->timings_length_));
out.append(" bytes]\n");
}
void InfraredRFReceiveEvent::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "InfraredRFReceiveEvent");
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "key", this->key);
for (const auto &it : *this->timings) {
dump_field(out, "timings", it, 4);
}
}
#endif
} // namespace esphome::api } // namespace esphome::api

View File

@@ -621,6 +621,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_water_heater_command_request(msg); this->on_water_heater_command_request(msg);
break; break;
} }
#endif
#ifdef USE_IR_RF
case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: {
InfraredRFTransmitRawTimingsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_infrared_rf_transmit_raw_timings_request: %s", msg.dump().c_str());
#endif
this->on_infrared_rf_transmit_raw_timings_request(msg);
break;
}
#endif #endif
default: default:
break; break;
@@ -819,6 +830,11 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
#endif #endif
#ifdef USE_IR_RF
void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
this->infrared_rf_transmit_raw_timings(msg);
}
#endif
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements for messages // Check authentication/connection requirements for messages

View File

@@ -217,6 +217,11 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
#endif #endif
#ifdef USE_IR_RF
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
#endif
protected: protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
}; };
@@ -347,6 +352,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif #endif
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0; virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0;
#endif
#ifdef USE_IR_RF
virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0;
#endif #endif
protected: protected:
void on_hello_request(const HelloRequest &msg) override; void on_hello_request(const HelloRequest &msg) override;
@@ -473,6 +481,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif #endif
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif
#ifdef USE_IR_RF
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
#endif #endif
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
}; };

View File

@@ -347,6 +347,21 @@ void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) {
} }
#endif #endif
#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{};
#ifdef USE_DEVICES
resp.device_id = device_id;
#endif
resp.key = key;
resp.timings = timings;
for (auto &c : this->clients_)
c->send_infrared_rf_receive_event(resp);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
#endif #endif

View File

@@ -185,6 +185,9 @@ class APIServer : public Component,
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
#endif #endif
#ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
bool is_connected(bool state_subscription_only = false) const; bool is_connected(bool state_subscription_only = false) const;

View File

@@ -76,6 +76,9 @@ LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPane
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse) LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
#endif #endif
#ifdef USE_INFRARED
LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse)
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif #endif

View File

@@ -85,6 +85,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override; bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif #endif
#ifdef USE_INFRARED
bool on_infrared(infrared::Infrared *entity) override;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *entity) override; bool on_event(event::Event *entity) override;
#endif #endif

View File

@@ -79,6 +79,9 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override; bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif #endif
#ifdef USE_INFRARED
bool on_infrared(infrared::Infrared *infrared) override { return true; };
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; }; bool on_event(event::Event *event) override { return true; };
#endif #endif

View File

@@ -0,0 +1,76 @@
"""
Infrared component for ESPHome.
WARNING: This component is EXPERIMENTAL. The API (both Python configuration
and C++ interfaces) 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.
"""
import esphome.codegen as cg
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 setup_entity
from esphome.coroutine import CoroPriority
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
AUTO_LOAD = ["remote_base"]
IS_PLATFORM_COMPONENT = True
infrared_ns = cg.esphome_ns.namespace("infrared")
Infrared = infrared_ns.class_("Infrared", cg.EntityBase, cg.Component)
InfraredCall = infrared_ns.class_("InfraredCall")
InfraredTraits = infrared_ns.class_("InfraredTraits")
CONF_INFRARED_ID = "infrared_id"
CONF_SUPPORTS_TRANSMITTER = "supports_transmitter"
CONF_SUPPORTS_RECEIVER = "supports_receiver"
def infrared_schema(class_: type[cg.MockObjClass]) -> cv.Schema:
"""Create a schema for an infrared platform.
:param class_: The infrared class to use for this schema.
:return: An extended schema for infrared configuration.
"""
entity_schema = cv.ENTITY_BASE_SCHEMA.extend(cv.COMPONENT_SCHEMA)
return entity_schema.extend(
{
cv.GenerateID(): cv.declare_id(class_),
}
)
async def setup_infrared_core_(var: cg.Pvariable, config: ConfigType) -> None:
"""Set up core infrared configuration."""
await setup_entity(var, config, "infrared")
async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None:
"""Register an infrared device with the core."""
cg.add_define("USE_IR_RF")
await cg.register_component(var, config)
await setup_infrared_core_(var, config)
cg.add(cg.App.register_infrared(var))
CORE.register_platform_component("infrared", var)
async def new_infrared(config: ConfigType, *args) -> cg.Pvariable:
"""Create a new Infrared instance.
:param config: Configuration dictionary.
:param args: Additional arguments to pass to new_Pvariable.
:return: The created Infrared instance.
"""
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_infrared(var, config)
return var
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config: ConfigType) -> None:
cg.add_global(infrared_ns.using)

View File

@@ -0,0 +1,150 @@
#include "infrared.h"
#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; // Clear packed if vector is set
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; // Clear vector if packed is set
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());
// Register as listener for received IR data
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
}
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()));
}
InfraredCall Infrared::make_call() { return InfraredCall(this); }
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
if (call.get_carrier_frequency().has_value()) {
transmit_data->set_carrier_frequency(call.get_carrier_frequency().value());
}
// Set timings based on format
if (call.is_packed()) {
// Zero-copy from packed protobuf data
ESP_LOGD(TAG, "Transmitting raw timings: timing_count=%u, repeat_count=%u", call.get_packed_count(),
call.get_repeat_count());
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
call.get_packed_count());
} else {
// From vector (lambdas/automations)
const auto &timings = call.get_raw_timings();
if (timings.empty()) {
ESP_LOGE(TAG, "Raw timings array is empty");
return;
}
ESP_LOGD(TAG, "Transmitting raw timings: timing_count=%zu, repeat_count=%u", timings.size(),
call.get_repeat_count());
// Timings format: positive values = mark (LED on), negative values = space (LED off)
for (const auto &timing : timings) {
if (timing > 0) {
transmit_data->mark(static_cast<uint32_t>(timing));
} else {
transmit_data->space(static_cast<uint32_t>(-timing));
}
}
}
// 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
}
} // namespace esphome::infrared

View File

@@ -0,0 +1,130 @@
#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/components/remote_base/remote_base.h"
#include <vector>
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
};
/// Forward declarations
class Infrared;
/// InfraredCall - Builder pattern for transmitting infrared signals
class InfraredCall {
public:
explicit InfraredCall(Infrared *parent) : parent_(parent) {}
/// Set the carrier frequency in Hz
InfraredCall &set_carrier_frequency(uint32_t frequency);
/// Set the raw timings (positive = mark, negative = space)
/// Note: The timings vector must outlive the InfraredCall (zero-copy reference)
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
/// Set the raw timings from packed protobuf sint32 data (zero-copy from wire)
/// Note: The data must outlive the InfraredCall
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();
/// 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, not packed)
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
/// Check if raw timings have been set (either vector or packed)
bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; }
/// Check if using packed data format
bool is_packed() const { return this->packed_data_ != nullptr; }
/// 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_;
// Vector-based timings (for lambdas/automations)
const std::vector<int32_t> *raw_timings_{nullptr};
// Packed protobuf timings (for API zero-copy)
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; }
protected:
bool supports_transmitter_{false};
bool supports_receiver_{false};
};
/// Infrared - Base class for infrared remote control implementations
class Infrared : public Component, public EntityBase, public remote_base::RemoteReceiverListener {
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
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_; }
const InfraredTraits &get_traits() const { return this->traits_; }
/// Create a call object for transmitting
InfraredCall make_call();
/// 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;
/// 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};
// Traits describing capabilities
InfraredTraits traits_;
};
} // namespace esphome::infrared

View File

@@ -141,6 +141,13 @@ bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) {
} }
#endif #endif
#ifdef USE_INFRARED
bool ListEntitiesIterator::on_infrared(infrared::Infrared *obj) {
// Infrared web_server support not yet implemented - this stub acknowledges the entity
return true;
}
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool ListEntitiesIterator::on_event(event::Event *obj) { bool ListEntitiesIterator::on_event(event::Event *obj) {
// Null event type, since we are just iterating over entities // Null event type, since we are just iterating over entities

View File

@@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *obj) override; bool on_water_heater(water_heater::WaterHeater *obj) override;
#endif #endif
#ifdef USE_INFRARED
bool on_infrared(infrared::Infrared *obj) override;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *obj) override; bool on_event(event::Event *obj) override;
#endif #endif

View File

@@ -91,6 +91,9 @@
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h" #include "esphome/components/water_heater/water_heater.h"
#endif #endif
#ifdef USE_INFRARED
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
#include "esphome/components/event/event.h" #include "esphome/components/event/event.h"
#endif #endif
@@ -223,6 +226,10 @@ class Application {
void register_water_heater(water_heater::WaterHeater *water_heater) { this->water_heaters_.push_back(water_heater); } void register_water_heater(water_heater::WaterHeater *water_heater) { this->water_heaters_.push_back(water_heater); }
#endif #endif
#ifdef USE_INFRARED
void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); }
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
void register_event(event::Event *event) { this->events_.push_back(event); } void register_event(event::Event *event) { this->events_.push_back(event); }
#endif #endif
@@ -457,6 +464,11 @@ class Application {
GET_ENTITY_METHOD(water_heater::WaterHeater, water_heater, water_heaters) GET_ENTITY_METHOD(water_heater::WaterHeater, water_heater, water_heaters)
#endif #endif
#ifdef USE_INFRARED
auto &get_infrareds() const { return this->infrareds_; }
GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds)
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
auto &get_events() const { return this->events_; } auto &get_events() const { return this->events_; }
GET_ENTITY_METHOD(event::Event, event, events) GET_ENTITY_METHOD(event::Event, event, events)
@@ -656,6 +668,9 @@ class Application {
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
StaticVector<water_heater::WaterHeater *, ESPHOME_ENTITY_WATER_HEATER_COUNT> water_heaters_{}; StaticVector<water_heater::WaterHeater *, ESPHOME_ENTITY_WATER_HEATER_COUNT> water_heaters_{};
#endif #endif
#ifdef USE_INFRARED
StaticVector<infrared::Infrared *, ESPHOME_ENTITY_INFRARED_COUNT> infrareds_{};
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{}; StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{};
#endif #endif

View File

@@ -169,6 +169,12 @@ void ComponentIterator::advance() {
break; break;
#endif #endif
#ifdef USE_INFRARED
case IteratorState::INFRARED:
this->process_platform_item_(App.get_infrareds(), &ComponentIterator::on_infrared);
break;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
case IteratorState::EVENT: case IteratorState::EVENT:
this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); this->process_platform_item_(App.get_events(), &ComponentIterator::on_event);

View File

@@ -16,6 +16,12 @@ class UserServiceDescriptor;
} // namespace api } // namespace api
#endif #endif
#ifdef USE_INFRARED
namespace infrared {
class Infrared;
} // namespace infrared
#endif
class ComponentIterator { class ComponentIterator {
public: public:
void begin(bool include_internal = false); void begin(bool include_internal = false);
@@ -87,6 +93,9 @@ class ComponentIterator {
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
virtual bool on_water_heater(water_heater::WaterHeater *water_heater) = 0; virtual bool on_water_heater(water_heater::WaterHeater *water_heater) = 0;
#endif #endif
#ifdef USE_INFRARED
virtual bool on_infrared(infrared::Infrared *infrared) = 0;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
virtual bool on_event(event::Event *event) = 0; virtual bool on_event(event::Event *event) = 0;
#endif #endif
@@ -167,6 +176,9 @@ class ComponentIterator {
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
WATER_HEATER, WATER_HEATER,
#endif #endif
#ifdef USE_INFRARED
INFRARED,
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
EVENT, EVENT,
#endif #endif

View File

@@ -49,6 +49,8 @@
#define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT
#define USE_IMAGE #define USE_IMAGE
#define USE_IMPROV_SERIAL_NEXT_URL #define USE_IMPROV_SERIAL_NEXT_URL
#define USE_INFRARED
#define USE_IR_RF
#define USE_JSON #define USE_JSON
#define USE_LIGHT #define USE_LIGHT
#define USE_LOCK #define USE_LOCK
@@ -321,9 +323,9 @@
// Default counts for static analysis // Default counts for static analysis
#define CONTROLLER_REGISTRY_MAX 2 #define CONTROLLER_REGISTRY_MAX 2
#define ESPHOME_AREA_COUNT 10
#define ESPHOME_COMPONENT_COUNT 50 #define ESPHOME_COMPONENT_COUNT 50
#define ESPHOME_DEVICE_COUNT 10 #define ESPHOME_DEVICE_COUNT 10
#define ESPHOME_AREA_COUNT 10
#define ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT 1 #define ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT 1
#define ESPHOME_ENTITY_BINARY_SENSOR_COUNT 1 #define ESPHOME_ENTITY_BINARY_SENSOR_COUNT 1
#define ESPHOME_ENTITY_BUTTON_COUNT 1 #define ESPHOME_ENTITY_BUTTON_COUNT 1
@@ -333,6 +335,7 @@
#define ESPHOME_ENTITY_DATETIME_COUNT 1 #define ESPHOME_ENTITY_DATETIME_COUNT 1
#define ESPHOME_ENTITY_EVENT_COUNT 1 #define ESPHOME_ENTITY_EVENT_COUNT 1
#define ESPHOME_ENTITY_FAN_COUNT 1 #define ESPHOME_ENTITY_FAN_COUNT 1
#define ESPHOME_ENTITY_INFRARED_COUNT 1
#define ESPHOME_ENTITY_LIGHT_COUNT 1 #define ESPHOME_ENTITY_LIGHT_COUNT 1
#define ESPHOME_ENTITY_LOCK_COUNT 1 #define ESPHOME_ENTITY_LOCK_COUNT 1
#define ESPHOME_ENTITY_MEDIA_PLAYER_COUNT 1 #define ESPHOME_ENTITY_MEDIA_PLAYER_COUNT 1

View File

@@ -37,3 +37,4 @@ datetime:
event: event:
update: update:
water_heater: water_heater:
infrared: