From 918bc4b74f2f9af49d1c3efa8846d392e1ee324e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 22 Dec 2025 15:41:14 -0500 Subject: [PATCH 1/5] [esp32] Remove remaining using_esp_idf checks (#12623) Co-authored-by: Claude --- esphome/components/captive_portal/__init__.py | 2 +- esphome/components/i2c/__init__.py | 2 +- esphome/components/i2s_audio/__init__.py | 5 +++-- esphome/components/improv_serial/__init__.py | 2 +- esphome/components/mdns/__init__.py | 6 ++---- esphome/components/network/__init__.py | 10 +++++----- esphome/components/wifi/__init__.py | 4 ++-- esphome/core/__init__.py | 5 +++++ 8 files changed, 20 insertions(+), 16 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 25d0a22083..763e2e4ec5 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) def AUTO_LOAD() -> list[str]: auto_load = ["web_server_base", "ota.web_server"] - if CORE.using_esp_idf: + if CORE.is_esp32: auto_load.append("socket") return auto_load diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index b7436ccc39..56e0c8e4ab 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -146,7 +146,7 @@ def _final_validate(config): full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") - if CORE.using_esp_idf and get_esp32_variant() in ESP32_I2C_CAPABILITIES: + if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES: variant = get_esp32_variant() max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"] if len(full_config) > max_num: diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 61c5ca4ec1..d3128c5f4c 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -232,6 +232,8 @@ def validate_use_legacy(value): if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino): raise cv.Invalid("Arduino supports only the legacy i2s driver") _set_use_legacy_driver(value[CONF_USE_LEGACY]) + elif CORE.using_arduino: + _set_use_legacy_driver(True) return value @@ -261,8 +263,7 @@ def _final_validate(_): def use_legacy(): - legacy_driver = _get_use_legacy_driver() - return not (CORE.using_esp_idf and not legacy_driver) + return _get_use_legacy_driver() FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 7f88b17e11..9a2ac2f40f 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -26,7 +26,7 @@ def validate_logger(config): logger_conf = fv.full_config.get()[CONF_LOGGER] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") - if CORE.using_esp_idf and ( + if CORE.is_esp32 and ( logger_conf[CONF_HARDWARE_UART] == USB_CDC and get_esp32_variant() == VARIANT_ESP32S3 ): diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 99b728b249..3088d8ad7e 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -157,14 +157,12 @@ async def to_code(config): return if CORE.using_arduino: - if CORE.is_esp32: - cg.add_library("ESPmDNS", None) - elif CORE.is_esp8266: + if CORE.is_esp8266: cg.add_library("ESP8266mDNS", None) elif CORE.is_rp2040: cg.add_library("LEAmDNS", None) - if CORE.using_esp_idf: + if CORE.is_esp32: add_idf_component(name="espressif/mdns", ref="1.9.1") cg.add_define("USE_MDNS") diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index d7a51fb0c6..5b63bbfce9 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -156,7 +156,7 @@ async def to_code(config): "High performance networking disabled by user configuration (overriding component request)" ) - if CORE.is_esp32 and CORE.using_esp_idf and should_enable: + if CORE.is_esp32 and should_enable: # Check if PSRAM is guaranteed (set by psram component during final validation) psram_guaranteed = psram_is_guaranteed() @@ -210,12 +210,12 @@ async def to_code(config): "USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT] ) if CORE.is_esp32: - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) - add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) - else: + if CORE.using_arduino: add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", True) add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", True) + else: + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) elif enable_ipv6: cg.add_build_flag("-DCONFIG_LWIP_IPV6") cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index fb23837e78..232e8d4f27 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -464,7 +464,7 @@ async def to_code(config): ) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) cg.add_define("USE_WIFI_AP") - elif CORE.is_esp32 and CORE.using_esp_idf: + elif CORE.is_esp32 and not CORE.using_arduino: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) @@ -509,7 +509,7 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) # Apply high performance WiFi settings if high performance networking is enabled - if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking(): + if CORE.is_esp32 and has_high_performance_networking(): # Check if PSRAM is guaranteed (set by psram component during final validation) psram_guaranteed = psram_is_guaranteed() diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index ad9844a3bf..3baec93186 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -798,6 +798,11 @@ class EsphomeCore: @property def using_esp_idf(self): + _LOGGER.warning( + "CORE.using_esp_idf was deprecated in 2026.1, will change behavior in 2026.6. " + "ESP32 Arduino builds on top of ESP-IDF, so ESP-IDF features are available in both frameworks. " + "Use CORE.is_esp32 and/or CORE.using_arduino instead." + ) return self.target_framework == "esp-idf" @property From c8b531ac06d569197da9b6740dd7062f1a7e77d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 11:13:51 -1000 Subject: [PATCH 2/5] [safe_mode] Defer preference sync in clean_rtc to avoid blocking event loop (#12625) --- esphome/components/safe_mode/safe_mode.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index f8e5d7d8e5..c933222273 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -141,7 +141,14 @@ uint32_t SafeModeComponent::read_rtc_() { return val; } -void SafeModeComponent::clean_rtc() { this->write_rtc_(0); } +void SafeModeComponent::clean_rtc() { + // Save without sync - preferences will be written at shutdown or by IntervalSyncer. + // This avoids blocking the loop for 50+ ms on flash write. If the device crashes + // before sync, the boot wasn't really successful anyway and the counter should + // remain incremented. + uint32_t val = 0; + this->rtc_.save(&val); +} void SafeModeComponent::on_safe_shutdown() { if (this->read_rtc_() != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) From bdbe72b7f15e0f0dc861546565798dfa9ed86fa9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 11:14:11 -1000 Subject: [PATCH 3/5] [web_server] Make internal JSON helper methods private (#12624) --- esphome/components/web_server/web_server.cpp | 182 +++++++++---------- esphome/components/web_server/web_server.h | 105 ++++++----- 2 files changed, 154 insertions(+), 133 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 207eafad5c..ece9d65121 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -455,7 +455,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->sensor_json(obj, obj->state, detail); + std::string data = this->sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -463,12 +463,12 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { - return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); + return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); } std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { - return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); + return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { +std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -500,7 +500,7 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->text_sensor_json(obj, obj->state, detail); + std::string data = this->text_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -508,15 +508,15 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const request->send(404); } std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { - return web_server->text_sensor_json((text_sensor::TextSensor *) (source), - ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); + return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), + ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); } std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { - return web_server->text_sensor_json((text_sensor::TextSensor *) (source), - ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); + return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), + ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, - JsonDetail start_config) { +std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -542,7 +542,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->switch_json(obj, obj->state, detail); + std::string data = this->switch_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -584,12 +584,12 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) { - return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); + return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); } std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) { - return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); + return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); } -std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { +std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -610,7 +610,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->button_json(obj, detail); + std::string data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals("press")) { this->defer([obj]() { obj->press(); }); @@ -624,12 +624,12 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) { - return web_server->button_json((button::Button *) (source), DETAIL_STATE); + return web_server->button_json_((button::Button *) (source), DETAIL_STATE); } std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) { - return web_server->button_json((button::Button *) (source), DETAIL_ALL); + return web_server->button_json_((button::Button *) (source), DETAIL_ALL); } -std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) { +std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -655,7 +655,7 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->binary_sensor_json(obj, obj->state, detail); + std::string data = this->binary_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -663,14 +663,14 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con request->send(404); } std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { - return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source), - ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); + return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), + ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); } std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { - return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source), - ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); + return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), + ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { +std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -696,7 +696,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->fan_json(obj, detail); + std::string data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); @@ -738,12 +738,12 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc request->send(404); } std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) { - return web_server->fan_json((fan::Fan *) (source), DETAIL_STATE); + return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE); } std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) { - return web_server->fan_json((fan::Fan *) (source), DETAIL_ALL); + return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL); } -std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { +std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -776,7 +776,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->light_json(obj, detail); + std::string data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); @@ -816,12 +816,12 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) { - return web_server->light_json((light::LightState *) (source), DETAIL_STATE); + return web_server->light_json_((light::LightState *) (source), DETAIL_STATE); } std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) { - return web_server->light_json((light::LightState *) (source), DETAIL_ALL); + return web_server->light_json_((light::LightState *) (source), DETAIL_ALL); } -std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) { +std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -854,7 +854,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->cover_json(obj, detail); + std::string data = this->cover_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -903,12 +903,12 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) { - return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE); + return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE); } std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { - return web_server->cover_json((cover::Cover *) (source), DETAIL_ALL); + return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL); } -std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { +std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -942,7 +942,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->number_json(obj, obj->state, detail); + std::string data = this->number_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -962,12 +962,12 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) { - return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); + return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); } std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) { - return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); + return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); } -std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) { +std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1009,7 +1009,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->date_json(obj, detail); + std::string data = this->date_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1035,12 +1035,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat } std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) { - return web_server->date_json((datetime::DateEntity *) (source), DETAIL_STATE); + return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE); } std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) { - return web_server->date_json((datetime::DateEntity *) (source), DETAIL_ALL); + return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL); } -std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) { +std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1072,7 +1072,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->time_json(obj, detail); + std::string data = this->time_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1097,12 +1097,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) { - return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_STATE); + return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE); } std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) { - return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_ALL); + return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) { +std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1134,7 +1134,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->datetime_json(obj, detail); + std::string data = this->datetime_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1159,12 +1159,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur request->send(404); } std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { - return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_STATE); + return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE); } std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { - return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_ALL); + return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) { +std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1199,7 +1199,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->text_json(obj, obj->state, detail); + std::string data = this->text_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1219,12 +1219,12 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat } std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) { - return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); + return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); } std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) { - return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); + return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) { +std::string WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1255,7 +1255,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->select_json(obj, obj->has_state() ? obj->current_option() : "", detail); + std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : "", detail); request->send(200, "application/json", data.c_str()); return; } @@ -1276,13 +1276,13 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); + return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); } std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); + return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); } -std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) { +std::string WebServer::select_json_(select::Select *obj, const char *value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1312,7 +1312,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->climate_json(obj, detail); + std::string data = this->climate_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1342,13 +1342,13 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url } std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->climate_json((climate::Climate *) (source), DETAIL_STATE); + return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE); } std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); + return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL); } -std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { +std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1456,7 +1456,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->lock_json(obj, obj->state, detail); + std::string data = this->lock_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1498,12 +1498,12 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) { - return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); + return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); } std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) { - return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); + return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); } -std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { +std::string WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1530,7 +1530,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->valve_json(obj, detail); + std::string data = this->valve_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1577,12 +1577,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) { - return web_server->valve_json((valve::Valve *) (source), DETAIL_STATE); + return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE); } std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) { - return web_server->valve_json((valve::Valve *) (source), DETAIL_ALL); + return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL); } -std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { +std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1614,7 +1614,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail); + std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1655,18 +1655,18 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques request->send(404); } std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) { - return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source), - ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), - DETAIL_STATE); + return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), + ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), + DETAIL_STATE); } std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { - return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source), - ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), - DETAIL_ALL); + return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), + ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), + DETAIL_ALL); } -std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, - JsonDetail start_config) { +std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1696,7 +1696,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->event_json(obj, "", detail); + std::string data = this->event_json_(obj, "", detail); request->send(200, "application/json", data.c_str()); return; } @@ -1711,14 +1711,14 @@ static std::string get_event_type(event::Event *event) { std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); - return web_server->event_json(event, get_event_type(event), DETAIL_STATE); + return web_server->event_json_(event, get_event_type(event), DETAIL_STATE); } // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); - return web_server->event_json(event, get_event_type(event), DETAIL_ALL); + return web_server->event_json_(event, get_event_type(event), DETAIL_ALL); } -std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { +std::string WebServer::event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1764,7 +1764,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->update_json(obj, detail); + std::string data = this->update_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1782,13 +1782,13 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); + return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); + return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } -std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { +std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 98234ec1ae..0078146284 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -271,8 +271,6 @@ class WebServer : public Controller, static std::string sensor_state_json_generator(WebServer *web_server, void *source); static std::string sensor_all_json_generator(WebServer *web_server, void *source); - /// Dump the sensor state with its value as a JSON string. - std::string sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config); #endif #ifdef USE_SWITCH @@ -283,8 +281,6 @@ class WebServer : public Controller, static std::string switch_state_json_generator(WebServer *web_server, void *source); static std::string switch_all_json_generator(WebServer *web_server, void *source); - /// Dump the switch state with its value as a JSON string. - std::string switch_json(switch_::Switch *obj, bool value, JsonDetail start_config); #endif #ifdef USE_BUTTON @@ -293,8 +289,6 @@ class WebServer : public Controller, static std::string button_state_json_generator(WebServer *web_server, void *source); static std::string button_all_json_generator(WebServer *web_server, void *source); - /// Dump the button details with its value as a JSON string. - std::string button_json(button::Button *obj, JsonDetail start_config); #endif #ifdef USE_BINARY_SENSOR @@ -305,8 +299,6 @@ class WebServer : public Controller, static std::string binary_sensor_state_json_generator(WebServer *web_server, void *source); static std::string binary_sensor_all_json_generator(WebServer *web_server, void *source); - /// Dump the binary sensor state with its value as a JSON string. - std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); #endif #ifdef USE_FAN @@ -317,8 +309,6 @@ class WebServer : public Controller, static std::string fan_state_json_generator(WebServer *web_server, void *source); static std::string fan_all_json_generator(WebServer *web_server, void *source); - /// Dump the fan state as a JSON string. - std::string fan_json(fan::Fan *obj, JsonDetail start_config); #endif #ifdef USE_LIGHT @@ -329,8 +319,6 @@ class WebServer : public Controller, static std::string light_state_json_generator(WebServer *web_server, void *source); static std::string light_all_json_generator(WebServer *web_server, void *source); - /// Dump the light state as a JSON string. - std::string light_json(light::LightState *obj, JsonDetail start_config); #endif #ifdef USE_TEXT_SENSOR @@ -341,8 +329,6 @@ class WebServer : public Controller, static std::string text_sensor_state_json_generator(WebServer *web_server, void *source); static std::string text_sensor_all_json_generator(WebServer *web_server, void *source); - /// Dump the text sensor state with its value as a JSON string. - std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_COVER @@ -353,8 +339,6 @@ class WebServer : public Controller, static std::string cover_state_json_generator(WebServer *web_server, void *source); static std::string cover_all_json_generator(WebServer *web_server, void *source); - /// Dump the cover state as a JSON string. - std::string cover_json(cover::Cover *obj, JsonDetail start_config); #endif #ifdef USE_NUMBER @@ -364,8 +348,6 @@ class WebServer : public Controller, static std::string number_state_json_generator(WebServer *web_server, void *source); static std::string number_all_json_generator(WebServer *web_server, void *source); - /// Dump the number state with its value as a JSON string. - std::string number_json(number::Number *obj, float value, JsonDetail start_config); #endif #ifdef USE_DATETIME_DATE @@ -375,8 +357,6 @@ class WebServer : public Controller, static std::string date_state_json_generator(WebServer *web_server, void *source); static std::string date_all_json_generator(WebServer *web_server, void *source); - /// Dump the date state with its value as a JSON string. - std::string date_json(datetime::DateEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_TIME @@ -386,8 +366,6 @@ class WebServer : public Controller, static std::string time_state_json_generator(WebServer *web_server, void *source); static std::string time_all_json_generator(WebServer *web_server, void *source); - /// Dump the time state with its value as a JSON string. - std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_DATETIME @@ -397,8 +375,6 @@ class WebServer : public Controller, static std::string datetime_state_json_generator(WebServer *web_server, void *source); static std::string datetime_all_json_generator(WebServer *web_server, void *source); - /// Dump the datetime state with its value as a JSON string. - std::string datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config); #endif #ifdef USE_TEXT @@ -408,8 +384,6 @@ class WebServer : public Controller, static std::string text_state_json_generator(WebServer *web_server, void *source); static std::string text_all_json_generator(WebServer *web_server, void *source); - /// Dump the text state with its value as a JSON string. - std::string text_json(text::Text *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_SELECT @@ -419,8 +393,6 @@ class WebServer : public Controller, static std::string select_state_json_generator(WebServer *web_server, void *source); static std::string select_all_json_generator(WebServer *web_server, void *source); - /// Dump the select state with its value as a JSON string. - std::string select_json(select::Select *obj, const char *value, JsonDetail start_config); #endif #ifdef USE_CLIMATE @@ -430,8 +402,6 @@ class WebServer : public Controller, static std::string climate_state_json_generator(WebServer *web_server, void *source); static std::string climate_all_json_generator(WebServer *web_server, void *source); - /// Dump the climate details - std::string climate_json(climate::Climate *obj, JsonDetail start_config); #endif #ifdef USE_LOCK @@ -442,8 +412,6 @@ class WebServer : public Controller, static std::string lock_state_json_generator(WebServer *web_server, void *source); static std::string lock_all_json_generator(WebServer *web_server, void *source); - /// Dump the lock state with its value as a JSON string. - std::string lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config); #endif #ifdef USE_VALVE @@ -454,8 +422,6 @@ class WebServer : public Controller, static std::string valve_state_json_generator(WebServer *web_server, void *source); static std::string valve_all_json_generator(WebServer *web_server, void *source); - /// Dump the valve state as a JSON string. - std::string valve_json(valve::Valve *obj, JsonDetail start_config); #endif #ifdef USE_ALARM_CONTROL_PANEL @@ -466,9 +432,6 @@ class WebServer : public Controller, static std::string alarm_control_panel_state_json_generator(WebServer *web_server, void *source); static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source); - /// Dump the alarm_control_panel state with its value as a JSON string. - std::string alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); #endif #ifdef USE_EVENT @@ -479,9 +442,6 @@ class WebServer : public Controller, /// Handle a event request under '/event'. void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match); - - /// Dump the event details with its value as a JSON string. - std::string event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config); #endif #ifdef USE_UPDATE @@ -492,8 +452,6 @@ class WebServer : public Controller, static std::string update_state_json_generator(WebServer *web_server, void *source); static std::string update_all_json_generator(WebServer *web_server, void *source); - /// Dump the update state with its value as a JSON string. - std::string update_json(update::UpdateEntity *obj, JsonDetail start_config); #endif /// Override the web handler's canHandle method. @@ -593,6 +551,69 @@ class WebServer : public Controller, const char *js_include_{nullptr}; #endif bool expose_log_{true}; + + private: +#ifdef USE_SENSOR + std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config); +#endif +#ifdef USE_SWITCH + std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config); +#endif +#ifdef USE_BUTTON + std::string button_json_(button::Button *obj, JsonDetail start_config); +#endif +#ifdef USE_BINARY_SENSOR + std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); +#endif +#ifdef USE_FAN + std::string fan_json_(fan::Fan *obj, JsonDetail start_config); +#endif +#ifdef USE_LIGHT + std::string light_json_(light::LightState *obj, JsonDetail start_config); +#endif +#ifdef USE_TEXT_SENSOR + std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); +#endif +#ifdef USE_COVER + std::string cover_json_(cover::Cover *obj, JsonDetail start_config); +#endif +#ifdef USE_NUMBER + std::string number_json_(number::Number *obj, float value, JsonDetail start_config); +#endif +#ifdef USE_DATETIME_DATE + std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config); +#endif +#ifdef USE_DATETIME_TIME + std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config); +#endif +#ifdef USE_DATETIME_DATETIME + std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config); +#endif +#ifdef USE_TEXT + std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); +#endif +#ifdef USE_SELECT + std::string select_json_(select::Select *obj, const char *value, JsonDetail start_config); +#endif +#ifdef USE_CLIMATE + std::string climate_json_(climate::Climate *obj, JsonDetail start_config); +#endif +#ifdef USE_LOCK + std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config); +#endif +#ifdef USE_VALVE + std::string valve_json_(valve::Valve *obj, JsonDetail start_config); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); +#endif +#ifdef USE_EVENT + std::string event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config); +#endif +#ifdef USE_UPDATE + std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config); +#endif }; } // namespace web_server From f238f9331272c25bf073b921f443e62e75c73c6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 11:37:51 -1000 Subject: [PATCH 4/5] [core] Move comment to PROGMEM on ESP8266 (#12554) --- esphome/components/web_server/web_server.cpp | 4 +- esphome/core/application.h | 23 +++--- esphome/core/build_info_data.h | 2 + esphome/core/config.py | 3 +- esphome/writer.py | 25 +++++-- tests/dummy_main.cpp | 2 +- tests/unit_tests/test_writer.py | 76 +++++++++++++++++--- 7 files changed, 105 insertions(+), 30 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index ece9d65121..b0731f335b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -287,7 +287,9 @@ std::string WebServer::get_config_json() { JsonObject root = builder.root(); root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); - root[ESPHOME_F("comment")] = App.get_comment_ref(); + char comment_buffer[ESPHOME_COMMENT_SIZE]; + App.get_comment_string(comment_buffer); + root[ESPHOME_F("comment")] = comment_buffer; #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal #else diff --git a/esphome/core/application.h b/esphome/core/application.h index d2146a6c16..13461b3ebd 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -12,6 +12,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/core/progmem.h" #include "esphome/core/scheduler.h" #include "esphome/core/string_ref.h" #include "esphome/core/version.h" @@ -107,8 +108,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment, - bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { @@ -127,7 +127,6 @@ class Application { this->name_ = name; this->friendly_name_ = friendly_name; } - this->comment_ = comment; } #ifdef USE_DEVICES @@ -264,10 +263,19 @@ class Application { return ""; } - /// Get the comment of this Application set by pre_setup(). - std::string get_comment() const { return this->comment_; } - /// Get the comment as StringRef (avoids allocation) - StringRef get_comment_ref() const { return StringRef(this->comment_); } + /// Copy the comment string into the provided buffer + /// Buffer must be ESPHOME_COMMENT_SIZE bytes (compile-time enforced) + void get_comment_string(std::span buffer) { + ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, buffer.size()); + buffer[buffer.size() - 1] = '\0'; + } + + /// Get the comment of this Application as a string + std::string get_comment() { + char buffer[ESPHOME_COMMENT_SIZE]; + this->get_comment_string(buffer); + return std::string(buffer); + } bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } @@ -513,7 +521,6 @@ class Application { // Pointer-sized members first Component *current_component_{nullptr}; - const char *comment_{nullptr}; // std::vector (3 pointers each: begin, end, capacity) // Partitioned vector design for looping components diff --git a/esphome/core/build_info_data.h b/esphome/core/build_info_data.h index 5e424ffaca..02bb465e44 100644 --- a/esphome/core/build_info_data.h +++ b/esphome/core/build_info_data.h @@ -7,4 +7,6 @@ #define ESPHOME_CONFIG_HASH 0x12345678U // NOLINT #define ESPHOME_BUILD_TIME 1700000000 // NOLINT +#define ESPHOME_COMMENT_SIZE 1 // NOLINT static const char ESPHOME_BUILD_TIME_STR[] = "2024-01-01 00:00:00 +0000"; +static const char ESPHOME_COMMENT_STR[] = ""; diff --git a/esphome/core/config.py b/esphome/core/config.py index 507a39b401..5e32b9380d 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -209,7 +209,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NAME): cv.valid_name, cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)), cv.Optional(CONF_AREA): validate_area_config, - cv.Optional(CONF_COMMENT): cv.string, + cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)), cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( { @@ -505,7 +505,6 @@ async def to_code(config: ConfigType) -> None: cg.App.pre_setup( config[CONF_NAME], config[CONF_FRIENDLY_NAME], - config.get(CONF_COMMENT, ""), config[CONF_NAME_ADD_MAC_SUFFIX], ) ) diff --git a/esphome/writer.py b/esphome/writer.py index 183fff8730..9ae40e417a 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -21,6 +21,7 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, + cpp_string_escape, get_str_env, is_ha_addon, read_file, @@ -271,7 +272,7 @@ def copy_src_tree(): "esphome", "core", "build_info_data.h" ) build_info_json_path = CORE.relative_build_path("build_info.json") - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() # Defensively force a rebuild if the build_info files don't exist, or if # there was a config change which didn't actually cause a source change @@ -292,7 +293,9 @@ def copy_src_tree(): if sources_changed: write_file( build_info_data_h_path, - generate_build_info_data_h(config_hash, build_time, build_time_str), + generate_build_info_data_h( + config_hash, build_time, build_time_str, comment + ), ) write_file( build_info_json_path, @@ -332,31 +335,39 @@ def generate_version_h(): ) -def get_build_info() -> tuple[int, int, str]: +def get_build_info() -> tuple[int, int, str, str]: """Calculate build_info values from current config. Returns: - Tuple of (config_hash, build_time, build_time_str) + Tuple of (config_hash, build_time, build_time_str, comment) """ config_hash = CORE.config_hash build_time = int(time.time()) build_time_str = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(build_time)) - return config_hash, build_time, build_time_str + comment = CORE.comment or "" + return config_hash, build_time, build_time_str, comment def generate_build_info_data_h( - config_hash: int, build_time: int, build_time_str: str + config_hash: int, build_time: int, build_time_str: str, comment: str ) -> str: - """Generate build_info_data.h header with config hash and build time.""" + """Generate build_info_data.h header with config hash, build time, and comment.""" + # cpp_string_escape returns '"escaped"', slice off the quotes since template has them + escaped_comment = cpp_string_escape(comment)[1:-1] + # +1 for null terminator + comment_size = len(comment) + 1 return f"""#pragma once // Auto-generated build_info data #define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT #define ESPHOME_BUILD_TIME {build_time} // NOLINT +#define ESPHOME_COMMENT_SIZE {comment_size} // NOLINT #ifdef USE_ESP8266 #include static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}"; +static const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}"; #else static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}"; +static const char ESPHOME_COMMENT_STR[] = "{escaped_comment}"; #endif """ diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 5849f4eb95..e6fe733807 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,7 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", "comment", false); + App.pre_setup("livingroom", "LivingRoom", false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 06a7d5dbdf..f354d71bb7 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -1186,8 +1186,9 @@ def test_get_build_info_new_build( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "Test comment" - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert isinstance(build_time, int) @@ -1195,6 +1196,7 @@ def test_get_build_info_new_build( assert isinstance(build_time_str, str) # Verify build_time_str format matches expected pattern assert len(build_time_str) >= 19 # e.g., "2025-12-15 16:27:44 +0000" + assert comment == "Test comment" @patch("esphome.writer.CORE") @@ -1206,6 +1208,7 @@ def test_get_build_info_always_returns_current_time( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create existing build_info.json with matching config_hash and version existing_build_time = 1700000000 @@ -1222,7 +1225,7 @@ def test_get_build_info_always_returns_current_time( ) with patch("esphome.writer.__version__", "2025.1.0-dev"): - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 # get_build_info now always returns current time @@ -1240,6 +1243,7 @@ def test_get_build_info_config_changed( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0xABCDEF00 # Different from existing + mock_core.comment = "" # Create existing build_info.json with different config_hash existing_build_time = 1700000000 @@ -1255,7 +1259,7 @@ def test_get_build_info_config_changed( ) with patch("esphome.writer.__version__", "2025.1.0-dev"): - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0xABCDEF00 assert build_time != existing_build_time # New time generated @@ -1271,6 +1275,7 @@ def test_get_build_info_version_changed( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create existing build_info.json with different version existing_build_time = 1700000000 @@ -1286,7 +1291,7 @@ def test_get_build_info_version_changed( ) with patch("esphome.writer.__version__", "2025.1.0-dev"): # New version - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert build_time != existing_build_time # New time generated @@ -1302,11 +1307,12 @@ def test_get_build_info_invalid_json( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create invalid JSON file build_info_path.write_text("not valid json {{{") - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert isinstance(build_time, int) @@ -1322,12 +1328,13 @@ def test_get_build_info_missing_keys( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create JSON with missing keys build_info_path.write_text(json.dumps({"config_hash": 0x12345678})) with patch("esphome.writer.__version__", "2025.1.0-dev"): - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert isinstance(build_time, int) @@ -1343,8 +1350,9 @@ def test_get_build_info_build_time_str_format( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() # Verify the format matches "%Y-%m-%d %H:%M:%S %z" # e.g., "2025-12-15 16:27:44 +0000" @@ -1357,36 +1365,73 @@ def test_generate_build_info_data_h_format() -> None: config_hash = 0x12345678 build_time = 1700000000 build_time_str = "2023-11-14 22:13:20 +0000" + comment = "Test comment" - result = generate_build_info_data_h(config_hash, build_time, build_time_str) + result = generate_build_info_data_h( + config_hash, build_time, build_time_str, comment + ) assert "#pragma once" in result assert "#define ESPHOME_CONFIG_HASH 0x12345678U" in result assert "#define ESPHOME_BUILD_TIME 1700000000" in result + assert "#define ESPHOME_COMMENT_SIZE 13" in result # len("Test comment") + 1 assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result + assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result def test_generate_build_info_data_h_esp8266_progmem() -> None: """Test generate_build_info_data_h includes PROGMEM for ESP8266.""" - result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test") + result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test", "comment") # Should have ESP8266 PROGMEM conditional assert "#ifdef USE_ESP8266" in result assert "#include " in result assert "PROGMEM" in result + # Both build time and comment should have PROGMEM versions + assert 'ESPHOME_BUILD_TIME_STR[] PROGMEM = "test"' in result + assert 'ESPHOME_COMMENT_STR[] PROGMEM = "comment"' in result def test_generate_build_info_data_h_hash_formatting() -> None: """Test generate_build_info_data_h formats hash with leading zeros.""" # Test with small hash value that needs leading zeros - result = generate_build_info_data_h(0x00000001, 0, "test") + result = generate_build_info_data_h(0x00000001, 0, "test", "") assert "#define ESPHOME_CONFIG_HASH 0x00000001U" in result # Test with larger hash value - result = generate_build_info_data_h(0xFFFFFFFF, 0, "test") + result = generate_build_info_data_h(0xFFFFFFFF, 0, "test", "") assert "#define ESPHOME_CONFIG_HASH 0xffffffffU" in result +def test_generate_build_info_data_h_comment_escaping() -> None: + r"""Test generate_build_info_data_h properly escapes special characters in comment. + + Uses cpp_string_escape which outputs octal escapes for special characters: + - backslash (ASCII 92) -> \134 + - double quote (ASCII 34) -> \042 + - newline (ASCII 10) -> \012 + """ + # Test backslash escaping (ASCII 92 = octal 134) + result = generate_build_info_data_h(0, 0, "test", "backslash\\here") + assert 'ESPHOME_COMMENT_STR[] = "backslash\\134here"' in result + + # Test quote escaping (ASCII 34 = octal 042) + result = generate_build_info_data_h(0, 0, "test", 'has "quotes"') + assert 'ESPHOME_COMMENT_STR[] = "has \\042quotes\\042"' in result + + # Test newline escaping (ASCII 10 = octal 012) + result = generate_build_info_data_h(0, 0, "test", "line1\nline2") + assert 'ESPHOME_COMMENT_STR[] = "line1\\012line2"' in result + + +def test_generate_build_info_data_h_empty_comment() -> None: + """Test generate_build_info_data_h handles empty comment.""" + result = generate_build_info_data_h(0, 0, "test", "") + + assert "#define ESPHOME_COMMENT_SIZE 1" in result # Just null terminator + assert 'ESPHOME_COMMENT_STR[] = ""' in result + + @patch("esphome.writer.CORE") @patch("esphome.writer.iter_components") @patch("esphome.writer.walk_files") @@ -1445,6 +1490,7 @@ def test_copy_src_tree_writes_build_info_files( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "Test comment" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [("core", mock_component)] @@ -1466,6 +1512,8 @@ def test_copy_src_tree_writes_build_info_files( assert "#define ESPHOME_CONFIG_HASH 0xdeadbeefU" in build_info_h_content assert "#define ESPHOME_BUILD_TIME" in build_info_h_content assert "ESPHOME_BUILD_TIME_STR" in build_info_h_content + assert "#define ESPHOME_COMMENT_SIZE" in build_info_h_content + assert "ESPHOME_COMMENT_STR" in build_info_h_content # Verify build_info.json was written build_info_json_path = build_path / "build_info.json" @@ -1517,6 +1565,7 @@ def test_copy_src_tree_detects_config_hash_change( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF # Different from existing + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] @@ -1578,6 +1627,7 @@ def test_copy_src_tree_detects_version_change( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] @@ -1627,6 +1677,7 @@ def test_copy_src_tree_handles_invalid_build_info_json( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] @@ -1700,6 +1751,7 @@ def test_copy_src_tree_build_info_timestamp_behavior( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [("test", mock_component)] @@ -1794,6 +1846,7 @@ def test_copy_src_tree_detects_removed_source_file( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] # No components = file should be removed @@ -1855,6 +1908,7 @@ def test_copy_src_tree_ignores_removed_generated_file( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] From af0d4d2c2cfd6c955bd2903af7a4039d7793c1cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 11:56:07 -1000 Subject: [PATCH 5/5] [web_server] Use stack buffers for value formatting to reduce flash usage (#12575) --- esphome/components/web_server/web_server.cpp | 68 +++++++++++--------- esphome/core/helpers.cpp | 34 ++++++---- esphome/core/helpers.h | 11 +++- 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index b0731f335b..df8a5364cf 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -432,7 +432,7 @@ static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const char *state, const T &value, JsonDetail start_config) { set_json_value(root, obj, prefix, value, start_config); root[ESPHOME_F("state")] = state; @@ -475,9 +475,10 @@ std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail JsonObject root = builder.root(); const auto uom_ref = obj->get_unit_of_measurement_ref(); - - std::string state = - std::isnan(value) ? "NA" : value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); + char buf[VALUE_ACCURACY_MAX_LEN]; + const char *state = std::isnan(value) + ? "NA" + : (value_accuracy_with_uom_to_buf(buf, value, obj->get_accuracy_decimals(), uom_ref), buf); set_json_icon_state_value(root, obj, "sensor", state, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); @@ -522,7 +523,7 @@ std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); + set_json_icon_state_value(root, obj, "text_sensor", value.c_str(), value.c_str(), start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -974,21 +975,20 @@ std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail JsonObject root = builder.root(); const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step()); - std::string val_str = std::isnan(value) - ? "\"NaN\"" - : value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - std::string state_str = std::isnan(value) ? "NA" - : value_accuracy_with_uom_to_string( - value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); + // Need two buffers: one for value, one for state with UOM + char val_buf[VALUE_ACCURACY_MAX_LEN]; + char state_buf[VALUE_ACCURACY_MAX_LEN]; + const char *val_str = std::isnan(value) ? "\"NaN\"" : (value_accuracy_to_buf(val_buf, value, accuracy), val_buf); + const char *state_str = + std::isnan(value) ? "NA" : (value_accuracy_with_uom_to_buf(state_buf, value, accuracy, uom_ref), state_buf); set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config); if (start_config == DETAIL_ALL) { - root[ESPHOME_F("min_value")] = - value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root[ESPHOME_F("max_value")] = - value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root[ESPHOME_F("step")] = - value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); + // ArduinoJson copies the string immediately, so we can reuse val_buf + root[ESPHOME_F("min_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_min_value(), accuracy), val_buf); + root[ESPHOME_F("max_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_max_value(), accuracy), val_buf); + root[ESPHOME_F("step")] = (value_accuracy_to_buf(val_buf, obj->traits.get_step(), accuracy), val_buf); root[ESPHOME_F("mode")] = (int) obj->traits.get_mode(); if (!uom_ref.empty()) root[ESPHOME_F("uom")] = uom_ref; @@ -1230,8 +1230,8 @@ std::string WebServer::text_json_(text::Text *obj, const std::string &value, Jso json::JsonBuilder builder; JsonObject root = builder.root(); - std::string state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value; - set_json_icon_state_value(root, obj, "text", state, value, start_config); + const char *state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value.c_str(); + set_json_icon_state_value(root, obj, "text", state, value.c_str(), start_config); root[ESPHOME_F("min_length")] = obj->traits.get_min_length(); root[ESPHOME_F("max_length")] = obj->traits.get_max_length(); root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str(); @@ -1359,6 +1359,7 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); char buf[PSTR_LOCAL_SIZE]; + char temp_buf[VALUE_ACCURACY_MAX_LEN]; if (start_config == DETAIL_ALL) { JsonArray opt = root[ESPHOME_F("modes")].to(); @@ -1395,8 +1396,10 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con bool has_state = false; root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root[ESPHOME_F("max_temp")] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); - root[ESPHOME_F("min_temp")] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); + root[ESPHOME_F("max_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("min_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf); root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action)); @@ -1419,23 +1422,26 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { - if (!std::isnan(obj->current_temperature)) { - root[ESPHOME_F("current_temperature")] = value_accuracy_to_string(obj->current_temperature, current_accuracy); - } else { - root[ESPHOME_F("current_temperature")] = "NA"; - } + root[ESPHOME_F("current_temperature")] = + std::isnan(obj->current_temperature) + ? "NA" + : (value_accuracy_to_buf(temp_buf, obj->current_temperature, current_accuracy), temp_buf); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { - root[ESPHOME_F("target_temperature_low")] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); + root[ESPHOME_F("target_temperature_low")] = + (value_accuracy_to_buf(temp_buf, obj->target_temperature_low, target_accuracy), temp_buf); root[ESPHOME_F("target_temperature_high")] = - value_accuracy_to_string(obj->target_temperature_high, target_accuracy); + (value_accuracy_to_buf(temp_buf, obj->target_temperature_high, target_accuracy), temp_buf); if (!has_state) { - root[ESPHOME_F("state")] = value_accuracy_to_string( - (obj->target_temperature_high + obj->target_temperature_low) / 2.0f, target_accuracy); + root[ESPHOME_F("state")] = + (value_accuracy_to_buf(temp_buf, (obj->target_temperature_high + obj->target_temperature_low) / 2.0f, + target_accuracy), + temp_buf); } } else { - root[ESPHOME_F("target_temperature")] = value_accuracy_to_string(obj->target_temperature, target_accuracy); + root[ESPHOME_F("target_temperature")] = + (value_accuracy_to_buf(temp_buf, obj->target_temperature, target_accuracy), temp_buf); if (!has_state) root[ESPHOME_F("state")] = root[ESPHOME_F("target_temperature")]; } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index f55f53f16b..d7d32ea28f 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -383,23 +383,33 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de } std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { - normalize_accuracy_decimals(value, accuracy_decimals); - char tmp[32]; // should be enough, but we should maybe improve this at some point. - snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); - return std::string(tmp); + char buf[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(buf, value, accuracy_decimals); + return std::string(buf); } -std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { +size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals) { normalize_accuracy_decimals(value, accuracy_decimals); - // Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm") - // snprintf truncates safely if exceeded, though ESPHome UOMs are typically short - char tmp[64]; + // snprintf returns chars that would be written (excluding null), or negative on error + int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value); + if (len < 0) + return 0; // encoding error + // On truncation, snprintf returns would-be length; actual written is buf.size() - 1 + return static_cast(len) >= buf.size() ? buf.size() - 1 : static_cast(len); +} + +size_t value_accuracy_with_uom_to_buf(std::span buf, float value, + int8_t accuracy_decimals, StringRef unit_of_measurement) { if (unit_of_measurement.empty()) { - snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); - } else { - snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + return value_accuracy_to_buf(buf, value, accuracy_decimals); } - return std::string(tmp); + normalize_accuracy_decimals(value, accuracy_decimals); + // snprintf returns chars that would be written (excluding null), or negative on error + int len = snprintf(buf.data(), buf.size(), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + if (len < 0) + return 0; // encoding error + // On truncation, snprintf returns would-be length; actual written is buf.size() - 1 + return static_cast(len) >= buf.size() ? buf.size() - 1 : static_cast(len); } int8_t step_to_accuracy_decimals(float step) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b575a14d14..769041160c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -886,8 +886,15 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); -/// Create a string from a value, an accuracy in decimals, and a unit of measurement. -std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement); + +/// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null) +static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64; + +/// Format value with accuracy to buffer, returns chars written (excluding null) +size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals); +/// Format value with accuracy and UOM to buffer, returns chars written (excluding null) +size_t value_accuracy_with_uom_to_buf(std::span buf, float value, + int8_t accuracy_decimals, StringRef unit_of_measurement); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step);