From a4026d6ba18043ddd977d12343bf4d8bdf08220e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Jul 2025 08:34:43 -1000 Subject: [PATCH 1/5] [ruff] Enable RET and fix all violations --- esphome/automation.py | 6 ++-- .../alarm_control_panel/__init__.py | 12 +++---- esphome/components/ble_client/__init__.py | 10 ++---- esphome/components/esp32/__init__.py | 6 ++-- esphome/components/esp32/gpio.py | 3 +- .../components/esp32_ble_server/__init__.py | 3 +- esphome/components/haier/climate.py | 15 +++----- esphome/components/light/effects.py | 3 +- esphome/components/lvgl/automation.py | 6 ++-- esphome/components/lvgl/lv_validation.py | 3 +- esphome/components/lvgl/styles.py | 3 +- esphome/components/lvgl/widgets/__init__.py | 2 +- .../components/lvgl/widgets/buttonmatrix.py | 2 +- esphome/components/mqtt/__init__.py | 3 +- esphome/components/one_wire/__init__.py | 3 +- esphome/components/packages/__init__.py | 3 +- esphome/components/pmwcs3/sensor.py | 3 +- esphome/components/rf_bridge/__init__.py | 12 +++---- .../components/rp2040_pio_led_strip/light.py | 3 +- esphome/components/sim800l/__init__.py | 6 ++-- esphome/components/ufire_ec/sensor.py | 3 +- esphome/components/ufire_ise/sensor.py | 3 +- esphome/config_helpers.py | 3 +- esphome/config_validation.py | 2 +- esphome/cpp_helpers.py | 2 +- esphome/mqtt.py | 3 +- esphome/vscode.py | 3 +- esphome/yaml_util.py | 3 +- pyproject.toml | 1 + script/api_protobuf/api_protobuf.py | 35 ++++++++----------- script/build_language_schema.py | 3 +- script/helpers_zephyr.py | 7 ++-- tests/dashboard/test_web_server.py | 3 +- tests/integration/conftest.py | 13 ++++--- .../loop_test_component/__init__.py | 6 ++-- tests/script/test_clang_tidy_hash.py | 2 +- tests/script/test_helpers.py | 5 ++- tests/unit_tests/test_substitutions.py | 5 ++- tests/unit_tests/test_vscode.py | 3 +- 39 files changed, 79 insertions(+), 133 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 34159561c2..99d4362845 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -391,8 +391,7 @@ async def build_action(full_config, template_arg, args): ) action_id = full_config[CONF_TYPE_ID] builder = registry_entry.coroutine_fun - ret = await builder(config, action_id, template_arg, args) - return ret + return await builder(config, action_id, template_arg, args) async def build_action_list(config, templ, arg_type): @@ -409,8 +408,7 @@ async def build_condition(full_config, template_arg, args): ) action_id = full_config[CONF_TYPE_ID] builder = registry_entry.coroutine_fun - ret = await builder(config, action_id, template_arg, args) - return ret + return await builder(config, action_id, template_arg, args) async def build_condition_list(config, templ, args): diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 6d37d53a4c..b076175eb8 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -301,8 +301,7 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args): ) async def alarm_action_pending_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -310,8 +309,7 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args): ) async def alarm_action_trigger_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -319,8 +317,7 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args): ) async def alarm_action_chime_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -333,8 +330,7 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args): ) async def alarm_action_ready_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_condition( diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index a88172ca87..0f3869c23b 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -175,8 +175,7 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( ) async def ble_disconnect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) @automation.register_action( @@ -184,8 +183,7 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args): ) async def ble_connect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) @automation.register_action( @@ -282,9 +280,7 @@ async def passkey_reply_to_code(config, action_id, template_arg, args): ) async def remove_bond_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - - return var + return cg.new_Pvariable(action_id, template_arg, parent) async def to_code(config): diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 2b4c4ff043..2dd5b8ec78 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -872,7 +872,7 @@ def get_arduino_partition_csv(flash_size): eeprom_partition_start = app1_partition_start + app_partition_size spiffs_partition_start = eeprom_partition_start + eeprom_partition_size - partition_csv = f"""\ + return f"""\ nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xE000, 0x2000, app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, @@ -880,20 +880,18 @@ app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X}, eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} """ - return partition_csv def get_idf_partition_csv(flash_size): app_partition_size = APP_PARTITION_SIZES[flash_size] - partition_csv = f"""\ + return f"""\ otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, app0, app, ota_0, , 0x{app_partition_size:X}, app1, app, ota_1, , 0x{app_partition_size:X}, nvs, data, nvs, , 0x6D000, """ - return partition_csv def _format_sdkconfig_val(value: SdkconfigValueType) -> str: diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index c35e5c2215..513f463d57 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -187,8 +187,7 @@ def validate_supports(value): "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] ) - value = _esp32_validations[variant].usage_validation(value) - return value + return _esp32_validations[variant].usage_validation(value) # https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 19f466eb7b..6f16d76a32 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -628,5 +628,4 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args) ) async def ble_server_characteristic_notify(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 0393c263d4..8c3649058f 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -330,8 +330,7 @@ HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( ) async def display_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -342,8 +341,7 @@ async def display_action_to_code(config, action_id, template_arg, args): ) async def beeper_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) # Start self cleaning or steri-cleaning action action @@ -359,8 +357,7 @@ async def beeper_action_to_code(config, action_id, template_arg, args): ) async def start_cleaning_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) # Set vertical airflow direction action @@ -417,8 +414,7 @@ async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, ) async def health_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -432,8 +428,7 @@ async def health_action_to_code(config, action_id, template_arg, args): ) async def power_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) def _final_validate(config): diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index f5749a17ab..6c8fd86225 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -353,10 +353,9 @@ async def addressable_lambda_effect_to_code(config, effect_id): (bool, "initial_run"), ] lambda_ = await cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) - var = cg.new_Pvariable( + return cg.new_Pvariable( effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL] ) - return var @register_addressable_effect( diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index cc0f833ced..fc70b0f682 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -85,8 +85,7 @@ async def action_to_code( async with LambdaContext(parameters=args, where=action_id) as context: for widget in widgets: await action(widget) - var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - return var + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) async def update_to_code(config, action_id, template_arg, args): @@ -354,8 +353,7 @@ async def widget_focus(config, action_id, template_arg, args): if config[CONF_FREEZE]: lv.group_focus_freeze(group, True) - var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - return var + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) @automation.register_action( diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 92fe74eb52..5a1b99cf7c 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -271,8 +271,7 @@ padding = LValidator(padding_validator, int32, retmapper=literal) def zoom_validator(value): - value = cv.float_range(0.1, 10.0)(value) - return value + return cv.float_range(0.1, 10.0)(value) def zoom_retmapper(value): diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 11d7bca5fa..3969c9f388 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -66,8 +66,7 @@ async def style_update_to_code(config, action_id, template_arg, args): async with LambdaContext(parameters=args, where=action_id) as context: await style_set(style, config) - var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - return var + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) async def theme_to_code(config): diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index a8cb8dce33..d12464fe71 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -189,7 +189,7 @@ class Widget: for matrix buttons :return: """ - return None + return def get_max(self): return self.type.get_max(self.config) diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index aa33be722c..c6b6d2440f 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -193,7 +193,7 @@ class ButtonMatrixType(WidgetType): async def to_code(self, w: Widget, config): lvgl_components_required.add("BUTTONMATRIX") if CONF_ROWS not in config: - return [] + return text_list, ctrl_list, width_list, key_list = await get_button_data( config[CONF_ROWS], w ) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 1a6fcabf42..52d3181780 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -312,14 +312,13 @@ CONFIG_SCHEMA = cv.All( def exp_mqtt_message(config): if config is None: return cg.optional(cg.TemplateArguments(MQTTMessage)) - exp = cg.StructInitializer( + return cg.StructInitializer( MQTTMessage, ("topic", config[CONF_TOPIC]), ("payload", config.get(CONF_PAYLOAD, "")), ("qos", config[CONF_QOS]), ("retain", config[CONF_RETAIN]), ) - return exp @coroutine_with_priority(40.0) diff --git a/esphome/components/one_wire/__init__.py b/esphome/components/one_wire/__init__.py index 99a1ccd1eb..6d95b8fd33 100644 --- a/esphome/components/one_wire/__init__.py +++ b/esphome/components/one_wire/__init__.py @@ -18,13 +18,12 @@ def one_wire_device_schema(): :return: The 1-wire device schema, `extend` this in your config schema. """ - schema = cv.Schema( + return cv.Schema( { cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus), cv.Optional(CONF_ADDRESS): cv.hex_uint64_t, } ) - return schema async def register_one_wire_device(var, config): diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 0db7841db2..2e7dc0e197 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -186,8 +186,7 @@ def _process_package(package_config, config): package_config = _process_base_package(package_config) if isinstance(package_config, dict): recursive_package = do_packages_pass(package_config) - config = merge_config(recursive_package, config) - return config + return merge_config(recursive_package, config) def do_packages_pass(config: dict): diff --git a/esphome/components/pmwcs3/sensor.py b/esphome/components/pmwcs3/sensor.py index d42338ab6f..075b9b00b5 100644 --- a/esphome/components/pmwcs3/sensor.py +++ b/esphome/components/pmwcs3/sensor.py @@ -114,8 +114,7 @@ PMWCS3_CALIBRATION_SCHEMA = cv.Schema( ) async def pmwcs3_calibration_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 5ccca823de..b4770726b4 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -136,8 +136,7 @@ RFBRIDGE_ID_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(RFBridgeComponent)}) @automation.register_action("rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA) async def rf_bridge_learnx_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_args, paren) - return var + return cg.new_Pvariable(action_id, template_args, paren) @automation.register_action( @@ -149,8 +148,7 @@ async def rf_bridge_start_advanced_sniffing_to_code( config, action_id, template_args, args ): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_args, paren) - return var + return cg.new_Pvariable(action_id, template_args, paren) @automation.register_action( @@ -162,8 +160,7 @@ async def rf_bridge_stop_advanced_sniffing_to_code( config, action_id, template_args, args ): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_args, paren) - return var + return cg.new_Pvariable(action_id, template_args, paren) @automation.register_action( @@ -175,8 +172,7 @@ async def rf_bridge_start_bucket_sniffing_to_code( config, action_id, template_args, args ): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_args, paren) - return var + return cg.new_Pvariable(action_id, template_args, paren) RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema( diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py index 9107db9b7f..62f7fffdc9 100644 --- a/esphome/components/rp2040_pio_led_strip/light.py +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -125,8 +125,7 @@ writezero: def time_to_cycles(time_us): cycles_per_us = 57.5 - cycles = round(float(time_us) * cycles_per_us) - return cycles + return round(float(time_us) * cycles_per_us) CONF_PIO = "pio" diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index 2ca9127d3f..c48a3c63c4 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -171,8 +171,7 @@ async def sim800l_dial_to_code(config, action_id, template_arg, args): ) async def sim800l_connect_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) SIM800L_SEND_USSD_SCHEMA = cv.Schema( @@ -201,5 +200,4 @@ async def sim800l_send_ussd_to_code(config, action_id, template_arg, args): ) async def sim800l_disconnect_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py index 944fdfdee9..9edf0f89ff 100644 --- a/esphome/components/ufire_ec/sensor.py +++ b/esphome/components/ufire_ec/sensor.py @@ -122,5 +122,4 @@ UFIRE_EC_RESET_SCHEMA = cv.Schema( ) async def ufire_ec_reset_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/ufire_ise/sensor.py b/esphome/components/ufire_ise/sensor.py index e57a1155a4..8009cdaa6a 100644 --- a/esphome/components/ufire_ise/sensor.py +++ b/esphome/components/ufire_ise/sensor.py @@ -123,5 +123,4 @@ UFIRE_ISE_RESET_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(UFireISEComponent ) async def ufire_ise_reset_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 50ce4e8e34..00cd8f9818 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -111,8 +111,7 @@ def merge_config(full_old, full_new): else: ids[new_id] = len(res) res.append(v) - res = [v for i, v in enumerate(res) if i not in ids_to_delete] - return res + return [v for i, v in enumerate(res) if i not in ids_to_delete] if new is None: return old diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1a4976e235..11a67b9478 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1866,7 +1866,7 @@ def validate_registry_entry(name, registry): def none(value): if value in ("none", "None"): - return None + return raise Invalid("Must be none") diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 3f64be6154..b61b215bdc 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -115,7 +115,7 @@ async def build_registry_list(registry, config): async def past_safe_mode(): if CONF_SAFE_MODE not in CORE.config: - return + return None def _safe_mode_generator(): while True: diff --git a/esphome/mqtt.py b/esphome/mqtt.py index acfa8a0926..f1c631697a 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -36,7 +36,7 @@ _LOGGER = logging.getLogger(__name__) def config_from_env(): - config = { + return { CONF_MQTT: { CONF_USERNAME: get_str_env("ESPHOME_DASHBOARD_MQTT_USERNAME"), CONF_PASSWORD: get_str_env("ESPHOME_DASHBOARD_MQTT_PASSWORD"), @@ -44,7 +44,6 @@ def config_from_env(): CONF_PORT: get_int_env("ESPHOME_DASHBOARD_MQTT_PORT", 1883), }, } - return config def initialize( diff --git a/esphome/vscode.py b/esphome/vscode.py index d8cfe91938..f5e2a20b97 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -81,8 +81,7 @@ def _print_file_read_event(path: str) -> None: def _request_and_get_stream_on_stdin(fname: str) -> StringIO: _print_file_read_event(fname) - raw_yaml_stream = StringIO(_read_file_content_from_json_on_stdin()) - return raw_yaml_stream + return StringIO(_read_file_content_from_json_on_stdin()) def _vscode_loader(fname: str) -> dict[str, Any]: diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 33a56fc158..f26bc0502d 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -305,8 +305,7 @@ class ESPHomeLoaderMixin: result = self.yaml_loader(self._rel_path(file)) if not vars: vars = {} - result = substitute_vars(result, vars) - return result + return substitute_vars(result, vars) @_add_data_ref def construct_include_dir_list(self, node: yaml.Node) -> list[dict[str, Any]]: diff --git a/pyproject.toml b/pyproject.toml index 200f51a873..4943c48eb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,6 +118,7 @@ select = [ "PERF", # performance "PL", # pylint "SIM", # flake8-simplify + "RET", # flake8-ret "UP", # pyupgrade ] diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 92c85d2366..22adc76288 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -539,8 +539,7 @@ class BoolType(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f"out.append(YESNO({name}));" - return o + return f"out.append(YESNO({name}));" def get_size_calculation(self, name: str, force: bool = False) -> str: return self._get_simple_size_calculation(name, force, "add_bool_field") @@ -680,8 +679,7 @@ class MessageType(TypeInfo): return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;" def dump(self, name: str) -> str: - o = f"{name}.dump_to(out);" - return o + return f"{name}.dump_to(out);" @property def dump_content(self) -> str: @@ -829,8 +827,7 @@ class FixedArrayBytesType(TypeInfo): return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" def dump(self, name: str) -> str: - o = f"out.append(format_hex_pretty({name}, {name}_len));" - return o + return f"out.append(format_hex_pretty({name}, {name}_len));" @property def dump_content(self) -> str: @@ -847,13 +844,12 @@ class FixedArrayBytesType(TypeInfo): if force: # For repeated fields, always calculate size return f"total_size += {field_id_size} + ProtoSize::varint(static_cast({length_field})) + {length_field};" - else: - # For non-repeated fields, skip if length is 0 (matching encode_string behavior) - return ( - f"if ({length_field} != 0) {{\n" - f" total_size += {field_id_size} + ProtoSize::varint(static_cast({length_field})) + {length_field};\n" - f"}}" - ) + # For non-repeated fields, skip if length is 0 (matching encode_string behavior) + return ( + f"if ({length_field} != 0) {{\n" + f" total_size += {field_id_size} + ProtoSize::varint(static_cast({length_field})) + {length_field};\n" + f"}}" + ) def get_estimated_size(self) -> int: # Estimate based on typical BLE advertisement size @@ -908,8 +904,7 @@ class EnumType(TypeInfo): return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}));" def dump(self, name: str) -> str: - o = f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" - return o + return f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" def dump_field_value(self, value: str) -> str: # Enums need explicit cast for the template @@ -1078,13 +1073,12 @@ class FixedArrayRepeatedType(TypeInfo): def encode_element(element: str) -> str: if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" - else: - return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" # Unroll small arrays for efficiency if self.array_size == 1: return encode_element(f"this->{self.field_name}[0]") - elif self.array_size == 2: + if self.array_size == 2: return ( encode_element(f"this->{self.field_name}[0]") + "\n " @@ -1240,8 +1234,9 @@ class RepeatedTypeInfo(TypeInfo): if isinstance(self._ti, MessageType): # For repeated messages, use the dedicated helper that handles iteration internally field_id_size = self._ti.calculate_field_id_size() - o = f"ProtoSize::add_repeated_message(total_size, {field_id_size}, {name});" - return o + return ( + f"ProtoSize::add_repeated_message(total_size, {field_id_size}, {name});" + ) # For other repeated types, use the underlying type's size calculation with force=True o = f"if (!{name}.empty()) {{\n" diff --git a/script/build_language_schema.py b/script/build_language_schema.py index c114d15315..ff6e898902 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -444,8 +444,7 @@ def get_str_path_schema(strPath): if len(parts) > 2: parts[0] += "." + parts[1] parts[1] = parts[2] - s1 = output.get(parts[0], {}).get(S_SCHEMAS, {}).get(parts[1], {}) - return s1 + return output.get(parts[0], {}).get(S_SCHEMAS, {}).get(parts[1], {}) def pop_str_path_schema(strPath): diff --git a/script/helpers_zephyr.py b/script/helpers_zephyr.py index 305ca00c0c..09a0850cbf 100644 --- a/script/helpers_zephyr.py +++ b/script/helpers_zephyr.py @@ -42,12 +42,11 @@ CONFIG_NEWLIB_LIBC=y def extract_defines(command): define_pattern = re.compile(r"-D\s*([^\s]+)") - defines = [ + return [ match for match in define_pattern.findall(command) if match not in ("_ASMLANGUAGE") ] - return defines def find_cxx_path(commands): for entry in commands: @@ -56,6 +55,7 @@ CONFIG_NEWLIB_LIBC=y if not cxx_path.endswith("++"): continue return cxx_path + return None def get_builtin_include_paths(compiler): result = subprocess.run( @@ -83,11 +83,10 @@ CONFIG_NEWLIB_LIBC=y flag_pattern = re.compile( r"(-O[0-3s]|-g|-std=[^\s]+|-Wall|-Wextra|-Werror|--[^\s]+|-f[^\s]+|-m[^\s]+|-imacros\s*[^\s]+)" ) - flags = [ + return [ match.replace("-imacros ", "-imacros") for match in flag_pattern.findall(command) ] - return flags def transform_to_idedata_format(compile_commands): cxx_path = find_cxx_path(compile_commands) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index cd02200d0b..b77ab7a7a3 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -31,8 +31,7 @@ class DashboardTestHelper: else: url = f"http://127.0.0.1:{self.port}{path}" future = self.client.fetch(url, raise_error=True, **kwargs) - result = await future - return result + return await future @pytest_asyncio.fixture() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 46eb6c88e2..55bf0b97a7 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -251,19 +251,18 @@ async def compile_esphome( if proc.returncode == 0: # Success! break - elif proc.returncode == -11 and attempt < max_retries - 1: + if proc.returncode == -11 and attempt < max_retries - 1: # Segfault (-11 = SIGSEGV), retry print( f"Compilation segfaulted (attempt {attempt + 1}/{max_retries}), retrying..." ) await asyncio.sleep(1) # Brief pause before retry continue - else: - # Other error or final retry - raise RuntimeError( - f"Failed to compile {config_path}, return code: {proc.returncode}. " - f"Run with 'pytest -s' to see compilation output." - ) + # Other error or final retry + raise RuntimeError( + f"Failed to compile {config_path}, return code: {proc.returncode}. " + f"Run with 'pytest -s' to see compilation output." + ) # Load the config to get idedata (blocking call, must use executor) loop = asyncio.get_running_loop() 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 3f3a40db09..a0b0f8c65a 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/__init__.py +++ b/tests/integration/fixtures/external_components/loop_test_component/__init__.py @@ -72,8 +72,7 @@ DisableAction = loop_test_component_ns.class_("DisableAction", automation.Action ) async def enable_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) @automation.register_action( @@ -87,8 +86,7 @@ async def enable_to_code(config, action_id, template_arg, args): ) async def disable_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) async def to_code(config): diff --git a/tests/script/test_clang_tidy_hash.py b/tests/script/test_clang_tidy_hash.py index 7b66a69adb..2f84d11a0d 100644 --- a/tests/script/test_clang_tidy_hash.py +++ b/tests/script/test_clang_tidy_hash.py @@ -69,7 +69,7 @@ def test_calculate_clang_tidy_hash() -> None: def read_file_mock(path: Path) -> bytes: if ".clang-tidy" in str(path): return clang_tidy_content - elif "platformio.ini" in str(path): + if "platformio.ini" in str(path): return platformio_content return b"" diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 423e2d3c30..9730efd366 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -315,9 +315,8 @@ def test_local_development_no_remotes_configured(monkeypatch: MonkeyPatch) -> No def side_effect_func(*args): if args == ("git", "remote"): return "origin\nupstream\n" - else: - # All merge-base attempts fail - raise Exception("Command failed") + # All merge-base attempts fail + raise Exception("Command failed") mock_output.side_effect = side_effect_func diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index b65fecb26e..b2b7cb1ea4 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -18,11 +18,10 @@ def sort_dicts(obj): """Recursively sort dictionaries for order-insensitive comparison.""" if isinstance(obj, dict): return {k: sort_dicts(obj[k]) for k in sorted(obj)} - elif isinstance(obj, list): + if isinstance(obj, list): # Lists are not sorted; we preserve order return [sort_dicts(i) for i in obj] - else: - return obj + return obj def dict_diff(a, b, path=""): diff --git a/tests/unit_tests/test_vscode.py b/tests/unit_tests/test_vscode.py index 6e0bde23b2..4b28a2215b 100644 --- a/tests/unit_tests/test_vscode.py +++ b/tests/unit_tests/test_vscode.py @@ -22,8 +22,7 @@ def _run_repl_test(input_data): call[0][0] for call in mock_stdout.write.call_args_list ).strip() splitted_output = full_output.split("\n") - remove_version = splitted_output[1:] # remove first entry with version info - return remove_version + return splitted_output[1:] # remove first entry with version info def _validate(file_path: str): From 14dd48f9c36c22c19fb28038e58ef11cb418b0c4 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Tue, 29 Jul 2025 17:41:31 -0700 Subject: [PATCH 2/5] [wifi] add more disconnect reason descriptions (#9955) Co-authored-by: Samuel Sieb --- .../components/wifi/wifi_component_esp32_arduino.cpp | 12 ++++++++++++ esphome/components/wifi/wifi_component_esp_idf.cpp | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index b3167c5696..3c3e87d332 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -474,8 +474,20 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Handshake Failed"; case WIFI_REASON_CONNECTION_FAIL: return "Connection Failed"; + case WIFI_REASON_AP_TSF_RESET: + return "AP TSF reset"; case WIFI_REASON_ROAMING: return "Station Roaming"; + case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG: + return "Association comeback time too long"; + case WIFI_REASON_SA_QUERY_TIMEOUT: + return "SA query timeout"; + case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY: + return "No AP found with compatible security"; + case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD: + return "No AP found in auth mode threshold"; + case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD: + return "No AP found in RSSI threshold"; case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index f0655a6d1d..0b281e9b80 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -640,8 +640,20 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Handshake Failed"; case WIFI_REASON_CONNECTION_FAIL: return "Connection Failed"; + case WIFI_REASON_AP_TSF_RESET: + return "AP TSF reset"; case WIFI_REASON_ROAMING: return "Station Roaming"; + case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG: + return "Association comeback time too long"; + case WIFI_REASON_SA_QUERY_TIMEOUT: + return "SA query timeout"; + case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY: + return "No AP found with compatible security"; + case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD: + return "No AP found in auth mode threshold"; + case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD: + return "No AP found in RSSI threshold"; case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; From 374858efeb50885de01f0e2075264354cab9f3ce Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 29 Jul 2025 19:53:14 -0500 Subject: [PATCH 3/5] [sensor] Add new filter: ``throttle_with_priority`` (#9937) --- esphome/components/sensor/__init__.py | 20 +++++++++++++++ esphome/components/sensor/filter.cpp | 35 +++++++++++++++++++++++++++ esphome/components/sensor/filter.h | 14 +++++++++++ 3 files changed, 69 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 20c6911d28..5d70785389 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -256,6 +256,7 @@ OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter) ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) +ThrottleWithPriorityFilter = sensor_ns.class_("ThrottleWithPriorityFilter", Filter) TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) @@ -595,6 +596,25 @@ async def throttle_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config) +TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( + { + cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, + cv.Optional(CONF_VALUE, default="nan"): cv.ensure_list(cv.float_), + }, + key=CONF_TIMEOUT, +) + + +@FILTER_REGISTRY.register( + "throttle_with_priority", + ThrottleWithPriorityFilter, + TIMEOUT_WITH_PRIORITY_SCHEMA, +) +async def throttle_with_priority_filter_to_code(config, filter_id): + template_ = [await cg.templatable(x, [], float) for x in config[CONF_VALUE]] + return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) + + @FILTER_REGISTRY.register( "heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds ) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 2fd56b7c8f..39b507f960 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,5 +1,6 @@ #include "filter.h" #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "sensor.h" @@ -332,6 +333,40 @@ optional ThrottleFilter::new_value(float value) { return {}; } +// ThrottleWithPriorityFilter +ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, + std::vector> prioritized_values) + : min_time_between_inputs_(min_time_between_inputs), prioritized_values_(std::move(prioritized_values)) {} + +optional ThrottleWithPriorityFilter::new_value(float value) { + bool is_prioritized_value = false; + int8_t accuracy = this->parent_->get_accuracy_decimals(); + float accuracy_mult = powf(10.0f, accuracy); + const uint32_t now = App.get_loop_component_start_time(); + // First, determine if the new value is one of the prioritized values + for (auto prioritized_value : this->prioritized_values_) { + if (std::isnan(prioritized_value.value())) { + if (std::isnan(value)) { + is_prioritized_value = true; + break; + } + continue; + } + float rounded_prioritized_value = roundf(accuracy_mult * prioritized_value.value()); + float rounded_value = roundf(accuracy_mult * value); + if (rounded_prioritized_value == rounded_value) { + is_prioritized_value = true; + break; + } + } + // Finally, determine if the new value should be throttled and pass it through if not + if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || is_prioritized_value) { + this->last_input_ = now; + return value; + } + return {}; +} + // DeltaFilter DeltaFilter::DeltaFilter(float delta, bool percentage_mode) : delta_(delta), current_delta_(delta), percentage_mode_(percentage_mode), last_value_(NAN) {} diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 94fec8208b..8e2c6fef08 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -314,6 +314,20 @@ class ThrottleFilter : public Filter { uint32_t min_time_between_inputs_; }; +/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`. +class ThrottleWithPriorityFilter : public Filter { + public: + explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, + std::vector> prioritized_values); + + optional new_value(float value) override; + + protected: + uint32_t last_input_{0}; + uint32_t min_time_between_inputs_; + std::vector> prioritized_values_; +}; + class TimeoutFilter : public Filter, public Component { public: explicit TimeoutFilter(uint32_t time_period, TemplatableValue new_value); From 913c58cd2c514c90b59c34e74cf80d3168b82395 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 29 Jul 2025 21:20:25 -0500 Subject: [PATCH 4/5] [template] Add tests for more sensor filters (#9973) --- tests/components/template/common.yaml | 159 +++++++++++++++++++------- 1 file changed, 118 insertions(+), 41 deletions(-) diff --git a/tests/components/template/common.yaml b/tests/components/template/common.yaml index fd9342b3e5..e185e01c5e 100644 --- a/tests/components/template/common.yaml +++ b/tests/components/template/common.yaml @@ -1,44 +1,3 @@ -sensor: - - platform: template - name: "Template Sensor" - id: template_sens - lambda: |- - if (id(some_binary_sensor).state) { - return 42.0; - } else { - return 0.0; - } - update_interval: 60s - filters: - - offset: 10 - - multiply: 1 - - offset: !lambda return 10; - - multiply: !lambda return 2; - - filter_out: - - 10 - - 20 - - !lambda return 10; - - filter_out: 10 - - filter_out: !lambda return NAN; - - timeout: - timeout: 10s - value: !lambda return 10; - - timeout: - timeout: 1h - value: 20.0 - - timeout: - timeout: 1d - - to_ntc_resistance: - calibration: - - 10.0kOhm -> 25°C - - 27.219kOhm -> 0°C - - 14.674kOhm -> 15°C - - to_ntc_temperature: - calibration: - - 10.0kOhm -> 25°C - - 27.219kOhm -> 0°C - - 14.674kOhm -> 15°C - esphome: on_boot: - sensor.template.publish: @@ -82,6 +41,123 @@ binary_sensor: sensor.in_range: id: template_sens below: 30.0 + filters: + - invert: + - delayed_on: 100ms + - delayed_off: 100ms + - delayed_on_off: !lambda "if (id(test_switch).state) return 1000; else return 0;" + - delayed_on_off: + time_on: 10s + time_off: !lambda "if (id(test_switch).state) return 1000; else return 0;" + - autorepeat: + - delay: 1s + time_off: 100ms + time_on: 900ms + - delay: 5s + time_off: 100ms + time_on: 400ms + - lambda: |- + if (id(other_binary_sensor).state) { + return x; + } else { + return {}; + } + - settle: 500ms + - timeout: 5s + +sensor: + - platform: template + name: "Template Sensor" + id: template_sens + lambda: |- + if (id(some_binary_sensor).state) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + filters: + - calibrate_linear: + - 0.0 -> 0.0 + - 40.0 -> 45.0 + - 100.0 -> 102.5 + - calibrate_polynomial: + degree: 2 + datapoints: + # Map 0.0 (from sensor) to 0.0 (true value) + - 0.0 -> 0.0 + - 10.0 -> 12.1 + - 13.0 -> 14.0 + - clamp: + max_value: 10.0 + min_value: -10.0 + - debounce: 0.1s + - delta: 5.0 + - exponential_moving_average: + alpha: 0.1 + send_every: 15 + - filter_out: + - 10 + - 20 + - !lambda return 10; + - filter_out: 10 + - filter_out: !lambda return NAN; + - heartbeat: 5s + - lambda: return x * (9.0/5.0) + 32.0; + - max: + window_size: 10 + send_every: 2 + send_first_at: 1 + - median: + window_size: 7 + send_every: 4 + send_first_at: 3 + - min: + window_size: 10 + send_every: 2 + send_first_at: 1 + - multiply: 1 + - multiply: !lambda return 2; + - offset: 10 + - offset: !lambda return 10; + - or: + - quantile: + window_size: 7 + send_every: 4 + send_first_at: 3 + quantile: .9 + - round: 1 + - round_to_multiple_of: 0.25 + - skip_initial: 3 + - sliding_window_moving_average: + window_size: 15 + send_every: 15 + - throttle: 1s + - throttle_average: 2s + - throttle_with_priority: 5s + - throttle_with_priority: + timeout: 3s + value: + - 42.0 + - nan + - timeout: + timeout: 10s + value: !lambda return 10; + - timeout: + timeout: 1h + value: 20.0 + - timeout: + timeout: 1d + - to_ntc_resistance: + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C + - to_ntc_temperature: + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C output: - platform: template @@ -92,6 +168,7 @@ output: switch: - platform: template + id: test_switch name: "Template Switch" lambda: |- if (id(some_binary_sensor).state) { From 9b4fe54f45a6b8ceacb35f4a6ed9ca35d077554c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Jul 2025 19:19:12 -1000 Subject: [PATCH 5/5] [esp32_ble_client] Fix connection failures with short discovery timeout devices and speed up BLE connections (#9971) --- .../esp32_ble_client/ble_client_base.cpp | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index bf425b3730..d3416641d9 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -5,10 +5,23 @@ #ifdef USE_ESP32 +#include +#include + namespace esphome { namespace esp32_ble_client { static const char *const TAG = "esp32_ble_client"; + +// Connection interval defaults matching ESP-IDF's BTM_BLE_CONN_INT_*_DEF +static const uint16_t DEFAULT_MIN_CONN_INTERVAL = 0x0A; // 10 * 1.25ms = 12.5ms +static const uint16_t DEFAULT_MAX_CONN_INTERVAL = 0x0C; // 12 * 1.25ms = 15ms +static const uint16_t DEFAULT_CONN_TIMEOUT = 600; // 600 * 10ms = 6s + +// Fastest connection parameters for devices with short discovery timeouts +static const uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) +static const uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms +static const uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = @@ -129,6 +142,7 @@ void BLEClientBase::connect() { ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), this->remote_addr_type_); this->paired_ = false; + auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); if (ret) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(), @@ -136,6 +150,22 @@ void BLEClientBase::connect() { this->set_state(espbt::ClientState::IDLE); } else { this->set_state(espbt::ClientState::CONNECTING); + + // For connections without cache, set fast connection parameters after initiating connection + // This ensures service discovery completes within the 10-second timeout that + // some devices like HomeKit BLE sensors enforce + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + auto param_ret = + esp_ble_gap_set_prefer_conn_params(this->remote_bda_, FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, + 0, // latency: 0 + FAST_CONN_TIMEOUT); + if (param_ret != ESP_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_, + this->address_str_.c_str(), param_ret); + } else { + ESP_LOGD(TAG, "[%d] [%s] Set fast conn params", this->connection_index_, this->address_str_.c_str()); + } + } } } @@ -278,12 +308,14 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->address_str_.c_str(), ret); } this->set_state(espbt::ClientState::CONNECTED); + ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str()); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Using cached services", this->connection_index_, this->address_str_.c_str()); // only set our state, subclients might have more stuff to do yet. this->state_ = espbt::ClientState::ESTABLISHED; break; } + ESP_LOGD(TAG, "[%d] [%s] Searching for services", this->connection_index_, this->address_str_.c_str()); esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); break; } @@ -296,8 +328,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_DISCONNECT_EVT: { if (!this->check_addr(param->disconnect.remote_bda)) return false; - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, - this->address_str_.c_str(), param->disconnect.reason); + // Check if we were disconnected while waiting for service discovery + if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && + this->state_ == espbt::ClientState::CONNECTED) { + ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_, + this->address_str_.c_str()); + } else { + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, + this->address_str_.c_str(), param->disconnect.reason); + } this->release_services(); this->set_state(espbt::ClientState::IDLE); break; @@ -353,7 +392,22 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_.c_str(), svc->start_handle, svc->end_handle); } - ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str()); + + // For non-cached connections, restore default connection parameters after service discovery + // Now that we've discovered all services, we can use more balanced parameters + // that save power and reduce interference + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + esp_ble_conn_update_params_t conn_params = {{0}}; + memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); + conn_params.min_int = DEFAULT_MIN_CONN_INTERVAL; + conn_params.max_int = DEFAULT_MAX_CONN_INTERVAL; + conn_params.latency = 0; + conn_params.timeout = DEFAULT_CONN_TIMEOUT; + ESP_LOGD(TAG, "[%d] [%s] Restored default conn params", this->connection_index_, this->address_str_.c_str()); + esp_ble_gap_update_conn_params(&conn_params); + } + this->state_ = espbt::ClientState::ESTABLISHED; break; }