From 2b9e1ce315bc3bcdf865ce0f5097b0b97708e690 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sat, 9 Aug 2025 13:09:40 +0200 Subject: [PATCH 01/19] [switch] Add trigger ``on_state`` (#10108) --- esphome/components/switch/__init__.py | 12 ++++++++++++ esphome/components/switch/automation.h | 7 +++++++ tests/components/switch/common.yaml | 12 ++++++++++++ 3 files changed, 31 insertions(+) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 4668a1458c..f495dbc0b4 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_ID, CONF_INVERTED, CONF_MQTT_ID, + CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_RESTORE_MODE, @@ -56,6 +57,9 @@ TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action) SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action) SwitchCondition = switch_ns.class_("SwitchCondition", Condition) +SwitchStateTrigger = switch_ns.class_( + "SwitchStateTrigger", automation.Trigger.template(bool) +) SwitchTurnOnTrigger = switch_ns.class_( "SwitchTurnOnTrigger", automation.Trigger.template() ) @@ -77,6 +81,11 @@ _SWITCH_SCHEMA = ( cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( RESTORE_MODES, upper=True, space="_" ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchStateTrigger), + } + ), cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), @@ -140,6 +149,9 @@ async def setup_switch_core_(var, config): if (inverted := config.get(CONF_INVERTED)) is not None: cg.add(var.set_inverted(inverted)) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(bool, "x")], conf) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 66818a80be..b8cbc9b976 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -64,6 +64,13 @@ template class SwitchCondition : public Condition { bool state_; }; +class SwitchStateTrigger : public Trigger { + public: + SwitchStateTrigger(Switch *a_switch) { + a_switch->add_on_state_callback([this](bool state) { this->trigger(state); }); + } +}; + class SwitchTurnOnTrigger : public Trigger<> { public: SwitchTurnOnTrigger(Switch *a_switch) { diff --git a/tests/components/switch/common.yaml b/tests/components/switch/common.yaml index b69e36a1c0..afdf26c150 100644 --- a/tests/components/switch/common.yaml +++ b/tests/components/switch/common.yaml @@ -9,6 +9,18 @@ switch: name: "Template Switch" id: the_switch optimistic: true + on_state: + - if: + condition: + - lambda: return x; + then: + - logger.log: "Switch turned ON" + else: + - logger.log: "Switch turned OFF" + on_turn_on: + - logger.log: "Switch is now ON" + on_turn_off: + - logger.log: "Switch is now OFF" esphome: on_boot: From cef39e7c597dd1ee3398108931a9a4d5b97278c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 04:44:23 -0500 Subject: [PATCH 02/19] [esp32_ble_tracker] Fix false reboots when event loop is blocked (#10144) --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 47 ++++++++++++++++--- .../esp32_ble_tracker/esp32_ble_tracker.h | 8 ++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 5e97c81044..0455d136df 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -101,6 +101,38 @@ void ESP32BLETracker::loop() { this->start_scan(); } } + + // Check for scan timeout - moved here from scheduler to avoid false reboots + // when the loop is blocked + if (this->scanner_state_ == ScannerState::RUNNING) { + switch (this->scan_timeout_state_) { + case ScanTimeoutState::MONITORING: { + uint32_t now = App.get_loop_component_start_time(); + uint32_t timeout_ms = this->scan_duration_ * 2000; + // Robust time comparison that handles rollover correctly + // This works because unsigned arithmetic wraps around predictably + if ((now - this->scan_start_time_) > timeout_ms) { + // First time we've seen the timeout exceeded - wait one more loop iteration + // This ensures all components have had a chance to process pending events + // This is because esp32_ble may not have run yet and called + // gap_scan_event_handler yet when the loop unblocks + ESP_LOGW(TAG, "Scan timeout exceeded"); + this->scan_timeout_state_ = ScanTimeoutState::EXCEEDED_WAIT; + } + break; + } + case ScanTimeoutState::EXCEEDED_WAIT: + // We've waited at least one full loop iteration, and scan is still running + ESP_LOGE(TAG, "Scan never terminated, rebooting"); + App.reboot(); + break; + + case ScanTimeoutState::INACTIVE: + // This case should be unreachable - scanner and timeout states are always synchronized + break; + } + } + ClientStateCounts counts = this->count_client_states_(); if (counts != this->client_state_counts_) { this->client_state_counts_ = counts; @@ -164,7 +196,8 @@ void ESP32BLETracker::stop_scan_() { ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); return; } - this->cancel_timeout("scan"); + // Reset timeout state machine when stopping scan + this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; this->set_scanner_state_(ScannerState::STOPPING); esp_err_t err = esp_ble_gap_stop_scanning(); if (err != ESP_OK) { @@ -197,11 +230,10 @@ void ESP32BLETracker::start_scan_(bool first) { this->scan_params_.scan_interval = this->scan_interval_; this->scan_params_.scan_window = this->scan_window_; - // Start timeout before scan is started. Otherwise scan never starts if any error. - this->set_timeout("scan", this->scan_duration_ * 2000, []() { - ESP_LOGE(TAG, "Scan never terminated, rebooting to restore stack (IDF)"); - App.reboot(); - }); + // Start timeout monitoring in loop() instead of using scheduler + // This prevents false reboots when the loop is blocked + this->scan_start_time_ = App.get_loop_component_start_time(); + this->scan_timeout_state_ = ScanTimeoutState::MONITORING; esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); if (err != ESP_OK) { @@ -752,7 +784,8 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { #ifdef USE_ESP32_BLE_DEVICE this->already_discovered_.clear(); #endif - this->cancel_timeout("scan"); + // Reset timeout state machine instead of cancelling scheduler timeout + this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; for (auto *listener : this->listeners_) listener->on_scan_end(); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 4b09d521b6..bf99026810 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -367,6 +367,14 @@ class ESP32BLETracker : public Component, #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE bool coex_prefer_ble_{false}; #endif + // Scan timeout state machine + enum class ScanTimeoutState : uint8_t { + INACTIVE, // No timeout monitoring + MONITORING, // Actively monitoring for timeout + EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot + }; + uint32_t scan_start_time_{0}; + ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE}; }; // NOLINTNEXTLINE From d5c9c10b3bd6eb5576affa9c21cd7ae2f72d5a68 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 10 Aug 2025 13:27:08 -0400 Subject: [PATCH 03/19] [esp32] Add IDF log_level option (#10134) --- esphome/components/esp32/__init__.py | 17 +++++++++++++++++ esphome/components/lvgl/__init__.py | 9 +++++---- esphome/components/lvgl/defines.py | 1 - esphome/const.py | 1 + 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index c43cafc100..c219b8851a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_FRAMEWORK, CONF_IGNORE_EFUSE_CUSTOM_MAC, CONF_IGNORE_EFUSE_MAC_CRC, + CONF_LOG_LEVEL, CONF_NAME, CONF_PATH, CONF_PLATFORM_VERSION, @@ -79,6 +80,15 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_RELEASE = "release" +LOG_LEVELS_IDF = [ + "NONE", + "ERROR", + "WARN", + "INFO", + "DEBUG", + "VERBOSE", +] + ASSERTION_LEVELS = { "DISABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE", "ENABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE", @@ -623,6 +633,9 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { cv.string_strict: cv.string_strict }, + cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( + *LOG_LEVELS_IDF, upper=True + ), cv.Optional(CONF_ADVANCED, default={}): cv.Schema( { cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( @@ -937,6 +950,10 @@ async def to_code(config): ), ) + add_idf_sdkconfig_option( + f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True + ) + for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index a37f4570f3..5af61300da 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_GROUP, CONF_ID, CONF_LAMBDA, + CONF_LOG_LEVEL, CONF_ON_BOOT, CONF_ON_IDLE, CONF_PAGES, @@ -186,7 +187,7 @@ def multi_conf_validate(configs: list[dict]): base_config = configs[0] for config in configs[1:]: for item in ( - df.CONF_LOG_LEVEL, + CONF_LOG_LEVEL, CONF_COLOR_DEPTH, df.CONF_BYTE_ORDER, df.CONF_TRANSPARENCY_KEY, @@ -269,11 +270,11 @@ async def to_code(configs): add_define( "LV_LOG_LEVEL", - f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[df.CONF_LOG_LEVEL]]}", + f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[CONF_LOG_LEVEL]]}", ) cg.add_define( "LVGL_LOG_LEVEL", - cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"), + cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"), ) add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) for font in helpers.lv_fonts_used: @@ -423,7 +424,7 @@ LVGL_SCHEMA = cv.All( cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage, - cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of( + cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( *df.LV_LOG_LEVELS, upper=True ), cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 206a3d1622..8f09a3a6d0 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -456,7 +456,6 @@ CONF_KEYPADS = "keypads" CONF_LAYOUT = "layout" CONF_LEFT_BUTTON = "left_button" CONF_LINE_WIDTH = "line_width" -CONF_LOG_LEVEL = "log_level" CONF_LONG_PRESS_TIME = "long_press_time" CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" CONF_LVGL_ID = "lvgl_id" diff --git a/esphome/const.py b/esphome/const.py index 7d373ff26c..56e0edab9b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -523,6 +523,7 @@ CONF_LOADED_INTEGRATIONS = "loaded_integrations" CONF_LOCAL = "local" CONF_LOCK_ACTION = "lock_action" CONF_LOG = "log" +CONF_LOG_LEVEL = "log_level" CONF_LOG_TOPIC = "log_topic" CONF_LOGGER = "logger" CONF_LOGS = "logs" From a1371aea37d64796449bc2fcd598c04111146d2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 16:04:40 -0500 Subject: [PATCH 04/19] [dashboard] Fix port fallback regression when device is offline (#10135) --- esphome/dashboard/web_server.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 46f09336bb..9db389c39a 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -324,14 +324,13 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): configuration = json_message["configuration"] config_file = settings.rel_path(configuration) port = json_message["port"] - addresses: list[str] = [port] + addresses: list[str] = [] if ( port == "OTA" # pylint: disable=too-many-boolean-expressions and (entry := entries.get(config_file)) and entry.loaded_integrations and "api" in entry.loaded_integrations ): - addresses = [] # First priority: entry.address AKA use_address if ( (use_address := entry.address) @@ -359,6 +358,13 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): # since MQTT logging will not work otherwise addresses.extend(sort_ip_addresses(new_addresses)) + if not addresses: + # If no address was found, use the port directly + # as otherwise they will get the chooser which + # does not work with the dashboard as there is no + # interactive way to get keyboard input + addresses = [port] + device_args: list[str] = [ arg for address in addresses for arg in ("--device", address) ] From 6bfe281d189c0b075396b350e781966395063784 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 16:09:31 -0500 Subject: [PATCH 05/19] [web_server] Reduce flash usage by consolidating parameter parsing (#10154) --- esphome/components/web_server/web_server.cpp | 208 +++++-------------- esphome/components/web_server/web_server.h | 60 ++++++ 2 files changed, 109 insertions(+), 159 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a8d94d80da..92c5961f87 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -635,15 +635,8 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) { auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off(); - if (request->hasParam("speed_level")) { - auto speed_level = request->getParam("speed_level")->value(); - auto val = parse_number(speed_level.c_str()); - if (!val.has_value()) { - ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str()); - return; - } - call.set_speed(*val); - } + parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed); + if (request->hasParam("oscillation")) { auto speed = request->getParam("oscillation")->value(); auto val = parse_on_off(speed.c_str()); @@ -715,69 +708,26 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa request->send(200); } else if (match.method_equals("turn_on")) { auto call = obj->turn_on(); - if (request->hasParam("brightness")) { - auto brightness = parse_number(request->getParam("brightness")->value().c_str()); - if (brightness.has_value()) { - call.set_brightness(*brightness / 255.0f); - } - } - if (request->hasParam("r")) { - auto r = parse_number(request->getParam("r")->value().c_str()); - if (r.has_value()) { - call.set_red(*r / 255.0f); - } - } - if (request->hasParam("g")) { - auto g = parse_number(request->getParam("g")->value().c_str()); - if (g.has_value()) { - call.set_green(*g / 255.0f); - } - } - if (request->hasParam("b")) { - auto b = parse_number(request->getParam("b")->value().c_str()); - if (b.has_value()) { - call.set_blue(*b / 255.0f); - } - } - if (request->hasParam("white_value")) { - auto white_value = parse_number(request->getParam("white_value")->value().c_str()); - if (white_value.has_value()) { - call.set_white(*white_value / 255.0f); - } - } - if (request->hasParam("color_temp")) { - auto color_temp = parse_number(request->getParam("color_temp")->value().c_str()); - if (color_temp.has_value()) { - call.set_color_temperature(*color_temp); - } - } - if (request->hasParam("flash")) { - auto flash = parse_number(request->getParam("flash")->value().c_str()); - if (flash.has_value()) { - call.set_flash_length(*flash * 1000); - } - } - if (request->hasParam("transition")) { - auto transition = parse_number(request->getParam("transition")->value().c_str()); - if (transition.has_value()) { - call.set_transition_length(*transition * 1000); - } - } - if (request->hasParam("effect")) { - const char *effect = request->getParam("effect")->value().c_str(); - call.set_effect(effect); - } + + // Parse color parameters + parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); + parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); + parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); + parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); + parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); + parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); + + // Parse timing parameters + parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); + parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); + + parse_string_param_(request, "effect", call, &decltype(call)::set_effect); this->defer([call]() mutable { call.perform(); }); request->send(200); } else if (match.method_equals("turn_off")) { auto call = obj->turn_off(); - if (request->hasParam("transition")) { - auto transition = parse_number(request->getParam("transition")->value().c_str()); - if (transition.has_value()) { - call.set_transition_length(*transition * 1000); - } - } + parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); this->defer([call]() mutable { call.perform(); }); request->send(200); } else { @@ -850,18 +800,8 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa return; } - if (request->hasParam("position")) { - auto position = parse_number(request->getParam("position")->value().c_str()); - if (position.has_value()) { - call.set_position(*position); - } - } - if (request->hasParam("tilt")) { - auto tilt = parse_number(request->getParam("tilt")->value().c_str()); - if (tilt.has_value()) { - call.set_tilt(*tilt); - } - } + parse_float_param_(request, "position", call, &decltype(call)::set_position); + parse_float_param_(request, "tilt", call, &decltype(call)::set_tilt); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -915,11 +855,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - if (request->hasParam("value")) { - auto value = parse_number(request->getParam("value")->value().c_str()); - if (value.has_value()) - call.set_value(*value); - } + parse_float_param_(request, "value", call, &decltype(call)::set_value); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -991,10 +927,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat return; } - if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); // NOLINT - call.set_date(value); - } + parse_string_param_(request, "value", call, &decltype(call)::set_date); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1050,10 +983,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat return; } - if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); // NOLINT - call.set_time(value); - } + parse_string_param_(request, "value", call, &decltype(call)::set_time); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1108,10 +1038,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur return; } - if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); // NOLINT - call.set_datetime(value); - } + parse_string_param_(request, "value", call, &decltype(call)::set_datetime); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1162,10 +1089,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat } auto call = obj->make_call(); - if (request->hasParam("value")) { - String value = request->getParam("value")->value(); - call.set_value(value.c_str()); // NOLINT - } + parse_string_param_(request, "value", call, &decltype(call)::set_value); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1224,11 +1148,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - - if (request->hasParam("option")) { - auto option = request->getParam("option")->value(); - call.set_option(option.c_str()); // NOLINT - } + parse_string_param_(request, "option", call, &decltype(call)::set_option); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1284,38 +1204,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url auto call = obj->make_call(); - if (request->hasParam("mode")) { - auto mode = request->getParam("mode")->value(); - call.set_mode(mode.c_str()); // NOLINT - } + // Parse string mode parameters + parse_string_param_(request, "mode", call, &decltype(call)::set_mode); + parse_string_param_(request, "fan_mode", call, &decltype(call)::set_fan_mode); + parse_string_param_(request, "swing_mode", call, &decltype(call)::set_swing_mode); - if (request->hasParam("fan_mode")) { - auto mode = request->getParam("fan_mode")->value(); - call.set_fan_mode(mode.c_str()); // NOLINT - } - - if (request->hasParam("swing_mode")) { - auto mode = request->getParam("swing_mode")->value(); - call.set_swing_mode(mode.c_str()); // NOLINT - } - - if (request->hasParam("target_temperature_high")) { - auto target_temperature_high = parse_number(request->getParam("target_temperature_high")->value().c_str()); - if (target_temperature_high.has_value()) - call.set_target_temperature_high(*target_temperature_high); - } - - if (request->hasParam("target_temperature_low")) { - auto target_temperature_low = parse_number(request->getParam("target_temperature_low")->value().c_str()); - if (target_temperature_low.has_value()) - call.set_target_temperature_low(*target_temperature_low); - } - - if (request->hasParam("target_temperature")) { - auto target_temperature = parse_number(request->getParam("target_temperature")->value().c_str()); - if (target_temperature.has_value()) - call.set_target_temperature(*target_temperature); - } + // Parse temperature parameters + parse_float_param_(request, "target_temperature_high", call, &decltype(call)::set_target_temperature_high); + parse_float_param_(request, "target_temperature_low", call, &decltype(call)::set_target_temperature_low); + parse_float_param_(request, "target_temperature", call, &decltype(call)::set_target_temperature); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1506,12 +1403,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa return; } - if (request->hasParam("position")) { - auto position = parse_number(request->getParam("position")->value().c_str()); - if (position.has_value()) { - call.set_position(*position); - } - } + parse_float_param_(request, "position", call, &decltype(call)::set_position); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1559,9 +1451,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } auto call = obj->make_call(); - if (request->hasParam("code")) { - call.set_code(request->getParam("code")->value().c_str()); // NOLINT - } + parse_string_param_(request, "code", call, &decltype(call)::set_code); if (match.method_equals("disarm")) { call.disarm(); @@ -1659,6 +1549,19 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty #endif #ifdef USE_UPDATE +static const char *update_state_to_string(update::UpdateState state) { + switch (state) { + case update::UPDATE_STATE_NO_UPDATE: + return "NO UPDATE"; + case update::UPDATE_STATE_AVAILABLE: + return "UPDATE AVAILABLE"; + case update::UPDATE_STATE_INSTALLING: + return "INSTALLING"; + default: + return "UNKNOWN"; + } +} + void WebServer::on_update(update::UpdateEntity *obj) { if (this->events_.empty()) return; @@ -1698,20 +1601,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c 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; - switch (obj->state) { - case update::UPDATE_STATE_NO_UPDATE: - root["state"] = "NO UPDATE"; - break; - case update::UPDATE_STATE_AVAILABLE: - root["state"] = "UPDATE AVAILABLE"; - break; - case update::UPDATE_STATE_INSTALLING: - root["state"] = "INSTALLING"; - break; - default: - root["state"] = "UNKNOWN"; - break; - } + root["state"] = update_state_to_string(obj->state); if (start_config == DETAIL_ALL) { root["current_version"] = obj->update_info.current_version; root["title"] = obj->update_info.title; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index ef1b03a73b..6bece732fc 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -498,6 +498,66 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { protected: void add_sorting_info_(JsonObject &root, EntityBase *entity); + +#ifdef USE_LIGHT + // Helper to parse and apply a float parameter with optional scaling + template + void parse_light_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float), + float scale = 1.0f) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value / scale); + } + } + } + + // Helper to parse and apply a uint32_t parameter with optional scaling + template + void parse_light_param_uint_(AsyncWebServerRequest *request, const char *param_name, T &call, + Ret (T::*setter)(uint32_t), uint32_t scale = 1) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value * scale); + } + } + } +#endif + + // Generic helper to parse and apply a float parameter + template + void parse_float_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float)) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value); + } + } + } + + // Generic helper to parse and apply an int parameter + template + void parse_int_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(int)) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value); + } + } + } + + // Generic helper to parse and apply a string parameter + template + void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call, + Ret (T::*setter)(const std::string &)) { + if (request->hasParam(param_name)) { + // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string + std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr) + (call.*setter)(value); + } + } + web_server_base::WebServerBase *base_; #ifdef USE_ARDUINO DeferredUpdateEventSourceList events_; From 279f56141ecd587aae369ef32e875a36553e8f2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 16:12:36 -0500 Subject: [PATCH 06/19] [ade7880] Fix duplicate sensor name validation error (#10155) --- esphome/components/ade7880/sensor.py | 124 ++++++++++++++++----------- tests/components/ade7880/common.yaml | 38 ++++---- 2 files changed, 94 insertions(+), 68 deletions(-) diff --git a/esphome/components/ade7880/sensor.py b/esphome/components/ade7880/sensor.py index 3ef5e6bfff..39dbeb225f 100644 --- a/esphome/components/ade7880/sensor.py +++ b/esphome/components/ade7880/sensor.py @@ -36,6 +36,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -51,6 +52,20 @@ CONF_POWER_GAIN = "power_gain" CONF_NEUTRAL = "neutral" +# Tuple of power channel phases +POWER_PHASES = (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C) + +# Tuple of sensor types that can be configured for power channels +POWER_SENSOR_TYPES = ( + CONF_CURRENT, + CONF_VOLTAGE, + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_POWER_FACTOR, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, +) + NEUTRAL_CHANNEL_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(NeutralChannel), @@ -150,7 +165,64 @@ POWER_CHANNEL_SCHEMA = cv.Schema( } ) -CONFIG_SCHEMA = ( + +def prefix_sensor_name( + sensor_conf: ConfigType, + channel_name: str, + channel_config: ConfigType, + sensor_type: str, +) -> None: + """Helper to prefix sensor name with channel name. + + Args: + sensor_conf: The sensor configuration (dict or string) + channel_name: The channel name to prefix with + channel_config: The channel configuration to update + sensor_type: The sensor type key in the channel config + """ + if isinstance(sensor_conf, dict) and CONF_NAME in sensor_conf: + sensor_name = sensor_conf[CONF_NAME] + if sensor_name and not sensor_name.startswith(channel_name): + sensor_conf[CONF_NAME] = f"{channel_name} {sensor_name}" + elif isinstance(sensor_conf, str): + # Simple value case - convert to dict with prefixed name + channel_config[sensor_type] = {CONF_NAME: f"{channel_name} {sensor_conf}"} + + +def process_channel_sensors( + config: ConfigType, channel_key: str, sensor_types: tuple +) -> None: + """Process sensors for a channel and prefix their names. + + Args: + config: The main configuration + channel_key: The channel key (e.g., CONF_PHASE_A, CONF_NEUTRAL) + sensor_types: Tuple of sensor types to process for this channel + """ + if not (channel_config := config.get(channel_key)) or not ( + channel_name := channel_config.get(CONF_NAME) + ): + return + + for sensor_type in sensor_types: + if sensor_conf := channel_config.get(sensor_type): + prefix_sensor_name(sensor_conf, channel_name, channel_config, sensor_type) + + +def preprocess_channels(config: ConfigType) -> ConfigType: + """Preprocess channel configurations to add channel name prefix to sensor names.""" + # Process power channels + for channel in POWER_PHASES: + process_channel_sensors(config, channel, POWER_SENSOR_TYPES) + + # Process neutral channel + process_channel_sensors(config, CONF_NEUTRAL, (CONF_CURRENT,)) + + return config + + +CONFIG_SCHEMA = cv.All( + preprocess_channels, cv.Schema( { cv.GenerateID(): cv.declare_id(ADE7880), @@ -167,7 +239,7 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x38)) + .extend(i2c.i2c_device_schema(0x38)), ) @@ -188,15 +260,7 @@ async def neutral_channel(config): async def power_channel(config): var = cg.new_Pvariable(config[CONF_ID]) - for sensor_type in [ - CONF_CURRENT, - CONF_VOLTAGE, - CONF_ACTIVE_POWER, - CONF_APPARENT_POWER, - CONF_POWER_FACTOR, - CONF_FORWARD_ACTIVE_ENERGY, - CONF_REVERSE_ACTIVE_ENERGY, - ]: + for sensor_type in POWER_SENSOR_TYPES: if conf := config.get(sensor_type): sens = await sensor.new_sensor(conf) cg.add(getattr(var, f"set_{sensor_type}")(sens)) @@ -216,44 +280,6 @@ async def power_channel(config): return var -def final_validate(config): - for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]: - if channel := config.get(channel): - channel_name = channel.get(CONF_NAME) - - for sensor_type in [ - CONF_CURRENT, - CONF_VOLTAGE, - CONF_ACTIVE_POWER, - CONF_APPARENT_POWER, - CONF_POWER_FACTOR, - CONF_FORWARD_ACTIVE_ENERGY, - CONF_REVERSE_ACTIVE_ENERGY, - ]: - if conf := channel.get(sensor_type): - sensor_name = conf.get(CONF_NAME) - if ( - sensor_name - and channel_name - and not sensor_name.startswith(channel_name) - ): - conf[CONF_NAME] = f"{channel_name} {sensor_name}" - - if channel := config.get(CONF_NEUTRAL): - channel_name = channel.get(CONF_NAME) - if conf := channel.get(CONF_CURRENT): - sensor_name = conf.get(CONF_NAME) - if ( - sensor_name - and channel_name - and not sensor_name.startswith(channel_name) - ): - conf[CONF_NAME] = f"{channel_name} {sensor_name}" - - -FINAL_VALIDATE_SCHEMA = final_validate - - async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/tests/components/ade7880/common.yaml b/tests/components/ade7880/common.yaml index 48c22c8485..0aa388a325 100644 --- a/tests/components/ade7880/common.yaml +++ b/tests/components/ade7880/common.yaml @@ -12,12 +12,12 @@ sensor: frequency: 60Hz phase_a: name: Channel A - voltage: Channel A Voltage - current: Channel A Current - active_power: Channel A Active Power - power_factor: Channel A Power Factor - forward_active_energy: Channel A Forward Active Energy - reverse_active_energy: Channel A Reverse Active Energy + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy calibration: current_gain: 3116628 voltage_gain: -757178 @@ -25,12 +25,12 @@ sensor: phase_angle: 188 phase_b: name: Channel B - voltage: Channel B Voltage - current: Channel B Current - active_power: Channel B Active Power - power_factor: Channel B Power Factor - forward_active_energy: Channel B Forward Active Energy - reverse_active_energy: Channel B Reverse Active Energy + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy calibration: current_gain: 3133655 voltage_gain: -755235 @@ -38,12 +38,12 @@ sensor: phase_angle: 188 phase_c: name: Channel C - voltage: Channel C Voltage - current: Channel C Current - active_power: Channel C Active Power - power_factor: Channel C Power Factor - forward_active_energy: Channel C Forward Active Energy - reverse_active_energy: Channel C Reverse Active Energy + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy calibration: current_gain: 3111158 voltage_gain: -743813 @@ -51,6 +51,6 @@ sensor: phase_angle: 180 neutral: name: Neutral - current: Neutral Current + current: Current calibration: current_gain: 3189 From 5a8f722316146d4509a07eddc3b62fb7ee89378b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 16:14:13 -0500 Subject: [PATCH 07/19] Optimize subprocess performance with close_fds=False (#10145) --- docker/build.py | 2 +- esphome/components/sdl/display.py | 4 +++- esphome/dashboard/web_server.py | 1 + esphome/git.py | 4 +++- esphome/helpers.py | 4 +++- esphome/platformio_api.py | 2 +- esphome/util.py | 7 ++++++- script/clang-format | 6 +++++- script/clang-tidy | 12 +++++++++--- script/platformio_install_deps.py | 4 +++- script/run-in-env.py | 4 ++-- tests/integration/conftest.py | 3 +++ 12 files changed, 40 insertions(+), 13 deletions(-) diff --git a/docker/build.py b/docker/build.py index 921adac7ab..4d093cf88d 100755 --- a/docker/build.py +++ b/docker/build.py @@ -90,7 +90,7 @@ def main(): def run_command(*cmd, ignore_error: bool = False): print(f"$ {shlex.join(list(cmd))}") if not args.dry_run: - rc = subprocess.call(list(cmd)) + rc = subprocess.call(list(cmd), close_fds=False) if rc != 0 and not ignore_error: print("Command failed") sys.exit(1) diff --git a/esphome/components/sdl/display.py b/esphome/components/sdl/display.py index ae8b0fd43a..78c180aa65 100644 --- a/esphome/components/sdl/display.py +++ b/esphome/components/sdl/display.py @@ -36,7 +36,9 @@ def get_sdl_options(value): if value != "": return value try: - return subprocess.check_output(["sdl2-config", "--cflags", "--libs"]).decode() + return subprocess.check_output( + ["sdl2-config", "--cflags", "--libs"], close_fds=False + ).decode() except Exception as e: raise cv.Invalid("Unable to run sdl2-config - have you installed sdl2?") from e diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9db389c39a..fd16667d8a 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -229,6 +229,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + close_fds=False, ) stdout_thread = threading.Thread(target=self._stdout_thread) stdout_thread.daemon = True diff --git a/esphome/git.py b/esphome/git.py index 005bcae702..56aedd1519 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -17,7 +17,9 @@ _LOGGER = logging.getLogger(__name__) def run_git_command(cmd, cwd=None) -> str: _LOGGER.debug("Running git command: %s", " ".join(cmd)) try: - ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) + ret = subprocess.run( + cmd, cwd=cwd, capture_output=True, check=False, close_fds=False + ) except FileNotFoundError as err: raise cv.Invalid( "git is not installed but required for external_components.\n" diff --git a/esphome/helpers.py b/esphome/helpers.py index f722dc3f7c..377a4e1717 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -114,7 +114,9 @@ def cpp_string_escape(string, encoding="utf-8"): def run_system_command(*args): import subprocess - with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p: + with subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False + ) as p: stdout, stderr = p.communicate() rc = p.returncode return rc, stdout, stderr diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 21124fc859..267277ebe1 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -211,7 +211,7 @@ def _decode_pc(config, addr): return command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr] try: - translation = subprocess.check_output(command).decode().strip() + translation = subprocess.check_output(command, close_fds=False).decode().strip() except Exception: # pylint: disable=broad-except _LOGGER.debug("Caught exception for command %s", command, exc_info=1) return diff --git a/esphome/util.py b/esphome/util.py index ed9ab4a446..6362260fde 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -239,7 +239,12 @@ def run_external_process(*cmd: str, **kwargs: Any) -> int | str: try: proc = subprocess.run( - cmd, stdout=sub_stdout, stderr=sub_stderr, encoding="utf-8", check=False + cmd, + stdout=sub_stdout, + stderr=sub_stderr, + encoding="utf-8", + check=False, + close_fds=False, ) return proc.stdout if capture_stdout else proc.returncode except KeyboardInterrupt: # pylint: disable=try-except-raise diff --git a/script/clang-format b/script/clang-format index d62a5b59c7..028d752c55 100755 --- a/script/clang-format +++ b/script/clang-format @@ -31,7 +31,11 @@ def run_format(executable, args, queue, lock, failed_files): invocation.append(path) proc = subprocess.run( - invocation, capture_output=True, encoding="utf-8", check=False + invocation, + capture_output=True, + encoding="utf-8", + check=False, + close_fds=False, ) if proc.returncode != 0: with lock: diff --git a/script/clang-tidy b/script/clang-tidy index 2c4a2e36ac..142b616119 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -158,7 +158,11 @@ def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): invocation.extend(options) proc = subprocess.run( - invocation, capture_output=True, encoding="utf-8", check=False + invocation, + capture_output=True, + encoding="utf-8", + check=False, + close_fds=False, ) if proc.returncode != 0: with lock: @@ -320,9 +324,11 @@ def main(): print("Applying fixes ...") try: try: - subprocess.call(["clang-apply-replacements-18", tmpdir]) + subprocess.call( + ["clang-apply-replacements-18", tmpdir], close_fds=False + ) except FileNotFoundError: - subprocess.call(["clang-apply-replacements", tmpdir]) + subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False) except FileNotFoundError: print( "Error please install clang-apply-replacements-18 or clang-apply-replacements.\n", diff --git a/script/platformio_install_deps.py b/script/platformio_install_deps.py index ed133ecb47..8f7261efc3 100755 --- a/script/platformio_install_deps.py +++ b/script/platformio_install_deps.py @@ -55,4 +55,6 @@ for section in config.sections(): tools.append("-t") tools.append(tool) -subprocess.check_call(["platformio", "pkg", "install", "-g", *libs, *platforms, *tools]) +subprocess.check_call( + ["platformio", "pkg", "install", "-g", *libs, *platforms, *tools], close_fds=False +) diff --git a/script/run-in-env.py b/script/run-in-env.py index d9bd01a62f..886e65db27 100755 --- a/script/run-in-env.py +++ b/script/run-in-env.py @@ -13,7 +13,7 @@ def find_and_activate_virtualenv(): try: # Get the top-level directory of the git repository my_path = subprocess.check_output( - ["git", "rev-parse", "--show-toplevel"], text=True + ["git", "rev-parse", "--show-toplevel"], text=True, close_fds=False ).strip() except subprocess.CalledProcessError: print( @@ -44,7 +44,7 @@ def find_and_activate_virtualenv(): def run_command(): # Execute the remaining arguments in the new environment if len(sys.argv) > 1: - subprocess.run(sys.argv[1:], check=False) + subprocess.run(sys.argv[1:], check=False, close_fds=False) else: print( "No command provided to run in the virtual environment.", diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 55bf0b97a7..0530752551 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -105,6 +105,7 @@ logger: check=True, cwd=init_dir, env=env, + close_fds=False, ) # Lock is held until here, ensuring cache is fully populated before any test proceeds @@ -245,6 +246,7 @@ async def compile_esphome( # Start in a new process group to isolate signal handling start_new_session=True, env=env, + close_fds=False, ) await proc.wait() @@ -477,6 +479,7 @@ async def run_binary_and_wait_for_port( # Start in a new process group to isolate signal handling start_new_session=True, pass_fds=(device_fd,), + close_fds=False, ) # Close the device end in the parent process From 2fc0a11596955c07dcb6dba5e8edd8a7ef2b0388 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:53:40 +1200 Subject: [PATCH 08/19] [CI] Print more info for when consts are duplicated (#10166) --- script/ci-custom.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 6f3c513f42..61081608d5 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -500,7 +500,8 @@ def lint_constants_usage(): continue errs.append( f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " - f"constant to const.py (Uses: {', '.join(uses)})" + f"constant to const.py (Uses: {', '.join(uses)}) in a separate PR. " + "See https://developers.esphome.io/contributing/code/#python" ) return errs From da02f970d442ac69fd8650bb2d8bad6a1deeec95 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:24:12 -0400 Subject: [PATCH 09/19] [neopixelbus] Fix neopixelbus on esp32 (#10123) --- esphome/components/neopixelbus/light.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index c63790e60b..0c9604e932 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -225,6 +225,9 @@ async def to_code(config): # https://github.com/Makuna/NeoPixelBus/blob/master/library.json # Version Listed Here: https://registry.platformio.org/libraries/makuna/NeoPixelBus/versions if CORE.is_esp32: + # disable built in rgb support as it uses the new RMT drivers and will + # conflict with NeoPixelBus which uses the legacy drivers + cg.add_build_flag("-DESP32_ARDUINO_NO_RGB_BUILTIN") cg.add_library("makuna/NeoPixelBus", "2.8.0") else: cg.add_library("makuna/NeoPixelBus", "2.7.3") From 581b4ef5a1279fe56f702eaec921fac3a001dcc8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Aug 2025 08:27:54 +1000 Subject: [PATCH 10/19] [lvgl] Various validation fixes (#10141) --- esphome/components/lvgl/lv_validation.py | 8 +++++-- esphome/components/lvgl/types.py | 2 +- esphome/components/lvgl/widgets/__init__.py | 2 +- esphome/components/lvgl/widgets/arc.py | 25 ++++++++++++--------- esphome/components/lvgl/widgets/qrcode.py | 14 ++++++------ esphome/components/lvgl/widgets/spinner.py | 12 +++++----- esphome/components/lvgl/widgets/tabview.py | 6 ++--- tests/components/lvgl/lvgl-package.yaml | 14 ++++++++++++ 8 files changed, 52 insertions(+), 31 deletions(-) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 5a1b99cf7c..d345ac70f3 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -287,10 +287,14 @@ def angle(value): :param value: The input in the range 0..360 :return: An angle in 1/10 degree units. """ - return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10) + return cv.float_range(0.0, 360.0)(cv.angle(value)) -lv_angle = LValidator(angle, uint32) +# Validator for angles in LVGL expressed in 1/10 degree units. +lv_angle = LValidator(angle, uint32, retmapper=lambda x: int(x * 10)) + +# Validator for angles in LVGL expressed in whole degrees +lv_angle_degrees = LValidator(angle, uint32, retmapper=int) @schema_extractor("one_of") diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 10b6f63528..c19c89401a 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -161,7 +161,7 @@ class WidgetType: """ return [] - def obj_creator(self, parent: MockObjClass, config: dict): + async def obj_creator(self, parent: MockObjClass, config: dict): """ Create an instance of the widget type :param parent: The parent to which it should be attached diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index d12464fe71..bb6155234c 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -439,7 +439,7 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): :return: """ spec: WidgetType = WIDGET_TYPES[w_type] - creator = spec.obj_creator(parent, w_cnfig) + creator = await spec.obj_creator(parent, w_cnfig) add_lv_use(spec.name) add_lv_use(*spec.get_uses()) wid = w_cnfig[CONF_ID] diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index 65f0e785b6..ef4da0d815 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -20,7 +20,7 @@ from ..defines import ( CONF_START_ANGLE, literal, ) -from ..lv_validation import angle, get_start_value, lv_float +from ..lv_validation import get_start_value, lv_angle_degrees, lv_float, lv_int from ..lvcode import lv, lv_expr, lv_obj from ..types import LvNumber, NumberType from . import Widget @@ -29,11 +29,11 @@ CONF_ARC = "arc" ARC_SCHEMA = cv.Schema( { cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, - cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, - cv.Optional(CONF_START_ANGLE, default=135): angle, - cv.Optional(CONF_END_ANGLE, default=45): angle, - cv.Optional(CONF_ROTATION, default=0.0): angle, + cv.Optional(CONF_MIN_VALUE, default=0): lv_int, + cv.Optional(CONF_MAX_VALUE, default=100): lv_int, + cv.Optional(CONF_START_ANGLE, default=135): lv_angle_degrees, + cv.Optional(CONF_END_ANGLE, default=45): lv_angle_degrees, + cv.Optional(CONF_ROTATION, default=0.0): lv_angle_degrees, cv.Optional(CONF_ADJUSTABLE, default=False): bool, cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of, cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t, @@ -59,11 +59,14 @@ class ArcType(NumberType): async def to_code(self, w: Widget, config): if CONF_MIN_VALUE in config: - lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) - lv.arc_set_bg_angles( - w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10 - ) - lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10) + max_value = await lv_int.process(config[CONF_MAX_VALUE]) + min_value = await lv_int.process(config[CONF_MIN_VALUE]) + lv.arc_set_range(w.obj, min_value, max_value) + start = await lv_angle_degrees.process(config[CONF_START_ANGLE]) + end = await lv_angle_degrees.process(config[CONF_END_ANGLE]) + rotation = await lv_angle_degrees.process(config[CONF_ROTATION]) + lv.arc_set_bg_angles(w.obj, start, end) + lv.arc_set_rotation(w.obj, rotation) lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 7d8d13d8c4..028a81b449 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -4,7 +4,7 @@ from esphome.const import CONF_SIZE, CONF_TEXT from esphome.cpp_generator import MockObjClass from ..defines import CONF_MAIN -from ..lv_validation import color, color_retmapper, lv_text +from ..lv_validation import lv_color, lv_text from ..lvcode import LocalVariable, lv, lv_expr from ..schemas import TEXT_SCHEMA from ..types import WidgetType, lv_obj_t @@ -16,8 +16,8 @@ CONF_LIGHT_COLOR = "light_color" QRCODE_SCHEMA = TEXT_SCHEMA.extend( { - cv.Optional(CONF_DARK_COLOR, default="black"): color, - cv.Optional(CONF_LIGHT_COLOR, default="white"): color, + cv.Optional(CONF_DARK_COLOR, default="black"): lv_color, + cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, cv.Required(CONF_SIZE): cv.int_, } ) @@ -34,11 +34,11 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return ("canvas", "img", "label") + return "canvas", "img", "label" - def obj_creator(self, parent: MockObjClass, config: dict): - dark_color = color_retmapper(config[CONF_DARK_COLOR]) - light_color = color_retmapper(config[CONF_LIGHT_COLOR]) + async def obj_creator(self, parent: MockObjClass, config: dict): + dark_color = await lv_color.process(config[CONF_DARK_COLOR]) + light_color = await lv_color.process(config[CONF_LIGHT_COLOR]) size = config[CONF_SIZE] return lv_expr.call("qrcode_create", parent, size, dark_color, light_color) diff --git a/esphome/components/lvgl/widgets/spinner.py b/esphome/components/lvgl/widgets/spinner.py index 2940feb594..83aac25a59 100644 --- a/esphome/components/lvgl/widgets/spinner.py +++ b/esphome/components/lvgl/widgets/spinner.py @@ -2,7 +2,7 @@ import esphome.config_validation as cv from esphome.cpp_generator import MockObjClass from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME -from ..lv_validation import angle +from ..lv_validation import lv_angle_degrees, lv_milliseconds from ..lvcode import lv_expr from ..types import LvType from . import Widget, WidgetType @@ -12,8 +12,8 @@ CONF_SPINNER = "spinner" SPINNER_SCHEMA = cv.Schema( { - cv.Required(CONF_ARC_LENGTH): angle, - cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds, + cv.Required(CONF_ARC_LENGTH): lv_angle_degrees, + cv.Required(CONF_SPIN_TIME): lv_milliseconds, } ) @@ -34,9 +34,9 @@ class SpinnerType(WidgetType): def get_uses(self): return (CONF_ARC,) - def obj_creator(self, parent: MockObjClass, config: dict): - spin_time = config[CONF_SPIN_TIME].total_milliseconds - arc_length = config[CONF_ARC_LENGTH] // 10 + async def obj_creator(self, parent: MockObjClass, config: dict): + spin_time = await lv_milliseconds.process(config[CONF_SPIN_TIME]) + arc_length = await lv_angle_degrees.process(config[CONF_ARC_LENGTH]) return lv_expr.call("spinner_create", parent, spin_time, arc_length) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 42cf486e1c..e8931bab7c 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -87,12 +87,12 @@ class TabviewType(WidgetType): ) as content_obj: await set_obj_properties(Widget(content_obj, obj_spec), content_style) - def obj_creator(self, parent: MockObjClass, config: dict): + async def obj_creator(self, parent: MockObjClass, config: dict): return lv_expr.call( "tabview_create", parent, - literal(config[CONF_POSITION]), - literal(config[CONF_SIZE]), + await DIRECTIONS.process(config[CONF_POSITION]), + await size.process(config[CONF_SIZE]), ) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 7cd2e2b93e..feee96672c 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -723,6 +723,20 @@ lvgl: arc_color: 0xFFFF00 focused: arc_color: 0x808080 + - arc: + align: center + id: lv_arc_1 + value: !lambda return 75; + min_value: !lambda return 50; + max_value: !lambda return 60; + arc_color: 0xFF0000 + indicator: + arc_width: !lambda return 20; + arc_color: 0xF000FF + pressed: + arc_color: 0xFFFF00 + focused: + arc_color: 0x808080 - bar: id: bar_id align: top_mid From 8b67d6dfec1b5ea9473ed3a2997f18f8d7db1b84 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Aug 2025 08:32:01 +1000 Subject: [PATCH 11/19] [lvgl] fix allocation of reduced size buffer with rotation (#10147) --- esphome/components/lvgl/lvgl_esphome.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 32930ddec4..7a32691b53 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -451,7 +451,8 @@ void LvglComponent::setup() { if (buffer == nullptr && this->buffer_frac_ == 0) { frac = MIN_BUFFER_FRAC; buffer_pixels /= MIN_BUFFER_FRAC; - buffer = lv_custom_mem_alloc(buf_bytes / MIN_BUFFER_FRAC); // NOLINT + buf_bytes /= MIN_BUFFER_FRAC; + buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT } if (buffer == nullptr) { this->status_set_error("Memory allocation failure"); From e48a223eac9ddcfc3650978b2ea154b162dafcec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 18:03:56 -0500 Subject: [PATCH 12/19] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 60a9ae3cf5..938bf88930 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -145,7 +145,7 @@ void ESPHomeOTAComponent::handle_handshake_() { this->handle_data_(); } else if (read == -1) { if (errno != EAGAIN && errno != EWOULDBLOCK) { - ESP_LOGW(TAG, "Error reading first byte, errno %d", errno); + this->log_socket_error_("reading first byte"); this->cleanup_connection_(); } // For EAGAIN/EWOULDBLOCK, just return and try again next loop From f5790bff7328501c6a58cad6737ca579c76d13ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 18:19:52 -0500 Subject: [PATCH 13/19] adjust --- esphome/components/esphome/ota/ota_esphome.cpp | 10 ++++++---- esphome/components/esphome/ota/ota_esphome.h | 9 +++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 938bf88930..ff429df5a5 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -20,6 +20,8 @@ namespace esphome { static const char *const TAG = "esphome.ota"; static constexpr u_int16_t OTA_BLOCK_SIZE = 8192; +static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake +static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer void ESPHomeOTAComponent::setup() { #ifdef USE_OTA_STATE_CALLBACK @@ -407,12 +409,12 @@ error: #endif } -bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len, uint32_t timeout) { +bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { uint32_t start = millis(); uint32_t at = 0; while (len - at > 0) { uint32_t now = millis(); - if (now - start > timeout) { + if (now - start > OTA_SOCKET_TIMEOUT_DATA) { ESP_LOGW(TAG, "Timeout reading %d bytes", len); return false; } @@ -438,12 +440,12 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len, uint32_t timeout) { return true; } -bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len, uint32_t timeout) { +bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { uint32_t start = millis(); uint32_t at = 0; while (len - at > 0) { uint32_t now = millis(); - if (now - start > timeout) { + if (now - start > OTA_SOCKET_TIMEOUT_DATA) { ESP_LOGW(TAG, "Timeout writing %d bytes", len); return false; } diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index d20d25d8c6..833d4b49d0 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -9,9 +9,6 @@ namespace esphome { -static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake -static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer - /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. class ESPHomeOTAComponent : public ota::OTAComponent { public: @@ -32,8 +29,8 @@ class ESPHomeOTAComponent : public ota::OTAComponent { protected: void handle_handshake_(); void handle_data_(); - bool readall_(uint8_t *buf, size_t len, uint32_t timeout = OTA_SOCKET_TIMEOUT_DATA); - bool writeall_(const uint8_t *buf, size_t len, uint32_t timeout = OTA_SOCKET_TIMEOUT_DATA); + bool readall_(uint8_t *buf, size_t len); + bool writeall_(const uint8_t *buf, size_t len); void log_socket_error_(const char *msg); void log_start_(const char *phase); void cleanup_connection_(); @@ -43,10 +40,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent { #endif // USE_OTA_PASSWORD uint16_t port_; + uint32_t client_connect_time_{0}; std::unique_ptr server_; std::unique_ptr client_; - uint32_t client_connect_time_{0}; }; } // namespace esphome From 4a8369ef939f1dd8e8d57d72880cf2d45e98da9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 18:24:41 -0500 Subject: [PATCH 14/19] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index ff429df5a5..1e61a1df23 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -152,7 +152,7 @@ void ESPHomeOTAComponent::handle_handshake_() { } // For EAGAIN/EWOULDBLOCK, just return and try again next loop } else if (read == 0) { - ESP_LOGW(TAG, "Remote closed connection during handshake"); + ESP_LOGW(TAG, "Remote closed during handshake"); this->cleanup_connection_(); } } From 856e13986ace04e99f8140a56aa282dbc3af4d0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 18:25:51 -0500 Subject: [PATCH 15/19] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 1e61a1df23..cb8cdb4a42 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -85,9 +85,10 @@ void ESPHomeOTAComponent::dump_config() { } void ESPHomeOTAComponent::loop() { - // Skip handle_() call if no client connected and no incoming connections + // Skip handle_handshake_() call if no client connected and no incoming connections // This optimization reduces idle loop overhead when OTA is not active - // Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails + // Note: No need to check server_ for null as the component is marked failed in setup() + // if server_ creation fails if (this->client_ != nullptr || this->server_->ready()) { this->handle_handshake_(); } From 4bdf44bb78405835bf34ba99d34f1368e12e54bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 18:28:27 -0500 Subject: [PATCH 16/19] preen --- .../components/esphome/ota/ota_esphome.cpp | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index cb8cdb4a42..fb862b3925 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -137,25 +137,31 @@ void ESPHomeOTAComponent::handle_handshake_() { // Try to read first byte of magic bytes uint8_t first_byte; ssize_t read = this->client_->read(&first_byte, 1); - if (read == 1) { - // Got the first byte, check if it's the magic byte - if (first_byte != 0x6C) { - ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte); - this->cleanup_connection_(); - return; - } - // First byte is valid, continue with data handling - this->handle_data_(); - } else if (read == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { - this->log_socket_error_("reading first byte"); - this->cleanup_connection_(); - } - // For EAGAIN/EWOULDBLOCK, just return and try again next loop - } else if (read == 0) { - ESP_LOGW(TAG, "Remote closed during handshake"); - this->cleanup_connection_(); + + if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return; // No data yet, try again next loop } + + if (read <= 0) { + // Error or connection closed + if (read == -1) { + this->log_socket_error_("reading first byte"); + } else { + ESP_LOGW(TAG, "Remote closed during handshake"); + } + this->cleanup_connection_(); + return; + } + + // Got first byte, check if it's the magic byte + if (first_byte != 0x6C) { + ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte); + this->cleanup_connection_(); + return; + } + + // First byte is valid, continue with data handling + this->handle_data_(); } void ESPHomeOTAComponent::handle_data_() { From 4faa9231a986558e85460de3c21c29f3769cc6b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 18:29:00 -0500 Subject: [PATCH 17/19] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index fb862b3925..3226567c5a 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -182,7 +182,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read remaining 4 bytes of magic (we already read the first byte 0x6C in handle_handshake_) if (!this->readall_(buf, 4)) { - ESP_LOGW(TAG, "Read magic bytes failed"); + this->log_socket_error_("reading magic bytes"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } // Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45 From ef676a5a77ddd4490e3fac9b6c3108a18ff21baf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 18:30:28 -0500 Subject: [PATCH 18/19] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 3226567c5a..53fbdb2fb4 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -201,7 +201,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read features - 1 byte if (!this->readall_(buf, 1)) { - ESP_LOGW(TAG, "Read features failed"); + this->log_socket_error_("reading features"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_features = buf[0]; // NOLINT From 2f9d1e6dac55147de9fd33817aa6f05438c783ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 18:33:30 -0500 Subject: [PATCH 19/19] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 12 +++++++----- esphome/components/esphome/ota/ota_esphome.h | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 53fbdb2fb4..c1d50790a2 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -182,7 +182,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read remaining 4 bytes of magic (we already read the first byte 0x6C in handle_handshake_) if (!this->readall_(buf, 4)) { - this->log_socket_error_("reading magic bytes"); + this->log_read_error_("magic bytes"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } // Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45 @@ -201,7 +201,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read features - 1 byte if (!this->readall_(buf, 1)) { - this->log_socket_error_("reading features"); + this->log_read_error_("features"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_features = buf[0]; // NOLINT @@ -280,7 +280,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read size, 4 bytes MSB first if (!this->readall_(buf, 4)) { - ESP_LOGW(TAG, "Read size failed"); + this->log_read_error_("size"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_size = 0; @@ -312,7 +312,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read binary MD5, 32 bytes if (!this->readall_(buf, 32)) { - ESP_LOGW(TAG, "Read MD5 checksum failed"); + this->log_read_error_("MD5 checksum"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } sbuf[32] = '\0'; @@ -387,7 +387,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read ACK if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Read ack failed"); + this->log_read_error_("ack"); // do not go to error, this is not fatal } @@ -481,6 +481,8 @@ void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } void ESPHomeOTAComponent::log_socket_error_(const char *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", msg, errno); } +void ESPHomeOTAComponent::log_read_error_(const char *what) { ESP_LOGW(TAG, "Read %s failed", what); } + void ESPHomeOTAComponent::log_start_(const char *phase) { ESP_LOGD(TAG, "Starting %s from %s", phase, this->client_->getpeername().c_str()); } diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 833d4b49d0..0059dfd0d9 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -32,6 +32,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); void log_socket_error_(const char *msg); + void log_read_error_(const char *what); void log_start_(const char *phase); void cleanup_connection_();