From b25a2f8d8e0d31284a3489431b310d02be1ef6ab Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 17 Jan 2026 16:01:13 -0600 Subject: [PATCH] [infrared][web_server] Implement initial `web_server` support (#13202) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../components/web_server/list_entities.cpp | 2 +- esphome/components/web_server/web_server.cpp | 116 ++++++++++++++++++ esphome/components/web_server/web_server.h | 10 ++ 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 0af9521326..8458298062 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -143,7 +143,7 @@ bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) { #ifdef USE_INFRARED bool ListEntitiesIterator::on_infrared(infrared::Infrared *obj) { - // Infrared web_server support not yet implemented - this stub acknowledges the entity + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::infrared_all_json_generator); return true; } #endif diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0e71d82233..593e0c32e3 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -33,6 +33,10 @@ #include "esphome/components/water_heater/water_heater.h" #endif +#ifdef USE_INFRARED +#include "esphome/components/infrared/infrared.h" +#endif + #ifdef USE_WEBSERVER_LOCAL #if USE_WEBSERVER_VERSION == 2 #include "server_index_v2.h" @@ -1952,6 +1956,110 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe } #endif +#ifdef USE_INFRARED +void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (infrared::Infrared *obj : App.get_infrareds()) { + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) + continue; + + if (request->method() == HTTP_GET && entity_match.action_is_empty) { + auto detail = get_request_detail(request); + std::string data = this->infrared_json_(obj, detail); + request->send(200, ESPHOME_F("application/json"), data.c_str()); + return; + } + if (!match.method_equals(ESPHOME_F("transmit"))) { + request->send(404); + return; + } + + // Only allow transmit if the device supports it + if (!obj->has_transmitter()) { + request->send(400, ESPHOME_F("text/plain"), "Device does not support transmission"); + return; + } + + // Parse parameters + auto call = obj->make_call(); + + // Parse carrier frequency (optional) + if (request->hasParam(ESPHOME_F("carrier_frequency"))) { + auto value = parse_number(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str()); + if (value.has_value()) { + call.set_carrier_frequency(*value); + } + } + + // Parse repeat count (optional, defaults to 1) + if (request->hasParam(ESPHOME_F("repeat_count"))) { + auto value = parse_number(request->getParam(ESPHOME_F("repeat_count"))->value().c_str()); + if (value.has_value()) { + call.set_repeat_count(*value); + } + } + + // Parse base64url-encoded raw timings (required) + // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) + if (!request->hasParam(ESPHOME_F("data"))) { + request->send(400, ESPHOME_F("text/plain"), "Missing 'data' parameter"); + return; + } + + // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string + std::string encoded = + request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr) + + // Validate base64url is not empty + if (encoded.empty()) { + request->send(400, ESPHOME_F("text/plain"), "Empty 'data' parameter"); + return; + } + +#ifdef USE_ESP8266 + // ESP8266 is single-threaded, call directly + call.set_raw_timings_base64url(encoded); + call.perform(); +#else + // Defer to main loop for thread safety. Move encoded string into lambda to ensure + // it outlives the call - set_raw_timings_base64url stores a pointer, so the string + // must remain valid until perform() completes. + this->defer([call, encoded = std::move(encoded)]() mutable { + call.set_raw_timings_base64url(encoded); + call.perform(); + }); +#endif + + request->send(200); + return; + } + request->send(404); +} + +std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + return web_server->infrared_json_(static_cast(source), DETAIL_ALL); +} + +std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) { + json::JsonBuilder builder; + JsonObject root = builder.root(); + + 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(); + + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); +} +#endif + #ifdef USE_EVENT void WebServer::on_event(event::Event *obj) { if (!this->include_internal_ && obj->is_internal()) @@ -2187,6 +2295,9 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { #endif #ifdef USE_WATER_HEATER "water_heater", +#endif +#ifdef USE_INFRARED + "infrared", #endif }; @@ -2352,6 +2463,11 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { else if (match.domain_equals(ESPHOME_F("water_heater"))) { this->handle_water_heater_request(request, match); } +#endif +#ifdef USE_INFRARED + else if (match.domain_equals(ESPHOME_F("infrared"))) { + this->handle_infrared_request(request, match); + } #endif else { // No matching handler found - send 404 diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index c434d664cf..92a5c7edee 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -460,6 +460,13 @@ class WebServer : public Controller, static std::string water_heater_all_json_generator(WebServer *web_server, void *source); #endif +#ifdef USE_INFRARED + /// Handle an infrared request under '/infrared//transmit'. + void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match); + + static std::string infrared_all_json_generator(WebServer *web_server, void *source); +#endif + #ifdef USE_EVENT void on_event(event::Event *obj) override; @@ -662,6 +669,9 @@ class WebServer : public Controller, #ifdef USE_WATER_HEATER std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config); #endif +#ifdef USE_INFRARED + std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config); +#endif #ifdef USE_UPDATE std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config); #endif