diff --git a/CODEOWNERS b/CODEOWNERS index bdcc86ef0c..48318ee064 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -249,6 +249,7 @@ esphome/components/ina260/* @mreditor97 esphome/components/ina2xx_base/* @latonita esphome/components/ina2xx_i2c/* @latonita esphome/components/ina2xx_spi/* @latonita +esphome/components/infrared/* @kbx81 esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkplate/* @jesserockz @JosipKuci esphome/components/integration/* @OttoWinter diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d6384456d5..597da25883 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -66,6 +66,8 @@ service APIConnection { rpc zwave_proxy_frame(ZWaveProxyFrame) 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; 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"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d6f0d84550..65f8c1a8cc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -46,6 +46,9 @@ #ifdef USE_WATER_HEATER #include "esphome/components/water_heater/water_heater.h" #endif +#ifdef USE_INFRARED +#include "esphome/components/infrared/infrared.h" +#endif namespace esphome::api { @@ -1438,6 +1441,35 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c } #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(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 bool APIConnection::send_update_state(update::UpdateEntity *update) { return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE, diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0289b3d2ff..b3d072ff69 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -172,6 +172,11 @@ class APIConnection final : public APIServerConnection { void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; #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 void send_event(event::Event *event, StringRef event_type); #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, bool is_single); #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 static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, uint32_t remaining_size, bool is_single); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 03a6639b5e..743f51dac7 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3347,5 +3347,98 @@ void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } #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(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(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 diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index e21b8596ca..0ab38b8b85 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -3049,5 +3049,70 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #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 *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 diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 160a9a93c9..8e4d55d11b 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2309,6 +2309,50 @@ void ZWaveProxyRequest::dump_to(std::string &out) const { out.append("\n"); } #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(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 diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index c9bf638ad7..576b802443 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -621,6 +621,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_water_heater_command_request(msg); 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 default: break; @@ -819,6 +830,11 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th #ifdef USE_ZWAVE_PROXY void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } #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) { // Check authentication/connection requirements for messages diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index e2a23827dc..4bd6a7b6a4 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -217,6 +217,11 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_ZWAVE_PROXY virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif + +#ifdef USE_IR_RF + virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; +#endif + protected: 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 #ifdef USE_ZWAVE_PROXY 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 protected: void on_hello_request(const HelloRequest &msg) override; @@ -473,6 +481,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_ZWAVE_PROXY 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 void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 336672f50b..949262098f 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -347,6 +347,21 @@ void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { } #endif +#ifdef USE_IR_RF +void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key, + const std::vector *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 API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f5b57f994a..93421ef801 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -185,6 +185,9 @@ class APIServer : public Component, #ifdef USE_ZWAVE_PROXY void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); #endif +#ifdef USE_IR_RF + void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector *timings); +#endif bool is_connected(bool state_subscription_only = false) const; diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 2470899c93..fe43a47c3b 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -76,6 +76,9 @@ LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPane #ifdef USE_WATER_HEATER LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse) #endif +#ifdef USE_INFRARED +LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse) +#endif #ifdef USE_EVENT LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 04e6525eb0..912aab72b2 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -85,6 +85,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_WATER_HEATER bool on_water_heater(water_heater::WaterHeater *entity) override; #endif +#ifdef USE_INFRARED + bool on_infrared(infrared::Infrared *entity) override; +#endif #ifdef USE_EVENT bool on_event(event::Event *entity) override; #endif diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 9230000ace..3c9f33835a 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -79,6 +79,9 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_WATER_HEATER bool on_water_heater(water_heater::WaterHeater *entity) override; #endif +#ifdef USE_INFRARED + bool on_infrared(infrared::Infrared *infrared) override { return true; }; +#endif #ifdef USE_EVENT bool on_event(event::Event *event) override { return true; }; #endif diff --git a/esphome/components/infrared/__init__.py b/esphome/components/infrared/__init__.py new file mode 100644 index 0000000000..5c759d6fd9 --- /dev/null +++ b/esphome/components/infrared/__init__.py @@ -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) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp new file mode 100644 index 0000000000..384ff431a5 --- /dev/null +++ b/esphome/components/infrared/infrared.cpp @@ -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 &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(timing)); + } else { + transmit_data->space(static_cast(-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 diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h new file mode 100644 index 0000000000..3a891301f4 --- /dev/null +++ b/esphome/components/infrared/infrared.h @@ -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 + +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 &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 &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 &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 carrier_frequency_; + // Vector-based timings (for lambdas/automations) + const std::vector *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 diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 1e852f6a96..0af9521326 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -141,6 +141,13 @@ bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) { } #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 bool ListEntitiesIterator::on_event(event::Event *obj) { // Null event type, since we are just iterating over entities diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 56fd91a8c6..d0a4fa2725 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_WATER_HEATER bool on_water_heater(water_heater::WaterHeater *obj) override; #endif +#ifdef USE_INFRARED + bool on_infrared(infrared::Infrared *obj) override; +#endif #ifdef USE_EVENT bool on_event(event::Event *obj) override; #endif diff --git a/esphome/core/application.h b/esphome/core/application.h index 13461b3ebd..592bf809f1 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -91,6 +91,9 @@ #ifdef USE_WATER_HEATER #include "esphome/components/water_heater/water_heater.h" #endif +#ifdef USE_INFRARED +#include "esphome/components/infrared/infrared.h" +#endif #ifdef USE_EVENT #include "esphome/components/event/event.h" #endif @@ -223,6 +226,10 @@ class Application { void register_water_heater(water_heater::WaterHeater *water_heater) { this->water_heaters_.push_back(water_heater); } #endif +#ifdef USE_INFRARED + void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); } +#endif + #ifdef USE_EVENT void register_event(event::Event *event) { this->events_.push_back(event); } #endif @@ -457,6 +464,11 @@ class Application { GET_ENTITY_METHOD(water_heater::WaterHeater, water_heater, water_heaters) #endif +#ifdef USE_INFRARED + auto &get_infrareds() const { return this->infrareds_; } + GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds) +#endif + #ifdef USE_EVENT auto &get_events() const { return this->events_; } GET_ENTITY_METHOD(event::Event, event, events) @@ -656,6 +668,9 @@ class Application { #ifdef USE_WATER_HEATER StaticVector water_heaters_{}; #endif +#ifdef USE_INFRARED + StaticVector infrareds_{}; +#endif #ifdef USE_UPDATE StaticVector updates_{}; #endif diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 4015d8ec60..ff76b2b81b 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -169,6 +169,12 @@ void ComponentIterator::advance() { break; #endif +#ifdef USE_INFRARED + case IteratorState::INFRARED: + this->process_platform_item_(App.get_infrareds(), &ComponentIterator::on_infrared); + break; +#endif + #ifdef USE_EVENT case IteratorState::EVENT: this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 37d1960601..e13d81a8e4 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -16,6 +16,12 @@ class UserServiceDescriptor; } // namespace api #endif +#ifdef USE_INFRARED +namespace infrared { +class Infrared; +} // namespace infrared +#endif + class ComponentIterator { public: void begin(bool include_internal = false); @@ -87,6 +93,9 @@ class ComponentIterator { #ifdef USE_WATER_HEATER virtual bool on_water_heater(water_heater::WaterHeater *water_heater) = 0; #endif +#ifdef USE_INFRARED + virtual bool on_infrared(infrared::Infrared *infrared) = 0; +#endif #ifdef USE_EVENT virtual bool on_event(event::Event *event) = 0; #endif @@ -167,6 +176,9 @@ class ComponentIterator { #ifdef USE_WATER_HEATER WATER_HEATER, #endif +#ifdef USE_INFRARED + INFRARED, +#endif #ifdef USE_EVENT EVENT, #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ed5f152e9f..633b0c6c5e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -49,6 +49,8 @@ #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_IMAGE #define USE_IMPROV_SERIAL_NEXT_URL +#define USE_INFRARED +#define USE_IR_RF #define USE_JSON #define USE_LIGHT #define USE_LOCK @@ -321,9 +323,9 @@ // Default counts for static analysis #define CONTROLLER_REGISTRY_MAX 2 +#define ESPHOME_AREA_COUNT 10 #define ESPHOME_COMPONENT_COUNT 50 #define ESPHOME_DEVICE_COUNT 10 -#define ESPHOME_AREA_COUNT 10 #define ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT 1 #define ESPHOME_ENTITY_BINARY_SENSOR_COUNT 1 #define ESPHOME_ENTITY_BUTTON_COUNT 1 @@ -333,6 +335,7 @@ #define ESPHOME_ENTITY_DATETIME_COUNT 1 #define ESPHOME_ENTITY_EVENT_COUNT 1 #define ESPHOME_ENTITY_FAN_COUNT 1 +#define ESPHOME_ENTITY_INFRARED_COUNT 1 #define ESPHOME_ENTITY_LIGHT_COUNT 1 #define ESPHOME_ENTITY_LOCK_COUNT 1 #define ESPHOME_ENTITY_MEDIA_PLAYER_COUNT 1 diff --git a/tests/components/web_server/common.yaml b/tests/components/web_server/common.yaml index 82307c189c..35a605484c 100644 --- a/tests/components/web_server/common.yaml +++ b/tests/components/web_server/common.yaml @@ -37,3 +37,4 @@ datetime: event: update: water_heater: +infrared: