diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index eac26997cf..2a0ddf91db 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size } uint32_t field_length = res->as_uint32(); ptr += consumed; - if (ptr + field_length > end) { + if (field_length > static_cast(end - ptr)) { return count; // Out of bounds } ptr += field_length; break; } case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes - if (ptr + 4 > end) { + if (end - ptr < 4) { return count; } ptr += 4; @@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { } uint32_t field_length = res->as_uint32(); ptr += consumed; - if (ptr + field_length > end) { + if (field_length > static_cast(end - ptr)) { ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); return; } @@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { break; } case WIRE_TYPE_FIXED32: { // 32-bit - if (ptr + 4 > end) { + if (end - ptr < 4) { ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); return; } diff --git a/esphome/components/hmac_sha256/hmac_sha256.cpp b/esphome/components/hmac_sha256/hmac_sha256.cpp index cf5daf63af..2146e961bc 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.cpp +++ b/esphome/components/hmac_sha256/hmac_sha256.cpp @@ -1,4 +1,3 @@ -#include #include #include "hmac_sha256.h" #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) @@ -26,9 +25,7 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_ void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); } void HmacSHA256::get_hex(char *output) { - for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) { - sprintf(output + (i * 2), "%02x", this->digest_[i]); - } + format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE); } bool HmacSHA256::equals_bytes(const uint8_t *expected) { diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 1b5fd9f00e..a8c2cdfc63 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -242,9 +242,7 @@ template class HttpRequestSendAction : public Action { return; } - size_t content_length = container->content_length; - size_t max_length = std::min(content_length, this->max_response_buffer_size_); - + size_t max_length = this->max_response_buffer_size_; #ifdef USE_HTTP_REQUEST_RESPONSE if (this->capture_response_.value(x...)) { std::string response_body; diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 725a9c1c1e..1de947ba5b 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -213,18 +213,12 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - int bufsize = std::min(max_len, this->content_length - this->bytes_read_); - - if (bufsize == 0) { - this->duration_ms += (millis() - start); - return 0; + this->feed_wdt(); + int read_len = esp_http_client_read(this->client_, (char *) buf, max_len); + this->feed_wdt(); + if (read_len > 0) { + this->bytes_read_ += read_len; } - - this->feed_wdt(); - int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize); - this->feed_wdt(); - this->bytes_read_ += read_len; - this->duration_ms += (millis() - start); return read_len; diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index 20c731e730..0eeb4bba33 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -1,3 +1,4 @@ +import logging from typing import Any from esphome import automation, pins @@ -18,13 +19,16 @@ from esphome.const import ( CONF_ROTATION, CONF_UPDATE_INTERVAL, ) -from esphome.core import ID +from esphome.core import ID, EnumValue from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv +from esphome.helpers import add_class_to_obj from esphome.types import ConfigType from . import boards, hub75_ns +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ["esp32"] CODEOWNERS = ["@stuartparmenter"] @@ -120,13 +124,51 @@ PANEL_LAYOUTS = { } Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True) -SCAN_PATTERNS = { +SCAN_WIRINGS = { "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, - "FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH, - "FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH, - "FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH, + "SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH, + "SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH, + "SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH, + "SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH, } +# Deprecated scan wiring names - mapped to new names +DEPRECATED_SCAN_WIRINGS = { + "FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH", + "FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH", + "FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH", +} + + +def _validate_scan_wiring(value): + """Validate scan_wiring with deprecation warnings for old names.""" + value = cv.string(value).upper().replace(" ", "_") + + # Check if using deprecated name + # Remove deprecated names in 2026.7.0 + if value in DEPRECATED_SCAN_WIRINGS: + new_name = DEPRECATED_SCAN_WIRINGS[value] + _LOGGER.warning( + "Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. " + "Please use '%s' instead.", + value, + new_name, + ) + value = new_name + + # Validate against allowed values + if value not in SCAN_WIRINGS: + raise cv.Invalid( + f"Unknown scan wiring '{value}'. " + f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}" + ) + + # Return as EnumValue like cv.enum does + result = add_class_to_obj(value, EnumValue) + result.enum_value = SCAN_WIRINGS[value] + return result + + Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True) CLOCK_SPEEDS = { "8MHZ": Hub75ClockSpeed.HZ_8M, @@ -382,9 +424,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_LAYOUT_COLS): cv.positive_int, cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"), # Panel hardware configuration - cv.Optional(CONF_SCAN_WIRING): cv.enum( - SCAN_PATTERNS, upper=True, space="_" - ), + cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring, cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True), # Display configuration cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean, @@ -547,7 +587,7 @@ def _build_config_struct( async def to_code(config: ConfigType) -> None: add_idf_component( name="esphome/esp-hub75", - ref="0.2.2", + ref="0.3.0", ) # Set compile-time configuration via build flags (so external library sees them) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 294d69e523..4431869951 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -19,12 +19,12 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) { InfraredCall &InfraredCall::set_raw_timings(const std::vector &timings) { this->raw_timings_ = &timings; this->packed_data_ = nullptr; - this->base85_ptr_ = nullptr; + this->base64url_ptr_ = nullptr; return *this; } -InfraredCall &InfraredCall::set_raw_timings_base85(const std::string &base85) { - this->base85_ptr_ = &base85; +InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) { + this->base64url_ptr_ = &base64url; this->raw_timings_ = nullptr; this->packed_data_ = nullptr; return *this; @@ -35,7 +35,7 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t this->packed_length_ = length; this->packed_count_ = count; this->raw_timings_ = nullptr; - this->base85_ptr_ = nullptr; + this->base64url_ptr_ = nullptr; return *this; } @@ -101,13 +101,22 @@ void Infrared::control(const InfraredCall &call) { call.get_packed_count()); ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(), call.get_repeat_count()); - } else if (call.is_base85()) { - // Decode base85 directly into transmit buffer (zero heap allocations) - if (!transmit_data->set_data_from_base85(call.get_base85_data())) { - ESP_LOGE(TAG, "Invalid base85 data"); + } else if (call.is_base64url()) { + // Decode base64url (URL-safe) into transmit buffer + if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) { + ESP_LOGE(TAG, "Invalid base64url data"); return; } - ESP_LOGD(TAG, "Transmitting base85 raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(), + // Sanity check: validate timing values are within reasonable bounds + constexpr int32_t max_timing_us = 500000; // 500ms absolute max + for (int32_t timing : transmit_data->get_data()) { + int32_t abs_timing = timing < 0 ? -timing : timing; + if (abs_timing > max_timing_us) { + ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us); + return; + } + } + ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(), call.get_repeat_count()); } else { // From vector (lambdas/automations) diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index ba426c9daa..59535f499a 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -40,11 +40,11 @@ class InfraredCall { /// @note Usage: Primarily for lambdas/automations where the vector is in scope. InfraredCall &set_raw_timings(const std::vector &timings); - /// Set the raw timings from base85-encoded int32 data + /// Set the raw timings from base64url-encoded little-endian int32 data /// @note Lifetime: Stores a pointer to the string. The string must outlive perform(). - /// @note Usage: For web_server where the encoded string is on the stack. + /// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_'). /// @note Decoding happens at perform() time, directly into the transmit buffer. - InfraredCall &set_raw_timings_base85(const std::string &base85); + InfraredCall &set_raw_timings_base64url(const std::string &base64url); /// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded) /// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform(). @@ -59,18 +59,18 @@ class InfraredCall { /// 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 or base85) + /// Get the raw timings (only valid if set via set_raw_timings) const std::vector &get_raw_timings() const { return *this->raw_timings_; } - /// Check if raw timings have been set (vector, packed, or base85) + /// Check if raw timings have been set (any format) bool has_raw_timings() const { - return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base85_ptr_ != nullptr; + return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr; } /// Check if using packed data format bool is_packed() const { return this->packed_data_ != nullptr; } - /// Check if using base85 data format - bool is_base85() const { return this->base85_ptr_ != nullptr; } - /// Get the base85 data string - const std::string &get_base85_data() const { return *this->base85_ptr_; } + /// Check if using base64url data format + bool is_base64url() const { return this->base64url_ptr_ != nullptr; } + /// Get the base64url data string + const std::string &get_base64url_data() const { return *this->base64url_ptr_; } /// Get packed data (only valid if set via set_raw_timings_packed) const uint8_t *get_packed_data() const { return this->packed_data_; } uint16_t get_packed_length() const { return this->packed_length_; } @@ -84,8 +84,8 @@ class InfraredCall { optional carrier_frequency_; // Pointer to vector-based timings (caller-owned, must outlive perform()) const std::vector *raw_timings_{nullptr}; - // Pointer to base85-encoded string (caller-owned, must outlive perform()) - const std::string *base85_ptr_{nullptr}; + // Pointer to base64url-encoded string (caller-owned, must outlive perform()) + const std::string *base64url_ptr_{nullptr}; // Pointer to packed protobuf buffer (caller-owned, must outlive perform()) const uint8_t *packed_data_{nullptr}; uint16_t packed_length_{0}; diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 53c9c38c7d..b4a549f0be 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -2,8 +2,6 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include - namespace esphome { namespace remote_base { @@ -160,8 +158,8 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t } } -bool RemoteTransmitData::set_data_from_base85(const std::string &base85) { - return base85_decode_int32_vector(base85, this->data_); +bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) { + return base64_decode_int32_vector(base64url, this->data_); } /* RemoteTransmitterBase */ diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 2d7642cc31..0cac28506f 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -36,11 +36,11 @@ class RemoteTransmitData { /// @param len Length of the buffer in bytes /// @param count Number of values (for reserve optimization) void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count); - /// Set data from base85-encoded int32 values - /// Decodes directly into internal buffer (zero heap allocations) - /// @param base85 Base85-encoded string (5 chars per int32 value) + /// Set data from base64url-encoded little-endian int32 values + /// Base64url is URL-safe: uses '-' instead of '+', '_' instead of '/' + /// @param base64url Base64url-encoded string of little-endian int32 values /// @return true if successful, false if decode failed or invalid size - bool set_data_from_base85(const std::string &base85); + bool set_data_from_base64url(const std::string &base64url); void reset() { this->data_.clear(); this->carrier_frequency_ = 0; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index cf984ea247..0e71d82233 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -658,6 +658,24 @@ std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std #endif #ifdef USE_SWITCH +enum SwitchAction : uint8_t { SWITCH_ACTION_NONE, SWITCH_ACTION_TOGGLE, SWITCH_ACTION_TURN_ON, SWITCH_ACTION_TURN_OFF }; + +static void execute_switch_action(switch_::Switch *obj, SwitchAction action) { + switch (action) { + case SWITCH_ACTION_TOGGLE: + obj->toggle(); + break; + case SWITCH_ACTION_TURN_ON: + obj->turn_on(); + break; + case SWITCH_ACTION_TURN_OFF: + obj->turn_off(); + break; + default: + break; + } +} + void WebServer::on_switch_update(switch_::Switch *obj) { if (!this->include_internal_ && obj->is_internal()) return; @@ -676,34 +694,22 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM return; } - // Handle action methods with single defer and response - enum SwitchAction { NONE, TOGGLE, TURN_ON, TURN_OFF }; - SwitchAction action = NONE; + SwitchAction action = SWITCH_ACTION_NONE; if (match.method_equals(ESPHOME_F("toggle"))) { - action = TOGGLE; + action = SWITCH_ACTION_TOGGLE; } else if (match.method_equals(ESPHOME_F("turn_on"))) { - action = TURN_ON; + action = SWITCH_ACTION_TURN_ON; } else if (match.method_equals(ESPHOME_F("turn_off"))) { - action = TURN_OFF; + action = SWITCH_ACTION_TURN_OFF; } - if (action != NONE) { - this->defer([obj, action]() { - switch (action) { - case TOGGLE: - obj->toggle(); - break; - case TURN_ON: - obj->turn_on(); - break; - case TURN_OFF: - obj->turn_off(); - break; - default: - break; - } - }); + if (action != SWITCH_ACTION_NONE) { +#ifdef USE_ESP8266 + execute_switch_action(obj, action); +#else + this->defer([obj, action]() { execute_switch_action(obj, action); }); +#endif request->send(200); } else { request->send(404); @@ -743,7 +749,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM std::string data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("press"))) { - this->defer([obj]() { obj->press(); }); + DEFER_ACTION(obj, obj->press()); request->send(200); return; } else { @@ -828,7 +834,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc std::string data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { - this->defer([obj]() { obj->toggle().perform(); }); + DEFER_ACTION(obj, obj->toggle().perform()); request->send(200); } else { bool is_on = match.method_equals(ESPHOME_F("turn_on")); @@ -859,7 +865,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc return; } } - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); } return; @@ -909,7 +915,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa std::string data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { - this->defer([obj]() { obj->toggle().perform(); }); + DEFER_ACTION(obj, obj->toggle().perform()); request->send(200); } else { bool is_on = match.method_equals(ESPHOME_F("turn_on")); @@ -938,7 +944,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect); } - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); } return; @@ -1027,7 +1033,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1086,7 +1092,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1159,7 +1165,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1223,7 +1229,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1286,7 +1292,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1346,7 +1352,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1404,7 +1410,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1473,7 +1479,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low); parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1589,6 +1595,24 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con #endif #ifdef USE_LOCK +enum LockAction : uint8_t { LOCK_ACTION_NONE, LOCK_ACTION_LOCK, LOCK_ACTION_UNLOCK, LOCK_ACTION_OPEN }; + +static void execute_lock_action(lock::Lock *obj, LockAction action) { + switch (action) { + case LOCK_ACTION_LOCK: + obj->lock(); + break; + case LOCK_ACTION_UNLOCK: + obj->unlock(); + break; + case LOCK_ACTION_OPEN: + obj->open(); + break; + default: + break; + } +} + void WebServer::on_lock_update(lock::Lock *obj) { if (!this->include_internal_ && obj->is_internal()) return; @@ -1607,34 +1631,22 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat return; } - // Handle action methods with single defer and response - enum LockAction { NONE, LOCK, UNLOCK, OPEN }; - LockAction action = NONE; + LockAction action = LOCK_ACTION_NONE; if (match.method_equals(ESPHOME_F("lock"))) { - action = LOCK; + action = LOCK_ACTION_LOCK; } else if (match.method_equals(ESPHOME_F("unlock"))) { - action = UNLOCK; + action = LOCK_ACTION_UNLOCK; } else if (match.method_equals(ESPHOME_F("open"))) { - action = OPEN; + action = LOCK_ACTION_OPEN; } - if (action != NONE) { - this->defer([obj, action]() { - switch (action) { - case LOCK: - obj->lock(); - break; - case UNLOCK: - obj->unlock(); - break; - case OPEN: - obj->open(); - break; - default: - break; - } - }); + if (action != LOCK_ACTION_NONE) { +#ifdef USE_ESP8266 + execute_lock_action(obj, action); +#else + this->defer([obj, action]() { execute_lock_action(obj, action); }); +#endif request->send(200); } else { request->send(404); @@ -1717,7 +1729,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1796,7 +1808,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques return; } - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1872,7 +1884,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons // Parse on/off parameter parse_bool_param_(request, ESPHOME_F("is_on"), base_call, &water_heater::WaterHeaterCall::set_on); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -2032,7 +2044,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM return; } - this->defer([obj]() mutable { obj->perform(); }); + DEFER_ACTION(obj, obj->perform()); request->send(200); return; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index b1a495ebef..c434d664cf 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -42,6 +42,14 @@ using ParamNameType = const __FlashStringHelper *; using ParamNameType = const char *; #endif +// ESP8266 is single-threaded, so actions can execute directly in request context. +// Multi-core platforms need to defer to main loop thread for thread safety. +#ifdef USE_ESP8266 +#define DEFER_ACTION(capture, action) action +#else +#define DEFER_ACTION(capture, action) this->defer([capture]() mutable { action; }) +#endif + /// Result of matching a URL against an entity struct EntityMatchResult { bool matched; ///< True if entity matched the URL diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 96b2d46d78..5de1c70562 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -624,53 +624,44 @@ std::vector base64_decode(const std::string &encoded_string) { return ret; } -/// Encode int32 to 5 base85 characters + null terminator -/// Standard ASCII85 alphabet: '!' (33) = 0 through 'u' (117) = 84 -inline void base85_encode_int32(int32_t value, std::span output) { - uint32_t v = static_cast(value); - // Encode least significant digit first, then reverse - for (int i = 4; i >= 0; i--) { - output[i] = static_cast('!' + (v % 85)); - v /= 85; - } - output[5] = '\0'; -} - -/// Decode 5 base85 characters to int32 -inline bool base85_decode_int32(const char *input, int32_t &out) { - uint8_t c0 = static_cast(input[0] - '!'); - uint8_t c1 = static_cast(input[1] - '!'); - uint8_t c2 = static_cast(input[2] - '!'); - uint8_t c3 = static_cast(input[3] - '!'); - uint8_t c4 = static_cast(input[4] - '!'); - - // Each digit must be 0-84. Since uint8_t wraps, chars below '!' become > 84 - if (c0 > 84 || c1 > 84 || c2 > 84 || c3 > 84 || c4 > 84) - return false; - - // 85^4 = 52200625, 85^3 = 614125, 85^2 = 7225, 85^1 = 85 - out = static_cast(c0 * 52200625u + c1 * 614125u + c2 * 7225u + c3 * 85u + c4); - return true; -} - -/// Decode base85 string directly into vector (no intermediate buffer) -bool base85_decode_int32_vector(const std::string &base85, std::vector &out) { - size_t len = base85.size(); - if (len % 5 != 0) - return false; +/// Decode base64/base64url string directly into vector of little-endian int32 values +/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted) +/// @param out Output vector (cleared and filled with decoded int32 values) +/// @return true if successful, false if decode failed or invalid size +bool base64_decode_int32_vector(const std::string &base64, std::vector &out) { + // Decode in chunks to minimize stack usage + constexpr size_t chunk_bytes = 48; // 12 int32 values + constexpr size_t chunk_chars = 64; // 48 * 4/3 = 64 chars + uint8_t chunk[chunk_bytes]; out.clear(); - const char *ptr = base85.data(); - const char *end = ptr + len; - while (ptr < end) { - int32_t value; - if (!base85_decode_int32(ptr, value)) + const uint8_t *input = reinterpret_cast(base64.data()); + size_t remaining = base64.size(); + size_t pos = 0; + + while (remaining > 0) { + size_t chars_to_decode = std::min(remaining, chunk_chars); + size_t decoded_len = base64_decode(input + pos, chars_to_decode, chunk, chunk_bytes); + + if (decoded_len == 0) return false; - out.push_back(value); - ptr += 5; + + // Parse little-endian int32 values + for (size_t i = 0; i + 3 < decoded_len; i += 4) { + int32_t timing = static_cast(encode_uint32(chunk[i + 3], chunk[i + 2], chunk[i + 1], chunk[i])); + out.push_back(timing); + } + + // Check for incomplete int32 in last chunk + if (remaining <= chunk_chars && (decoded_len % 4) != 0) + return false; + + pos += chars_to_decode; + remaining -= chars_to_decode; } - return true; + + return !out.empty(); } // Colors diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9dc289c743..0acc6bdc60 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1137,13 +1137,11 @@ std::vector base64_decode(const std::string &encoded_string); size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len); size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len); -/// Size of buffer needed for base85 encoded int32 (5 chars + null terminator) -static constexpr size_t BASE85_INT32_ENCODED_SIZE = 6; - -void base85_encode_int32(int32_t value, std::span output); - -bool base85_decode_int32(const char *input, int32_t &out); -bool base85_decode_int32_vector(const std::string &base85, std::vector &out); +/// Decode base64/base64url string directly into vector of little-endian int32 values +/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted) +/// @param out Output vector (cleared and filled with decoded int32 values) +/// @return true if successful, false if decode failed or invalid size +bool base64_decode_int32_vector(const std::string &base64, std::vector &out); ///@} diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 045b3f9168..5903e68e8e 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -28,8 +28,8 @@ dependencies: rules: - if: "target in [esp32s2, esp32s3, esp32p4]" esphome/esp-hub75: - version: 0.2.2 + version: 0.3.0 rules: - - if: "target in [esp32, esp32s2, esp32s3, esp32p4]" + - if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]" esp32async/asynctcp: version: 3.4.91