From 84fc6ff71a7f7417b3486725702874b79b56100d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Jul 2025 21:47:52 -1000 Subject: [PATCH 01/13] Suppress spurious volatile and Python syntax warnings during builds (#9488) --- esphome/platformio_api.py | 2 ++ esphome/writer.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 808db03231..e34ac028f8 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int: os.environ.setdefault( "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) ) + # Suppress Python syntax warnings from third-party scripts during compilation + os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") cmd = ["platformio"] + list(args) if not CORE.verbose: diff --git a/esphome/writer.py b/esphome/writer.py index 943dfa78cc..ca9e511c19 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -162,6 +162,9 @@ def get_ini_content(): # Sort to avoid changing build unflags order CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) + # Add extra script for C++ flags + CORE.add_platformio_option("extra_scripts", ["pre:cxx_flags.py"]) + content = "[platformio]\n" content += f"description = ESPHome {__version__}\n" @@ -222,6 +225,9 @@ def write_platformio_project(): write_gitignore() write_platformio_ini(content) + # Write extra script for C++ specific flags + write_cxx_flags_script() + DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ #pragma once @@ -394,3 +400,16 @@ def write_gitignore(): if not os.path.isfile(path): with open(file=path, mode="w", encoding="utf-8") as f: f.write(GITIGNORE_CONTENT) + + +CXX_FLAGS_SCRIPT = """# Auto-generated ESPHome script for C++ specific compiler flags +Import("env") + +# Add C++ specific warning flags +env.Append(CXXFLAGS=["-Wno-volatile"]) +""" + + +def write_cxx_flags_script() -> None: + path = CORE.relative_build_path("cxx_flags.py") + write_file_if_changed(path, CXX_FLAGS_SCRIPT) From 78e8001aa8c30415b6a285132ad30f1dbf00347f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 15 Jul 2025 12:09:18 +1000 Subject: [PATCH 02/13] [online_image] Support `byte_order` (#9502) --- esphome/components/online_image/__init__.py | 5 ++++- esphome/components/online_image/online_image.cpp | 16 +++++++++++----- esphome/components/online_image/online_image.h | 7 ++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 3f15db6e50..7a6d25bc7d 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -2,7 +2,7 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components.const import CONF_REQUEST_HEADERS +from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.image import ( CONF_INVERT_ALPHA, @@ -11,6 +11,7 @@ from esphome.components.image import ( Image_, get_image_type_enum, get_transparency_enum, + validate_settings, ) import esphome.config_validation as cv from esphome.const import ( @@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema( rp2040_arduino=cv.Version(0, 0, 0), host=cv.Version(0, 0, 0), ), + validate_settings, ) ) @@ -213,6 +215,7 @@ async def to_code(config): get_image_type_enum(config[CONF_TYPE]), transparent, config[CONF_BUFFER_SIZE], + config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", ) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index d0c743ef93..4e2ecc2c77 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -35,14 +35,15 @@ inline bool is_color_on(const Color &color) { } OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, - image::Transparency transparency, uint32_t download_buffer_size) + image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian) : Image(nullptr, 0, 0, type, transparency), buffer_(nullptr), download_buffer_(download_buffer_size), download_buffer_initial_size_(download_buffer_size), format_(format), fixed_width_(width), - fixed_height_(height) { + fixed_height_(height), + is_big_endian_(is_big_endian) { this->set_url(url); } @@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { break; } case ImageType::IMAGE_TYPE_GRAYSCALE: { - uint8_t gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); + auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { if (gray == 1) { gray = 0; @@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { case ImageType::IMAGE_TYPE_RGB565: { this->map_chroma_key(color); uint16_t col565 = display::ColorUtil::color_to_565(color); - this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); - this->buffer_[pos + 1] = static_cast(col565 & 0xFF); + if (this->is_big_endian_) { + this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); + this->buffer_[pos + 1] = static_cast(col565 & 0xFF); + } else { + this->buffer_[pos + 0] = static_cast(col565 & 0xFF); + this->buffer_[pos + 1] = static_cast((col565 >> 8) & 0xFF); + } if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { this->buffer_[pos + 2] = color.w; } diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 6a2144538f..3326cbe8d6 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -50,7 +50,7 @@ class OnlineImage : public PollingComponent, * @param buffer_size Size of the buffer used to download the image. */ OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, - image::Transparency transparency, uint32_t buffer_size); + image::Transparency transparency, uint32_t buffer_size, bool is_big_endian); void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; @@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent, const int fixed_width_; /** height requested on configuration, or 0 if non specified. */ const int fixed_height_; + /** + * Whether the image is stored in big-endian format. + * This is used to determine how to store 16 bit colors in the buffer. + */ + bool is_big_endian_; /** * Actual width of the current image. If fixed_width_ is specified, * this will be equal to it; otherwise it will be set once the decoding From 35b3f75f7c954ffb9843aca49de39b27378dedda Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 15 Jul 2025 03:11:10 +0100 Subject: [PATCH 03/13] [json] Bump ArduinoJson library to 7.4.2 (#8857) Co-authored-by: J. Nick Koston Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../update/http_request_update.cpp | 14 +-- esphome/components/json/__init__.py | 2 +- esphome/components/json/json_util.cpp | 119 +++++++++--------- .../components/light/light_json_schema.cpp | 33 ++--- .../mqtt/mqtt_alarm_control_panel.cpp | 3 +- .../components/mqtt/mqtt_binary_sensor.cpp | 1 + esphome/components/mqtt/mqtt_button.cpp | 5 +- esphome/components/mqtt/mqtt_client.cpp | 2 + esphome/components/mqtt/mqtt_climate.cpp | 10 +- esphome/components/mqtt/mqtt_component.cpp | 4 +- esphome/components/mqtt/mqtt_cover.cpp | 1 + esphome/components/mqtt/mqtt_date.cpp | 7 +- esphome/components/mqtt/mqtt_datetime.cpp | 13 +- esphome/components/mqtt/mqtt_event.cpp | 9 +- esphome/components/mqtt/mqtt_fan.cpp | 1 + esphome/components/mqtt/mqtt_light.cpp | 12 +- esphome/components/mqtt/mqtt_lock.cpp | 4 +- esphome/components/mqtt/mqtt_number.cpp | 1 + esphome/components/mqtt/mqtt_select.cpp | 3 +- esphome/components/mqtt/mqtt_sensor.cpp | 4 +- esphome/components/mqtt/mqtt_switch.cpp | 4 +- esphome/components/mqtt/mqtt_text.cpp | 1 + esphome/components/mqtt/mqtt_text_sensor.cpp | 4 +- esphome/components/mqtt/mqtt_time.cpp | 7 +- esphome/components/mqtt/mqtt_update.cpp | 1 + esphome/components/mqtt/mqtt_valve.cpp | 4 +- esphome/components/web_server/web_server.cpp | 22 ++-- platformio.ini | 2 +- 28 files changed, 164 insertions(+), 129 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 202c7b88b2..06aa6da6a4 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) { container.reset(); // Release ownership of the container's shared_ptr valid = json::parse_json(response, [this_update](JsonObject root) -> bool { - if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { + if (!root["name"].is() || !root["version"].is() || !root["builds"].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } @@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) { this_update->update_info_.latest_version = root["version"].as(); for (auto build : root["builds"].as()) { - if (!build.containsKey("chipFamily")) { + if (!build["chipFamily"].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } if (build["chipFamily"] == ESPHOME_VARIANT) { - if (!build.containsKey("ota")) { + if (!build["ota"].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - auto ota = build["ota"]; - if (!ota.containsKey("path") || !ota.containsKey("md5")) { + JsonObject ota = build["ota"].as(); + if (!ota["path"].is() || !ota["md5"].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } this_update->update_info_.firmware_url = ota["path"].as(); this_update->update_info_.md5 = ota["md5"].as(); - if (ota.containsKey("summary")) + if (ota["summary"].is()) this_update->update_info_.summary = ota["summary"].as(); - if (ota.containsKey("release_url")) + if (ota["release_url"].is()) this_update->update_info_.release_url = ota["release_url"].as(); return true; diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index 6a0e4c50d2..9773bf67ce 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(1.0) async def to_code(config): - cg.add_library("bblanchon/ArduinoJson", "6.18.5") + cg.add_library("bblanchon/ArduinoJson", "7.4.2") cg.add_define("USE_JSON") cg.add_global(json_ns.using) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 6c66476dc1..94c531222a 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -1,83 +1,76 @@ #include "json_util.h" #include "esphome/core/log.h" +// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h + namespace esphome { namespace json { static const char *const TAG = "json"; -static std::vector global_json_build_buffer; // NOLINT -static const auto ALLOCATOR = RAMAllocator(RAMAllocator::ALLOC_INTERNAL); +// Build an allocator for the JSON Library using the RAMAllocator class +struct SpiRamAllocator : ArduinoJson::Allocator { + void *allocate(size_t size) override { return this->allocator_.allocate(size); } + + void deallocate(void *pointer) override { + // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. + // RAMAllocator::deallocate() requires the size, which we don't have access to here. + // RAMAllocator::deallocate implementation just calls free() regardless of whether + // the memory was allocated with heap_caps_malloc or malloc. + // This is safe because ESP-IDF's heap implementation internally tracks the memory region + // and routes free() to the appropriate heap. + free(pointer); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) + } + + void *reallocate(void *ptr, size_t new_size) override { + return this->allocator_.reallocate(static_cast(ptr), new_size); + } + + protected: + RAMAllocator allocator_{RAMAllocator(RAMAllocator::NONE)}; +}; std::string build_json(const json_build_t &f) { - // Here we are allocating up to 5kb of memory, - // with the heap size minus 2kb to be safe if less than 5kb - // as we can not have a true dynamic sized document. - // The excess memory is freed below with `shrinkToFit()` - auto free_heap = ALLOCATOR.get_max_free_block_size(); - size_t request_size = std::min(free_heap, (size_t) 512); - while (true) { - ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size); - DynamicJsonDocument json_document(request_size); - if (json_document.capacity() == 0) { - ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes", - request_size, free_heap); - return "{}"; - } - JsonObject root = json_document.to(); - f(root); - if (json_document.overflowed()) { - if (request_size == free_heap) { - ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes", - free_heap); - return "{}"; - } - request_size = std::min(request_size * 2, free_heap); - continue; - } - json_document.shrinkToFit(); - ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity()); - std::string output; - serializeJson(json_document, output); - return output; + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + auto doc_allocator = SpiRamAllocator(); + JsonDocument json_document(&doc_allocator); + if (json_document.overflowed()) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); + return "{}"; } + JsonObject root = json_document.to(); + f(root); + if (json_document.overflowed()) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); + return "{}"; + } + std::string output; + serializeJson(json_document, output); + return output; + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } bool parse_json(const std::string &data, const json_parse_t &f) { - // Here we are allocating 1.5 times the data size, - // with the heap size minus 2kb to be safe if less than that - // as we can not have a true dynamic sized document. - // The excess memory is freed below with `shrinkToFit()` - auto free_heap = ALLOCATOR.get_max_free_block_size(); - size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); - while (true) { - DynamicJsonDocument json_document(request_size); - if (json_document.capacity() == 0) { - ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size, - free_heap); - return false; - } - DeserializationError err = deserializeJson(json_document, data); - json_document.shrinkToFit(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + auto doc_allocator = SpiRamAllocator(); + JsonDocument json_document(&doc_allocator); + if (json_document.overflowed()) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); + return false; + } + DeserializationError err = deserializeJson(json_document, data); - JsonObject root = json_document.as(); + JsonObject root = json_document.as(); - if (err == DeserializationError::Ok) { - return f(root); - } else if (err == DeserializationError::NoMemory) { - if (request_size * 2 >= free_heap) { - ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); - return false; - } - ESP_LOGV(TAG, "Increasing memory allocation."); - request_size *= 2; - continue; - } else { - ESP_LOGE(TAG, "Parse error: %s", err.c_str()); - return false; - } - }; + if (err == DeserializationError::Ok) { + return f(root); + } else if (err == DeserializationError::NoMemory) { + ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); + return false; + } + ESP_LOGE(TAG, "Parse error: %s", err.c_str()); return false; + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } } // namespace json diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 6f8cc11f25..26615bae5c 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -9,6 +9,7 @@ namespace light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema void LightJSONSchema::dump_json(LightState &state, JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (state.supports_effects()) root["effect"] = state.get_effect_name(); @@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { if (values.get_color_mode() & ColorCapability::BRIGHTNESS) root["brightness"] = uint8_t(values.get_brightness() * 255); - JsonObject color = root.createNestedObject("color"); + JsonObject color = root["color"].to(); if (values.get_color_mode() & ColorCapability::RGB) { color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); @@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { } void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { - if (root.containsKey("state")) { + if (root["state"].is()) { auto val = parse_on_off(root["state"]); switch (val) { case PARSE_ON: @@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO } } - if (root.containsKey("brightness")) { + if (root["brightness"].is()) { call.set_brightness(float(root["brightness"]) / 255.0f); } - if (root.containsKey("color")) { + if (root["color"].is()) { JsonObject color = root["color"]; // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. float max_rgb = 0.0f; - if (color.containsKey("r")) { + if (color["r"].is()) { float r = float(color["r"]) / 255.0f; max_rgb = fmaxf(max_rgb, r); call.set_red(r); } - if (color.containsKey("g")) { + if (color["g"].is()) { float g = float(color["g"]) / 255.0f; max_rgb = fmaxf(max_rgb, g); call.set_green(g); } - if (color.containsKey("b")) { + if (color["b"].is()) { float b = float(color["b"]) / 255.0f; max_rgb = fmaxf(max_rgb, b); call.set_blue(b); } - if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { + if (color["r"].is() || color["g"].is() || color["b"].is()) { call.set_color_brightness(max_rgb); } - if (color.containsKey("c")) { + if (color["c"].is()) { call.set_cold_white(float(color["c"]) / 255.0f); } - if (color.containsKey("w")) { + if (color["w"].is()) { // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm // white channel in RGBWW. - if (color.containsKey("c")) { + if (color["c"].is()) { call.set_warm_white(float(color["w"]) / 255.0f); } else { call.set_white(float(color["w"]) / 255.0f); @@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO } } - if (root.containsKey("white_value")) { // legacy API + if (root["white_value"].is()) { // legacy API call.set_white(float(root["white_value"]) / 255.0f); } - if (root.containsKey("color_temp")) { + if (root["color_temp"].is()) { call.set_color_temperature(float(root["color_temp"])); } } @@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { LightJSONSchema::parse_color_json(state, call, root); - if (root.containsKey("flash")) { + if (root["flash"].is()) { auto length = uint32_t(float(root["flash"]) * 1000); call.set_flash_length(length); } - if (root.containsKey("transition")) { + if (root["transition"].is()) { auto length = uint32_t(float(root["transition"]) * 1000); call.set_transition_length(length); } - if (root.containsKey("effect")) { + if (root["effect"].is()) { const char *effect = root["effect"]; call.set_effect(effect); } diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 0a38598679..94460c31a7 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() { } void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to(); const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); if (acp_supported_features & ACP_FEAT_ARM_AWAY) { supported_features.add("arm_away"); diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 6d12e88391..2ce4928574 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,6 +30,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor } void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (!this->binary_sensor_->get_device_class().empty()) root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); if (this->binary_sensor_->is_status_binary_sensor()) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 204f60fe67..c619a02344 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -31,9 +31,12 @@ void MQTTButtonComponent::dump_config() { } void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - if (!this->button_->get_device_class().empty()) + if (!this->button_->get_device_class().empty()) { root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } std::string MQTTButtonComponent::component_type() const { return "button"; } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index ab7fd15a35..5b93789447 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() { std::string topic = "esphome/discover/"; topic.append(App.get_name()); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson this->publish_json( topic, [](JsonObject root) { @@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() { #endif }, 2, this->discovery_info_.retain); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } void MQTTClientComponent::dump_config() { diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index a8768114a4..e16f097812 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson auto traits = this->device_->get_traits(); // current_temperature_topic if (traits.get_supports_current_temperature()) { @@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // mode_state_topic root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); // modes - JsonArray modes = root.createNestedArray(MQTT_MODES); + JsonArray modes = root[MQTT_MODES].to(); // sort array for nice UI in HA if (traits.supports_mode(CLIMATE_MODE_AUTO)) modes.add("auto"); @@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // preset_mode_state_topic root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); // presets - JsonArray presets = root.createNestedArray("preset_modes"); + JsonArray presets = root["preset_modes"].to(); if (traits.supports_preset(CLIMATE_PRESET_HOME)) presets.add("home"); if (traits.supports_preset(CLIMATE_PRESET_AWAY)) @@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // fan_mode_state_topic root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); // fan_modes - JsonArray fan_modes = root.createNestedArray("fan_modes"); + JsonArray fan_modes = root["fan_modes"].to(); if (traits.supports_fan_mode(CLIMATE_FAN_ON)) fan_modes.add("on"); if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) @@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // swing_mode_state_topic root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); // swing_modes - JsonArray swing_modes = root.createNestedArray("swing_modes"); + JsonArray swing_modes = root["swing_modes"].to(); if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) swing_modes.add("off"); if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) @@ -163,6 +164,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo config.state_topic = false; config.command_topic = false; + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } void MQTTClimateComponent::setup() { auto traits = this->device_->get_traits(); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index eee5644c9d..b51f4d903e 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -70,6 +70,7 @@ bool MQTTComponent::send_discovery_() { ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return global_mqtt_client->publish_json( this->get_discovery_topic_(discovery_info), [this](JsonObject root) { @@ -155,7 +156,7 @@ bool MQTTComponent::send_discovery_() { } std::string node_area = App.get_area(); - JsonObject device_info = root.createNestedObject(MQTT_DEVICE); + JsonObject device_info = root[MQTT_DEVICE].to(); const auto mac = get_mac_address(); device_info[MQTT_DEVICE_IDENTIFIERS] = mac; device_info[MQTT_DEVICE_NAME] = node_friendly_name; @@ -192,6 +193,7 @@ bool MQTTComponent::send_discovery_() { device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; }, this->qos_, discovery_info.retain); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } uint8_t MQTTComponent::get_qos() const { return this->qos_; } diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 8d09d836f3..6fb61ee469 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -67,6 +67,7 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (!this->cover_->get_device_class().empty()) root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); diff --git a/esphome/components/mqtt/mqtt_date.cpp b/esphome/components/mqtt/mqtt_date.cpp index 088a4788ed..0f0a334ae7 100644 --- a/esphome/components/mqtt/mqtt_date.cpp +++ b/esphome/components/mqtt/mqtt_date.cpp @@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {} void MQTTDateComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { auto call = this->date_->make_call(); - if (root.containsKey("year")) { + if (root["year"].is()) { call.set_year(root["year"]); } - if (root.containsKey("month")) { + if (root["month"].is()) { call.set_month(root["month"]); } - if (root.containsKey("day")) { + if (root["day"].is()) { call.set_day(root["day"]); } call.perform(); @@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() { } bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["year"] = year; root["month"] = month; root["day"] = day; diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp index 4ae6d0d416..5c56baabe0 100644 --- a/esphome/components/mqtt/mqtt_datetime.cpp +++ b/esphome/components/mqtt/mqtt_datetime.cpp @@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim void MQTTDateTimeComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { auto call = this->datetime_->make_call(); - if (root.containsKey("year")) { + if (root["year"].is()) { call.set_year(root["year"]); } - if (root.containsKey("month")) { + if (root["month"].is()) { call.set_month(root["month"]); } - if (root.containsKey("day")) { + if (root["day"].is()) { call.set_day(root["day"]); } - if (root.containsKey("hour")) { + if (root["hour"].is()) { call.set_hour(root["hour"]); } - if (root.containsKey("minute")) { + if (root["minute"].is()) { call.set_minute(root["minute"]); } - if (root.containsKey("second")) { + if (root["second"].is()) { call.set_second(root["second"]); } call.perform(); @@ -68,6 +68,7 @@ bool MQTTDateTimeComponent::send_initial_state() { bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second) { return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["year"] = year; root["month"] = month; root["day"] = day; diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index cf0b90e3d6..f972d545c6 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -16,7 +16,8 @@ using namespace esphome::event; MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + JsonArray event_types = root[MQTT_EVENT_TYPES].to(); for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); @@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() { } bool MQTTEventComponent::publish_event_(const std::string &event_type) { - return this->publish_json(this->get_state_topic_(), - [event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; }); + return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + root[MQTT_EVENT_TYPE] = event_type; + }); } std::string MQTTEventComponent::component_type() const { return "event"; } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 35713bdab6..70e1ae3b4a 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -143,6 +143,7 @@ void MQTTFanComponent::dump_config() { bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (this->state_->get_traits().supports_direction()) { root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic(); diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index f970da7d8c..4f5ff408a4 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() { MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} bool MQTTJSONLightComponent::publish_state_() { - return this->publish_json(this->get_state_topic_(), - [this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); }); + return this->publish_json(this->get_state_topic_(), [this](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + LightJSONSchema::dump_json(*this->state_, root); + }); } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["schema"] = "json"; auto traits = this->state_->get_traits(); root[MQTT_COLOR_MODE] = true; - JsonArray color_modes = root.createNestedArray("supported_color_modes"); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + JsonArray color_modes = root["supported_color_modes"].to(); if (traits.supports_color_mode(ColorMode::ON_OFF)) color_modes.add("onoff"); if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) @@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery if (this->state_->supports_effects()) { root["effect"] = true; - JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST); + JsonArray effect_list = root[MQTT_EFFECT_LIST].to(); for (auto *effect : this->state_->get_effects()) effect_list.add(effect->get_name()); effect_list.add("None"); diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index f4a5126d0c..0412624983 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() { std::string MQTTLockComponent::component_type() const { return "lock"; } const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (this->lock_->traits.get_assumed_state()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (this->lock_->traits.get_assumed_state()) { root[MQTT_OPTIMISTIC] = true; + } if (this->lock_->traits.get_supports_open()) root[MQTT_PAYLOAD_OPEN] = "OPEN"; } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 3a6ea97967..a44632ff30 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -40,6 +40,7 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto &traits = number_->traits; // https://www.home-assistant.io/integrations/number.mqtt/ + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_MIN] = traits.get_min_value(); root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index ea5130f823..b851348306 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -35,7 +35,8 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_ void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto &traits = select_->traits; // https://www.home-assistant.io/integrations/select.mqtt/ - JsonArray options = root.createNestedArray(MQTT_OPTIONS); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + JsonArray options = root[MQTT_OPTIONS].to(); for (const auto &option : traits.get_options()) options.add(option); diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 2cbc291ccf..9324ea9bb1 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,8 +44,10 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (!this->sensor_->get_device_class().empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (!this->sensor_->get_device_class().empty()) { root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + } if (!this->sensor_->get_unit_of_measurement().empty()) root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index 3fd578825a..8b1323bdb2 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -45,8 +45,10 @@ void MQTTSwitchComponent::dump_config() { std::string MQTTSwitchComponent::component_type() const { return "switch"; } const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (this->switch_->assumed_state()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (this->switch_->assumed_state()) { root[MQTT_OPTIMISTIC] = true; + } } bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp index cb852b64cd..5ab0ca9688 100644 --- a/esphome/components/mqtt/mqtt_text.cpp +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; } const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson switch (this->text_->traits.get_mode()) { case TEXT_MODE_TEXT: root[MQTT_MODE] = "text"; diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index b0754bc8b3..0cc5de07a3 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -15,8 +15,10 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (!this->sensor_->get_device_class().empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (!this->sensor_->get_device_class().empty()) { root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + } config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_time.cpp b/esphome/components/mqtt/mqtt_time.cpp index 332ef53cbc..0c95bd8147 100644 --- a/esphome/components/mqtt/mqtt_time.cpp +++ b/esphome/components/mqtt/mqtt_time.cpp @@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {} void MQTTTimeComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { auto call = this->time_->make_call(); - if (root.containsKey("hour")) { + if (root["hour"].is()) { call.set_hour(root["hour"]); } - if (root.containsKey("minute")) { + if (root["minute"].is()) { call.set_minute(root["minute"]); } - if (root.containsKey("second")) { + if (root["second"].is()) { call.set_second(root["second"]); } call.perform(); @@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() { } bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["hour"] = hour; root["minute"] = minute; root["second"] = second; diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index 2ed8faf074..5d4807c7f3 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() { } void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["schema"] = "json"; root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; } diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 85e06fe79c..551398cf42 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() { } } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (!this->valve_->get_device_class().empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (!this->valve_->get_device_class().empty()) { root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); + } auto traits = this->valve_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 8ced5b7e18..170947dc20 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi light::LightJSONSchema::dump_json(*obj, root); if (start_config == DETAIL_ALL) { - JsonArray opt = root.createNestedArray("effects"); + JsonArray opt = root["effects"].to(); opt.add("None"); for (auto const &option : obj->get_effects()) { opt.add(option->get_name()); @@ -1238,7 +1238,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); if (start_config == DETAIL_ALL) { - JsonArray opt = root.createNestedArray("option"); + JsonArray opt = root["option"].to(); for (auto &option : obj->traits.get_options()) { opt.add(option); } @@ -1322,6 +1322,7 @@ std::string WebServer::climate_all_json_generator(WebServer *web_server, void *s return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); } std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); const auto traits = obj->get_traits(); @@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf char buf[16]; if (start_config == DETAIL_ALL) { - JsonArray opt = root.createNestedArray("modes"); + JsonArray opt = root["modes"].to(); for (climate::ClimateMode m : traits.get_supported_modes()) opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); if (!traits.get_supported_custom_fan_modes().empty()) { - JsonArray opt = root.createNestedArray("fan_modes"); + JsonArray opt = root["fan_modes"].to(); for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); } if (!traits.get_supported_custom_fan_modes().empty()) { - JsonArray opt = root.createNestedArray("custom_fan_modes"); + JsonArray opt = root["custom_fan_modes"].to(); for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) opt.add(custom_fan_mode); } if (traits.get_supports_swing_modes()) { - JsonArray opt = root.createNestedArray("swing_modes"); + JsonArray opt = root["swing_modes"].to(); for (auto swing_mode : traits.get_supported_swing_modes()) opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); } if (traits.get_supports_presets() && obj->preset.has_value()) { - JsonArray opt = root.createNestedArray("presets"); + JsonArray opt = root["presets"].to(); for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { - JsonArray opt = root.createNestedArray("custom_presets"); + JsonArray opt = root["custom_presets"].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } @@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf root["state"] = root["target_temperature"]; } }); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } #endif @@ -1635,7 +1637,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty root["event_type"] = event_type; } if (start_config == DETAIL_ALL) { - JsonArray event_types = root.createNestedArray("event_types"); + JsonArray event_types = root["event_types"].to(); for (auto const &event_type : obj->get_event_types()) { event_types.add(event_type); } @@ -1682,6 +1684,7 @@ std::string WebServer::update_all_json_generator(WebServer *web_server, void *so return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); } std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); root["value"] = obj->update_info.latest_version; @@ -1707,6 +1710,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c this->add_sorting_info_(root, obj); } }); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } #endif diff --git a/platformio.ini b/platformio.ini index 54c72eb28d..f9e4e31ece 100644 --- a/platformio.ini +++ b/platformio.ini @@ -35,7 +35,7 @@ build_flags = lib_deps = esphome/noise-c@0.1.10 ; api improv/Improv@1.2.4 ; improv_serial / esp32_improv - bblanchon/ArduinoJson@6.18.5 ; json + bblanchon/ArduinoJson@7.4.2 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier From 42b6939e90fafcd3cc23792e914aad6907a5bf05 Mon Sep 17 00:00:00 2001 From: skyegecko Date: Tue, 15 Jul 2025 04:15:47 +0200 Subject: [PATCH 04/13] [fan] Do not save state for fan if configured as NO_RESTORE (#9472) --- esphome/components/fan/fan.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 25f710f893..82fc5319e0 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -177,6 +177,10 @@ optional Fan::restore_state_() { return {}; } void Fan::save_state_() { + if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) { + return; + } + FanRestoreState state{}; state.state = this->state; state.oscillating = this->oscillating; From 6148dd7e4113ac2dc1feb301a65bd8ee50ca9921 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Jul 2025 16:49:12 -1000 Subject: [PATCH 05/13] Fix LibreTiny compilation error by updating ESPAsyncWebServer and dependencies (#9492) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/async_tcp/__init__.py | 2 +- esphome/components/web_server_base/__init__.py | 2 +- platformio.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 29097ce1b6..4a469fa0e0 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): if CORE.is_esp32 or CORE.is_libretiny: # https://github.com/ESP32Async/AsyncTCP - cg.add_library("ESP32Async/AsyncTCP", "3.4.4") + cg.add_library("ESP32Async/AsyncTCP", "3.4.5") elif CORE.is_esp8266: # https://github.com/ESP32Async/ESPAsyncTCP cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 754bf7d433..9f3371c233 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -40,4 +40,4 @@ async def to_code(config): if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json - cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8") + cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10") diff --git a/platformio.ini b/platformio.ini index f9e4e31ece..8fcc578103 100644 --- a/platformio.ini +++ b/platformio.ini @@ -235,7 +235,7 @@ build_flags = -DUSE_ZEPHYR -DUSE_NRF52 lib_deps = - bblanchon/ArduinoJson@7.0.0 ; json + bblanchon/ArduinoJson@7.4.2 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code pavlodn/HaierProtocol@0.9.31 ; haier functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 From 11a051401f316a743f319384c260c1d35811fbbf Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:36:07 +1000 Subject: [PATCH 06/13] [captive_portal] Add test case for libretiny (#9457) Co-authored-by: J. Nick Koston --- tests/components/captive_portal/test.bk72xx-ard.yaml | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/components/captive_portal/test.bk72xx-ard.yaml diff --git a/tests/components/captive_portal/test.bk72xx-ard.yaml b/tests/components/captive_portal/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 321f2f87b0d251e076a63f6ad82611a7f37a7af8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 14 Jul 2025 22:43:00 -0500 Subject: [PATCH 07/13] [opentherm.output] Fix ``lerp`` (#9506) --- esphome/components/opentherm/output/output.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/opentherm/output/output.cpp b/esphome/components/opentherm/output/output.cpp index f820dc76f1..486aa0d4e7 100644 --- a/esphome/components/opentherm/output/output.cpp +++ b/esphome/components/opentherm/output/output.cpp @@ -10,7 +10,7 @@ void opentherm::OpenthermOutput::write_state(float state) { ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_); this->state = state < 0.003 && this->zero_means_zero_ ? 0.0 - : clamp(lerp(state, min_value_, max_value_), min_value_, max_value_); + : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_); this->has_state_ = true; ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); } From 7f01c25782a3ec84f4ef430eaf5576d71369e473 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 14 Jul 2025 22:45:38 -0500 Subject: [PATCH 08/13] [servo] Fix ``lerp`` (#9507) --- esphome/components/servo/servo.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/servo/servo.cpp b/esphome/components/servo/servo.cpp index b8546d345c..b4511de2d0 100644 --- a/esphome/components/servo/servo.cpp +++ b/esphome/components/servo/servo.cpp @@ -88,9 +88,9 @@ void Servo::internal_write(float value) { value = clamp(value, -1.0f, 1.0f); float level; if (value < 0.0) { - level = lerp(-value, this->idle_level_, this->min_level_); + level = std::lerp(this->idle_level_, this->min_level_, -value); } else { - level = lerp(value, this->idle_level_, this->max_level_); + level = std::lerp(this->idle_level_, this->max_level_, value); } this->output_->set_level(level); this->current_value_ = value; From 786cb7ded5a93db8e4e820e3addd1c5530ebb263 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Jul 2025 20:26:54 -1000 Subject: [PATCH 09/13] Add missing clang-tidy NOLINT comments for ArduinoJson v7 in IDF webserver (#9508) --- esphome/components/web_server_idf/web_server_idf.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index d2447681f5..734259093e 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * #ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson message = json::build_json([group](JsonObject root) { root["name"] = group.second.name; root["sorting_weight"] = group.second.weight; }); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) // a (very) large number of these should be able to be queued initially without defer // since the only thing in the send buffer at this point is the initial ping/config From 9bc3ff5f53270bf7ce67f8f782820f3be128612c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:59:20 +1000 Subject: [PATCH 10/13] [core] Don't issue -Wno-volatile for host platform (#9511) --- esphome/writer.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index ca9e511c19..5438e48570 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -163,7 +163,7 @@ def get_ini_content(): CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) # Add extra script for C++ flags - CORE.add_platformio_option("extra_scripts", ["pre:cxx_flags.py"]) + CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) content = "[platformio]\n" content += f"description = ESPHome {__version__}\n" @@ -402,14 +402,18 @@ def write_gitignore(): f.write(GITIGNORE_CONTENT) -CXX_FLAGS_SCRIPT = """# Auto-generated ESPHome script for C++ specific compiler flags +CXX_FLAGS_FILE_NAME = "cxx_flags.py" +CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags Import("env") -# Add C++ specific warning flags -env.Append(CXXFLAGS=["-Wno-volatile"]) +# Add C++ specific flags """ def write_cxx_flags_script() -> None: - path = CORE.relative_build_path("cxx_flags.py") - write_file_if_changed(path, CXX_FLAGS_SCRIPT) + path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME) + contents = CXX_FLAGS_FILE_CONTENTS + if not CORE.is_host: + contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])' + contents += "\n" + write_file_if_changed(path, contents) From 02b7db73112d73bf5de6d298712c9339ea49dc85 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 15 Jul 2025 19:20:18 +1200 Subject: [PATCH 11/13] [component] Fix ``is_ready`` flag when loop disabled (#9501) Co-authored-by: J. Nick Koston --- esphome/core/component.cpp | 1 + .../loop_test_component/__init__.py | 28 ++++++++++- .../loop_test_component.cpp | 24 +++++++++ .../loop_test_component/loop_test_component.h | 25 ++++++++++ .../fixtures/loop_disable_enable.yaml | 32 ++++++++++++ tests/integration/test_loop_disable_enable.py | 50 +++++++++++++++++++ 6 files changed, 159 insertions(+), 1 deletion(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index b360e1d20b..c47f16b5f7 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -264,6 +264,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::is_ready() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || + (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; } bool Component::can_proceed() { return true; } diff --git a/tests/integration/fixtures/external_components/loop_test_component/__init__.py b/tests/integration/fixtures/external_components/loop_test_component/__init__.py index b66d4598f4..3f3a40db09 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/__init__.py +++ b/tests/integration/fixtures/external_components/loop_test_component/__init__.py @@ -1,7 +1,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME +from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL CODEOWNERS = ["@esphome/tests"] @@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon LoopTestISRComponent = loop_test_component_ns.class_( "LoopTestISRComponent", cg.Component ) +LoopTestUpdateComponent = loop_test_component_ns.class_( + "LoopTestUpdateComponent", cg.PollingComponent +) CONF_DISABLE_AFTER = "disable_after" CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" CONF_ISR_COMPONENTS = "isr_components" +CONF_UPDATE_COMPONENTS = "update_components" +CONF_DISABLE_LOOP_AFTER = "disable_loop_after" COMPONENT_CONFIG_SCHEMA = cv.Schema( { @@ -31,11 +36,23 @@ ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema( } ) +UPDATE_COMPONENT_CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LoopTestUpdateComponent), + cv.Required(CONF_NAME): cv.string, + cv.Optional(CONF_DISABLE_LOOP_AFTER, default=0): cv.int_, + cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval, + } +) + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(LoopTestComponent), cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA), + cv.Optional(CONF_UPDATE_COMPONENTS): cv.ensure_list( + UPDATE_COMPONENT_CONFIG_SCHEMA + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -94,3 +111,12 @@ async def to_code(config): var = cg.new_Pvariable(isr_config[CONF_ID]) await cg.register_component(var, isr_config) cg.add(var.set_name(isr_config[CONF_NAME])) + + # Create update test components + for update_config in config.get(CONF_UPDATE_COMPONENTS, []): + var = cg.new_Pvariable(update_config[CONF_ID]) + await cg.register_component(var, update_config) + + cg.add(var.set_name(update_config[CONF_NAME])) + cg.add(var.set_disable_loop_after(update_config[CONF_DISABLE_LOOP_AFTER])) + cg.add(var.set_update_interval(update_config[CONF_UPDATE_INTERVAL])) diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp index 470740c534..28a05d3d45 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp @@ -39,5 +39,29 @@ void LoopTestComponent::service_disable() { this->disable_loop(); } +// LoopTestUpdateComponent implementation +void LoopTestUpdateComponent::setup() { + ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent setup called", this->name_.c_str()); +} + +void LoopTestUpdateComponent::loop() { + this->loop_count_++; + ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent loop count: %d", this->name_.c_str(), this->loop_count_); + + // Disable loop after specified count to test component.update when loop is disabled + if (this->disable_loop_after_ > 0 && this->loop_count_ == this->disable_loop_after_) { + ESP_LOGI(TAG, "[%s] Disabling loop after %d iterations", this->name_.c_str(), this->disable_loop_after_); + this->disable_loop(); + } +} + +void LoopTestUpdateComponent::update() { + this->update_count_++; + // Check if loop is disabled by testing component state + bool loop_disabled = this->component_state_ == COMPONENT_STATE_LOOP_DONE; + ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent update() called, count: %d, loop_disabled: %s", this->name_.c_str(), + this->update_count_, loop_disabled ? "YES" : "NO"); +} + } // namespace loop_test_component } // namespace esphome diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h index 5c43dd4b43..cdc04d491b 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h @@ -4,6 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" namespace esphome { namespace loop_test_component { @@ -54,5 +55,29 @@ template class DisableAction : public Action { LoopTestComponent *parent_; }; +// Component with update() method to test component.update action +class LoopTestUpdateComponent : public PollingComponent { + public: + LoopTestUpdateComponent() : PollingComponent(1000) {} // Default 1s update interval + + void set_name(const std::string &name) { this->name_ = name; } + void set_disable_loop_after(int count) { this->disable_loop_after_ = count; } + + void setup() override; + void loop() override; + void update() override; + + int get_update_count() const { return this->update_count_; } + int get_loop_count() const { return this->loop_count_; } + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + std::string name_; + int loop_count_{0}; + int update_count_{0}; + int disable_loop_after_{0}; +}; + } // namespace loop_test_component } // namespace esphome diff --git a/tests/integration/fixtures/loop_disable_enable.yaml b/tests/integration/fixtures/loop_disable_enable.yaml index f19d7f60ca..f87fe9130b 100644 --- a/tests/integration/fixtures/loop_disable_enable.yaml +++ b/tests/integration/fixtures/loop_disable_enable.yaml @@ -40,6 +40,13 @@ loop_test_component: - id: isr_test name: "isr_test" + # Update test component to test component.update when loop is disabled + update_components: + - id: update_test_component + name: "update_test" + disable_loop_after: 3 # Disable loop after 3 iterations + update_interval: 0.1s # Fast update interval for testing + # Interval to re-enable the self_disable_10 component after some time interval: - interval: 0.5s @@ -51,3 +58,28 @@ interval: - logger.log: "Re-enabling self_disable_10 via service" - loop_test_component.enable: id: self_disable_10 + + # Test component.update on a component with disabled loop + - interval: 0.1s + then: + - lambda: |- + static bool manual_update_done = false; + if (!manual_update_done && + id(update_test_component).get_loop_count() == 3 && + id(update_test_component).get_update_count() >= 3) { + ESP_LOGI("main", "Manually calling component.update on update_test_component with disabled loop"); + manual_update_done = true; + } + - if: + condition: + lambda: |- + static bool manual_update_triggered = false; + if (!manual_update_triggered && + id(update_test_component).get_loop_count() == 3 && + id(update_test_component).get_update_count() >= 3) { + manual_update_triggered = true; + return true; + } + return false; + then: + - component.update: update_test_component diff --git a/tests/integration/test_loop_disable_enable.py b/tests/integration/test_loop_disable_enable.py index d5f868aa93..e93fc32178 100644 --- a/tests/integration/test_loop_disable_enable.py +++ b/tests/integration/test_loop_disable_enable.py @@ -45,11 +45,18 @@ async def test_loop_disable_enable( isr_component_disabled = asyncio.Event() isr_component_re_enabled = asyncio.Event() isr_component_pure_re_enabled = asyncio.Event() + # Events for update component testing + update_component_loop_disabled = asyncio.Event() + update_component_manual_update_called = asyncio.Event() # Track loop counts for components self_disable_10_counts: list[int] = [] normal_component_counts: list[int] = [] isr_component_counts: list[int] = [] + # Track update component behavior + update_component_loop_count = 0 + update_component_update_count = 0 + update_component_manual_update_count = 0 def on_log_line(line: str) -> None: """Process each log line from the process output.""" @@ -59,6 +66,7 @@ async def test_loop_disable_enable( if ( "loop_test_component" not in clean_line and "loop_test_isr_component" not in clean_line + and "Manually calling component.update" not in clean_line ): return @@ -112,6 +120,23 @@ async def test_loop_disable_enable( elif "Running after pure ISR re-enable!" in clean_line: isr_component_pure_re_enabled.set() + # Update component events + elif "[update_test]" in clean_line: + if "LoopTestUpdateComponent loop count:" in clean_line: + nonlocal update_component_loop_count + update_component_loop_count = int( + clean_line.split("LoopTestUpdateComponent loop count: ")[1] + ) + elif "LoopTestUpdateComponent update() called" in clean_line: + nonlocal update_component_update_count + update_component_update_count += 1 + if "Manually calling component.update" in " ".join(log_messages[-5:]): + nonlocal update_component_manual_update_count + update_component_manual_update_count += 1 + update_component_manual_update_called.set() + elif "Disabling loop after" in clean_line: + update_component_loop_disabled.set() + # Write, compile and run the ESPHome device with log callback async with ( run_compiled(yaml_config, line_callback=on_log_line), @@ -205,3 +230,28 @@ async def test_loop_disable_enable( assert final_count > 10, ( f"Component didn't run after pure ISR enable: got {final_count} counts total" ) + + # Test component.update functionality when loop is disabled + # Wait for update component to disable its loop + try: + await asyncio.wait_for(update_component_loop_disabled.wait(), timeout=3.0) + except asyncio.TimeoutError: + pytest.fail("Update component did not disable its loop within 3 seconds") + + # Verify it ran exactly 3 loops before disabling + assert update_component_loop_count == 3, ( + f"Expected 3 loop iterations before disable, got {update_component_loop_count}" + ) + + # Wait for manual component.update to be called + try: + await asyncio.wait_for( + update_component_manual_update_called.wait(), timeout=5.0 + ) + except asyncio.TimeoutError: + pytest.fail("Manual component.update was not called within 5 seconds") + + # The key test: verify that manual component.update worked after loop was disabled + assert update_component_manual_update_count >= 1, ( + "component.update did not fire after loop was disabled" + ) From 37982290f7ac1203dc713dd028162226b061e295 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:35:55 +1200 Subject: [PATCH 12/13] Bump version to 2025.7.0b4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 42af703a94..e09116d202 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.7.0b3 +PROJECT_NUMBER = 2025.7.0b4 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index de15050d0c..44ec5ec9b9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.7.0b3" +__version__ = "2025.7.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From bd0fe34b148ea93d01b3a0264b25e78c2cdf1267 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Tue, 15 Jul 2025 21:33:15 +0200 Subject: [PATCH 13/13] [ms8607] Fix humidity calc (#9499) --- esphome/components/ms8607/ms8607.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index b985623b24..f8ea26bfd9 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -356,7 +356,7 @@ void MS8607Component::read_humidity_(float temperature_float) { // map 16 bit humidity value into range [-6%, 118%] float const humidity_partial = double(humidity) / (1 << 16); - float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0); + float const humidity_percentage = std::lerp(-6.0, 118.0, humidity_partial); float const compensated_humidity_percentage = humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);