diff --git a/CODEOWNERS b/CODEOWNERS index 48318ee0646..8a37aeb29f4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -255,6 +255,7 @@ esphome/components/inkplate/* @jesserockz @JosipKuci esphome/components/integration/* @OttoWinter esphome/components/internal_temperature/* @Mat931 esphome/components/interval/* @esphome/core +esphome/components/ir_rf_proxy/* @kbx81 esphome/components/jsn_sr04t/* @Mafus1 esphome/components/json/* @esphome/core esphome/components/kamstrup_kmp/* @cfeenstra1024 diff --git a/esphome/components/ir_rf_proxy/__init__.py b/esphome/components/ir_rf_proxy/__init__.py new file mode 100644 index 00000000000..bc4079ede7e --- /dev/null +++ b/esphome/components/ir_rf_proxy/__init__.py @@ -0,0 +1,11 @@ +"""IR/RF Proxy component - provides remote_base backend for infrared platform.""" + +import esphome.codegen as cg + +CODEOWNERS = ["@kbx81"] + +# Namespace and constants exported for infrared.py platform +ir_rf_proxy_ns = cg.esphome_ns.namespace("ir_rf_proxy") + +CONF_REMOTE_RECEIVER_ID = "remote_receiver_id" +CONF_REMOTE_TRANSMITTER_ID = "remote_transmitter_id" diff --git a/esphome/components/ir_rf_proxy/infrared.py b/esphome/components/ir_rf_proxy/infrared.py new file mode 100644 index 00000000000..4a4d9fa8604 --- /dev/null +++ b/esphome/components/ir_rf_proxy/infrared.py @@ -0,0 +1,77 @@ +"""Infrared platform implementation using remote_base (remote_transmitter/receiver).""" + +from typing import Any + +import esphome.codegen as cg +from esphome.components import infrared, remote_receiver, remote_transmitter +import esphome.config_validation as cv +from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY +import esphome.final_validate as fv + +from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["infrared"] + +IrRfProxy = ir_rf_proxy_ns.class_("IrRfProxy", infrared.Infrared) + +CONFIG_SCHEMA = cv.All( + infrared.infrared_schema(IrRfProxy).extend( + { + cv.Optional(CONF_FREQUENCY, default=0): cv.frequency, + cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id( + remote_receiver.RemoteReceiverComponent + ), + cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id( + remote_transmitter.RemoteTransmitterComponent + ), + } + ), + cv.has_exactly_one_key(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID), +) + + +def _final_validate(config: dict[str, Any]) -> None: + """Validate that transmitters have a proper carrier duty cycle.""" + # Only validate if this is an infrared (not RF) configuration with a transmitter + if config.get(CONF_FREQUENCY, 0) != 0 or CONF_REMOTE_TRANSMITTER_ID not in config: + return + + # Get the transmitter configuration + transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID] + full_config = fv.full_config.get() + transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1] + transmitter_config = full_config.get_config_for_path(transmitter_path) + + # Check if carrier_duty_percent set to 0 or 100 + # Note: remote_transmitter schema requires this field and validates 1-100%, + # but we double-check here for infrared to provide a helpful error message + duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT) + if duty_percent in {0, 100}: + raise cv.Invalid( + f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' configured with " + "an intermediate value (typically 30-50%) for infrared transmission. If this is an RF " + f"transmitter, configure this infrared with a '{CONF_FREQUENCY}' value greater than 0" + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config: dict[str, Any]) -> None: + """Code generation for remote_base infrared platform.""" + # Create and register the infrared entity + var = await infrared.new_infrared(config) + + # Set frequency / 1000; zero indicates infrared hardware + cg.add(var.set_frequency(config[CONF_FREQUENCY] / 1000)) + + # 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)) + + # Link receiver if specified + if CONF_REMOTE_RECEIVER_ID in config: + receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) + cg.add(var.set_receiver(receiver)) diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp new file mode 100644 index 00000000000..5239a4667c0 --- /dev/null +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp @@ -0,0 +1,23 @@ +#include "ir_rf_proxy.h" +#include "esphome/core/log.h" + +namespace esphome::ir_rf_proxy { + +static const char *const TAG = "ir_rf_proxy"; + +void IrRfProxy::dump_config() { + ESP_LOGCONFIG(TAG, + "IR/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())); + + if (this->is_rf()) { + ESP_LOGCONFIG(TAG, " Hardware Type: RF (%.3f MHz)", this->frequency_khz_ / 1e3f); + } else { + ESP_LOGCONFIG(TAG, " Hardware Type: Infrared"); + } +} + +} // namespace esphome::ir_rf_proxy diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h new file mode 100644 index 00000000000..d7c8919def1 --- /dev/null +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -0,0 +1,32 @@ +#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/components/infrared/infrared.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#include "esphome/components/remote_receiver/remote_receiver.h" + +namespace esphome::ir_rf_proxy { + +/// IrRfProxy - Infrared platform implementation using remote_transmitter/receiver as backend +class IrRfProxy : public infrared::Infrared { + public: + IrRfProxy() = default; + + void dump_config() override; + + /// Set RF frequency in kHz (0 = infrared, non-zero = RF) + void set_frequency(uint32_t frequency_khz) { this->frequency_khz_ = frequency_khz; } + /// Get RF frequency in kHz + uint32_t get_frequency() const { return this->frequency_khz_; } + /// Check if this is RF mode (non-zero frequency) + bool is_rf() const { return this->frequency_khz_ > 0; } + + protected: + // RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF + uint32_t frequency_khz_{0}; +}; + +} // namespace esphome::ir_rf_proxy diff --git a/tests/components/infrared/common.yaml b/tests/components/infrared/common.yaml new file mode 100644 index 00000000000..cd2b10d31b6 --- /dev/null +++ b/tests/components/infrared/common.yaml @@ -0,0 +1,42 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +remote_transmitter: + id: ir_transmitter + pin: ${tx_pin} + carrier_duty_percent: 50% + +remote_receiver: + id: ir_receiver + pin: ${rx_pin} + +# Test various hardware types with transmitter/receiver using infrared platform +infrared: + # Infrared transmitter + - platform: ir_rf_proxy + id: ir_tx + name: "IR Transmitter" + remote_transmitter_id: ir_transmitter + + # Infrared receiver + - platform: ir_rf_proxy + id: ir_rx + name: "IR Receiver" + remote_receiver_id: ir_receiver + + # RF 433MHz transmitter + - platform: ir_rf_proxy + id: rf_433_tx + name: "RF 433 Transmitter" + frequency: 433 MHz + remote_transmitter_id: ir_transmitter + + # RF 900MHz receiver + - platform: ir_rf_proxy + id: rf_900_rx + name: "RF 900 Receiver" + frequency: 900 MHz + remote_receiver_id: ir_receiver diff --git a/tests/components/infrared/test.esp32-idf.yaml b/tests/components/infrared/test.esp32-idf.yaml new file mode 100644 index 00000000000..b516342f3bc --- /dev/null +++ b/tests/components/infrared/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/infrared/test.esp8266-ard.yaml b/tests/components/infrared/test.esp8266-ard.yaml new file mode 100644 index 00000000000..b516342f3bc --- /dev/null +++ b/tests/components/infrared/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/infrared/test.rp2040-ard.yaml b/tests/components/infrared/test.rp2040-ard.yaml new file mode 100644 index 00000000000..b516342f3bc --- /dev/null +++ b/tests/components/infrared/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ir_rf_proxy/common.yaml b/tests/components/ir_rf_proxy/common.yaml new file mode 100644 index 00000000000..cd2b10d31b6 --- /dev/null +++ b/tests/components/ir_rf_proxy/common.yaml @@ -0,0 +1,42 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +remote_transmitter: + id: ir_transmitter + pin: ${tx_pin} + carrier_duty_percent: 50% + +remote_receiver: + id: ir_receiver + pin: ${rx_pin} + +# Test various hardware types with transmitter/receiver using infrared platform +infrared: + # Infrared transmitter + - platform: ir_rf_proxy + id: ir_tx + name: "IR Transmitter" + remote_transmitter_id: ir_transmitter + + # Infrared receiver + - platform: ir_rf_proxy + id: ir_rx + name: "IR Receiver" + remote_receiver_id: ir_receiver + + # RF 433MHz transmitter + - platform: ir_rf_proxy + id: rf_433_tx + name: "RF 433 Transmitter" + frequency: 433 MHz + remote_transmitter_id: ir_transmitter + + # RF 900MHz receiver + - platform: ir_rf_proxy + id: rf_900_rx + name: "RF 900 Receiver" + frequency: 900 MHz + remote_receiver_id: ir_receiver diff --git a/tests/components/ir_rf_proxy/test.esp32-idf.yaml b/tests/components/ir_rf_proxy/test.esp32-idf.yaml new file mode 100644 index 00000000000..b516342f3bc --- /dev/null +++ b/tests/components/ir_rf_proxy/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ir_rf_proxy/test.esp8266-ard.yaml b/tests/components/ir_rf_proxy/test.esp8266-ard.yaml new file mode 100644 index 00000000000..b516342f3bc --- /dev/null +++ b/tests/components/ir_rf_proxy/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ir_rf_proxy/test.rp2040-ard.yaml b/tests/components/ir_rf_proxy/test.rp2040-ard.yaml new file mode 100644 index 00000000000..b516342f3bc --- /dev/null +++ b/tests/components/ir_rf_proxy/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml