diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 71dedd65aa..f3f7cb30eb 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -67,7 +67,7 @@ jobs: with: python-version: "3.12" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Determine tag and whether to push id: tag @@ -153,7 +153,7 @@ jobs: with: python-version: "3.12" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to the GitHub container registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 10b28ace38..d0dee8165c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,7 +123,7 @@ jobs: python-version: "3.12" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to docker hub uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 @@ -202,7 +202,7 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub' diff --git a/CODEOWNERS b/CODEOWNERS index 9ddbca5c71..3047072ea2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -381,6 +381,7 @@ esphome/components/nextion/switch/* @senexcrenshaw esphome/components/nextion/text_sensor/* @senexcrenshaw esphome/components/nfc/* @jesserockz @kbx81 esphome/components/noblex/* @AGalfra +esphome/components/noise/* @esphome/core esphome/components/npi19/* @bakerkj esphome/components/nrf52/* @tomaszduda23 esphome/components/number/* @esphome/core diff --git a/docker/Dockerfile b/docker/Dockerfile index 18f705b501..9f27d51059 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.12.0 +RUN uv pip install --no-cache-dir esphome-device-builder==1.12.4 RUN \ platformio settings set enable_telemetry No \ diff --git a/esphome/__main__.py b/esphome/__main__.py index c1e05d2ea7..769b66ecc8 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -762,9 +762,11 @@ def _wrap_to_code(name, comp, yaml_util): async def wrapped(conf): cg.add(cg.LineComment(f"{name}:")) if comp.config_schema is not None: - conf_str = yaml_util.dump(conf) + # sort_keys: voluptuous fills defaults in set order, so an + # unsorted dump would churn main.cpp and relink every run + conf_str = yaml_util.dump(conf, sort_keys=True) conf_str = conf_str.replace("//", "") - # remove tailing \ to avoid multi-line comment warning + # remove trailing \ to avoid multi-line comment warning conf_str = conf_str.replace("\\\n", "\n") cg.add(cg.LineComment(indent(conf_str))) await coro(conf) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index cf476555e7..5d4e6b8401 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -3,7 +3,12 @@ import json from pathlib import Path -from esphome.components.esp32 import get_esp32_variant, idf_version +from esphome.components.esp32 import ( + get_esp32_variant, + get_excluded_builtin_components, + get_managed_component_require_names, + idf_version, +) import esphome.config_validation as cv from esphome.core import CORE from esphome.framework_helpers import ( @@ -67,6 +72,13 @@ def has_discovered_components() -> bool: return get_available_components() is not None +def _cmake_quote(value: str) -> str: + """Quote a cmake arg value for a set() line. add_cmake_arg rejects + whitespace, quotes, and '$', so only backslashes need escaping.""" + escaped = value.replace("\\", "\\\\") + return f'"{escaped}"' + + def get_project_cmakelists(minimal: bool = False) -> str: """Generate the top-level CMakeLists.txt for ESP-IDF project. @@ -109,6 +121,15 @@ def get_project_cmakelists(minimal: bool = False) -> str: else "" ) + # CMake variables registered via cg.add_cmake_arg(). Emitted before + # include(project.cmake) so values like EXCLUDE_COMPONENTS are already + # set when project.cmake seeds the component list, and on minimal + # (discovery) writes too so excluded components never register. + cmake_args = "\n".join( + f"set({name} {_cmake_quote(value)})" + for name, value in sorted(CORE.cmake_args.items()) + ) + # Per-project list exposed as a CMake variable so converted PIO libs # can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking # project-specific names into their cached CMakeLists. @@ -119,8 +140,6 @@ def get_project_cmakelists(minimal: bool = False) -> str: # runs as a separate CMake script invocation that doesn't load the # project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_ # MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty). - from esphome.components.esp32 import get_managed_component_require_names - managed_components_property = "\n".join( f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)" for name in get_managed_component_require_names() @@ -131,12 +150,22 @@ def get_project_cmakelists(minimal: bool = False) -> str: # component's REQUIRES including real IDF components). Referenced by # src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped # on minimal writes because project_description.json may be stale. + # Excluded components are dropped here as well: a stale + # project_description.json from a build without exclusions may still + # list them, and requiring an excluded component pulls it back into + # the build (IDF requirement expansion overrides EXCLUDE_COMPONENTS). + # Derived from the EXCLUDE_COMPONENTS cmake arg emitted above so the + # two can never disagree within one generated file. builtin_components_property = ( "" if minimal else "\n".join( f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)" - for name in sorted(get_available_components() or []) + for name in sorted( + set(get_available_components() or []).difference( + CORE.cmake_args.get("EXCLUDE_COMPONENTS", "").split(";") + ) + ) ) ) @@ -163,6 +192,8 @@ set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1) set(IDF_TARGET {idf_target}) set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) +{cmake_args} + include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) {cpp_standard_options} @@ -264,3 +295,13 @@ def write_project(minimal: bool = False) -> None: CORE.relative_src_path("CMakeLists.txt"), get_component_cmakelists(), ) + + # Snapshot the exclusion set so has_outdated_files() can trigger a + # discovery reconfigure when it changes. Excluded components never + # register in project_description.json, so re-including one (e.g. a + # config gains mqtt) requires a fresh discovery pass before the + # ESPHOME_PROJECT_BUILTIN_COMPONENTS property can list it. + write_file_if_changed( + CORE.relative_build_path("exclude_components.esphomeinternal"), + ";".join(get_excluded_builtin_components()), + ) diff --git a/esphome/build_gen/platformio.py b/esphome/build_gen/platformio.py index b63c4b733d..0a12d344a0 100644 --- a/esphome/build_gen/platformio.py +++ b/esphome/build_gen/platformio.py @@ -63,6 +63,17 @@ def get_ini_content(): # Add extra script for C++ flags CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) + # Add CMake args. A user-supplied value (str or list) is deliberately + # replaced; this option was always overwritten at FINAL priority. + if CORE.cmake_args: + CORE.add_platformio_option( + "board_build.cmake_extra_args", + " ".join( + f"-D{name}={value}" for name, value in sorted(CORE.cmake_args.items()) + ), + replace=True, + ) + content = "[platformio]\n" content += f"description = ESPHome {__version__}\n" diff --git a/esphome/codegen.py b/esphome/codegen.py index 2430f17f3a..2aa6a70abd 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -25,6 +25,7 @@ from esphome.cpp_generator import ( # noqa: F401 add, add_build_flag, add_build_unflag, + add_cmake_arg, add_cxx_build_flag, add_define, add_global, diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index 1f35095e0e..48bef2c317 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -49,6 +49,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + # Re-enable the gptimer driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_gptimer") + if CORE.is_esp8266: # ac_dimmer uses setTimer1Callback which requires the waveform generator from esphome.components.esp8266.const import require_waveform diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 1c50b6b81b..5c763a4f4c 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import pins import esphome.codegen as cg from esphome.components.esp32 import ( @@ -16,6 +18,7 @@ from esphome.components.esp32 import ( import esphome.config_validation as cv from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 from esphome.core import CORE +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -225,7 +228,7 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { } -def validate_adc_pin(value): +def validate_adc_pin(value: Any) -> ConfigType | str: if str(value).upper() == "VCC": if CORE.is_rp2: return pins.internal_gpio_input_pin_schema(29) diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index b2a4382a21..5d1031825e 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -52,7 +52,7 @@ _attenuation = cv.enum(ATTENUATION_MODES, lower=True) _sampling_mode = cv.enum(SAMPLING_MODES, lower=True) -def validate_config(config): +def validate_config(config: ConfigType) -> ConfigType: if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": raise cv.Invalid("Automatic attenuation cannot be used when raw output is set") @@ -120,7 +120,7 @@ CONFIG_SCHEMA = cv.All( CONF_ADC_CHANNEL_ID = "adc_channel_id" -def _overlay_io_channels(): +def _overlay_io_channels() -> str: channel_count = CORE.data[CONF_ADC_CHANNEL_ID] entries = ", ".join(f"<&adc {channel_id}>" for channel_id in range(channel_count)) return f""" @@ -132,7 +132,7 @@ def _overlay_io_channels(): """ -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await sensor.register_sensor(var, config) diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py index 6491d7d810..8606e7c247 100644 --- a/esphome/components/ags10/sensor.py +++ b/esphome/components/ags10/sensor.py @@ -17,6 +17,9 @@ from esphome.const import ( UNIT_OHM, UNIT_PARTS_PER_BILLION, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CONF_RESISTANCE = "resistance" @@ -62,7 +65,7 @@ CONFIG_SCHEMA = ( FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency="15khz") -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) @@ -94,7 +97,12 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( AGS10_NEW_I2C_ADDRESS_SCHEMA, synchronous=True, ) -async def ags10newi2caddress_to_code(config, action_id, template_arg, args): +async def ags10newi2caddress_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) @@ -126,7 +134,12 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( AGS10_SET_ZERO_POINT_SCHEMA, synchronous=True, ) -async def ags10setzeropoint_to_code(config, action_id, template_arg, args): +async def ags10setzeropoint_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) mode = await cg.templatable( diff --git a/esphome/components/aic3204/audio_dac.py b/esphome/components/aic3204/audio_dac.py index b478b573a3..50e2f81f1b 100644 --- a/esphome/components/aic3204/audio_dac.py +++ b/esphome/components/aic3204/audio_dac.py @@ -4,6 +4,9 @@ from esphome.components import i2c from esphome.components.audio_dac import AudioDac import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODE +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] DEPENDENCIES = ["i2c"] @@ -39,7 +42,12 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value( SET_AUTO_MUTE_ACTION_SCHEMA, synchronous=True, ) -async def aic3204_set_volume_to_code(config, action_id, template_arg, args): +async def aic3204_set_volume_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -49,7 +57,7 @@ async def aic3204_set_volume_to_code(config, action_id, template_arg, args): return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/animation/image.py b/esphome/components/animation/image.py index 73d428bd20..0265a350f7 100644 --- a/esphome/components/animation/image.py +++ b/esphome/components/animation/image.py @@ -6,6 +6,8 @@ from esphome.components.file.image import image_schema, write_image from esphome.components.image import Image_, validate_settings import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_REPEAT +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@syndlex"] @@ -79,7 +81,12 @@ SET_FRAME_SCHEMA = cv.Schema( @automation.register_action( "animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True ) -async def animation_action_to_code(config, action_id, template_arg, args): +async def animation_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/apds9960/__init__.py b/esphome/components/apds9960/__init__.py index 99e37d3764..7ac1e5eb32 100644 --- a/esphome/components/apds9960/__init__.py +++ b/esphome/components/apds9960/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] MULTI_CONF = True @@ -57,7 +58,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/apds9960/binary_sensor.py b/esphome/components/apds9960/binary_sensor.py index 48e923ab2b..342f688249 100644 --- a/esphome/components/apds9960/binary_sensor.py +++ b/esphome/components/apds9960/binary_sensor.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import CONF_DIRECTION, DEVICE_CLASS_MOVING +from esphome.types import ConfigType from . import APDS9960, CONF_APDS9960_ID @@ -19,7 +20,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_APDS9960_ID]) var = await binary_sensor.new_binary_sensor(config) func = getattr(hub, f"set_{config[CONF_DIRECTION]}_direction_binary_sensor") diff --git a/esphome/components/apds9960/sensor.py b/esphome/components/apds9960/sensor.py index 468eb0995f..a75fb79d1b 100644 --- a/esphome/components/apds9960/sensor.py +++ b/esphome/components/apds9960/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ) +from esphome.types import ConfigType from . import APDS9960, CONF_APDS9960_ID @@ -27,7 +28,7 @@ CONFIG_SCHEMA = sensor.sensor_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_APDS9960_ID]) var = await sensor.new_sensor(config) func = getattr(hub, f"set_{config[CONF_TYPE]}_sensor") diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 912d580a0f..53ad0fe5d7 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,10 +1,19 @@ -import base64 import logging +from typing import Any from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components.logger import request_log_listener + +# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external +# components and downstream consumers that import them from api +from esphome.components.noise import ( # noqa: F401 + ENCRYPTION_SCHEMA, + decode_encryption_key, + encryption_schema, + validate_encryption_key, +) from esphome.config_helpers import get_logger_level import esphome.config_validation as cv from esphome.const import ( @@ -37,6 +46,10 @@ from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_pr from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigFragmentType, ConfigType +# Compat alias: downstream consumers (e.g. device-builder) referenced the +# schema by its old private name before it moved to the noise component +_encryption_schema = encryption_schema + _LOGGER = logging.getLogger(__name__) DOMAIN = "api" @@ -45,9 +58,15 @@ CODEOWNERS = ["@esphome/core"] def AUTO_LOAD(config: ConfigType) -> list[str]: - """Conditionally auto-load json only when capture_response is used.""" + """Conditionally auto-load noise (encryption) and json (capture_response).""" base = ["socket"] + # A falsy config is a tooling probe for the maximal set (None from + # dependency resolution, {} from the components-graph platform probe); + # a validated config always carries defaults, never empty + if not config or CONF_ENCRYPTION in config: + base = base + ["noise"] + # Check if any homeassistant.action/homeassistant.service has capture_response: true # This flag is set during config validation in _validate_response_config if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False): @@ -129,20 +148,6 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType: return config -def validate_encryption_key(value): - value = cv.string_strict(value) - try: - decoded = base64.b64decode(value, validate=True) - except ValueError as err: - raise cv.Invalid("Invalid key format, please check it's using base64") from err - - if len(decoded) != 32: - raise cv.Invalid("Encryption key must be base64 and 32 bytes long") - - # Return original data for roundtrip conversion - return value - - CONF_SUPPORTS_RESPONSE = "supports_response" # Enum values in api::enums namespace @@ -217,7 +222,7 @@ def _auto_detect_supports_response(config: ConfigType) -> ConfigType: return config -def _validate_supports_response(value): +def _validate_supports_response(value: Any) -> str: """Validate supports_response after auto-detection has set the value.""" return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value) @@ -249,18 +254,6 @@ ACTIONS_SCHEMA = automation.validate_automation( ), ) -ENCRYPTION_SCHEMA = cv.Schema( - { - cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key), - } -) - - -def _encryption_schema(config): - if config is None: - config = {} - return ENCRYPTION_SCHEMA(config) - def _consume_api_sockets(config: ConfigType) -> ConfigType: """Register socket needs for API component.""" @@ -296,7 +289,7 @@ CONFIG_SCHEMA = cv.All( CONF_SERVICES, group_of_exclusion=CONF_ACTIONS ): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, - cv.Optional(CONF_ENCRYPTION): _encryption_schema, + cv.Optional(CONF_ENCRYPTION): encryption_schema, cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All( cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=65535)), @@ -393,7 +386,7 @@ async def to_code(config: ConfigType) -> None: if actions := config.get(CONF_ACTIONS, []): # Collect all triggers first, then register all at once with initializer_list - triggers: list[cg.Pvariable] = [] + triggers: list[cg.MockObj] = [] for conf in actions: func_args: list[tuple[MockObj, str]] = [] service_template_args: list[MockObj] = [] # User service argument types @@ -483,7 +476,7 @@ async def to_code(config: ConfigType) -> None: if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None: if key := encryption_config.get(CONF_KEY): - decoded = base64.b64decode(key) + decoded = decode_encryption_key(key) cg.add(var.set_noise_psk(list(decoded))) cg.add_define("USE_API_NOISE_PSK_FROM_YAML") else: @@ -497,10 +490,6 @@ async def to_code(config: ConfigType) -> None: # and plaintext disabled. Only a factory reset can remove it. cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.21") - # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops - cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") - cg.add_build_flag("-DHAVE_INLINE_ASM=1") else: cg.add_define("USE_API_PLAINTEXT") @@ -581,7 +570,7 @@ async def homeassistant_service_to_code( action_id: ID, template_arg: cg.TemplateArguments, args: TemplateArgsType, -): +) -> MockObj: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) @@ -647,7 +636,7 @@ async def homeassistant_service_to_code( return var -def validate_homeassistant_event(value): +def validate_homeassistant_event(value: Any) -> str: value = cv.string(value) if not value.startswith("esphome."): raise cv.Invalid( @@ -676,7 +665,12 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema( HOMEASSISTANT_EVENT_ACTION_SCHEMA, synchronous=True, ) -async def homeassistant_event_to_code(config, action_id, template_arg, args): +async def homeassistant_event_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) @@ -724,7 +718,12 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value( HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA, synchronous=True, ) -async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args): +async def homeassistant_tag_scanned_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) @@ -740,7 +739,7 @@ CONF_SUCCESS = "success" CONF_ERROR_MESSAGE = "error_message" -def _validate_api_respond_data(config): +def _validate_api_respond_data(config: ConfigType) -> ConfigType: """Set flag during validation so AUTO_LOAD can include json component.""" if CONF_DATA in config: CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True @@ -824,7 +823,12 @@ API_CONNECTED_CONDITION_SCHEMA = cv.Schema( @automation.register_condition( "api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA ) -async def api_connected_to_code(config, condition_id, template_arg, args): +async def api_connected_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_) cg.add(var.set_state_subscription_only(templ)) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2eb8c21c73..91d13eed65 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -412,15 +412,15 @@ void APIConnection::finalize_iterator_sync_() { } void APIConnection::process_iterator_batch_(ComponentIterator &iterator) { - size_t initial_size = this->deferred_batch_.size(); - size_t max_batch = MAX_INITIAL_PER_BATCH; - while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { - iterator.advance(); - } + // Budget by remaining batch capacity so a pass cannot overfill the batch; + // stops early on a refused send and resumes next loop pass + size_t batch_size = this->deferred_batch_.size(); + if (batch_size < MAX_INITIAL_BATCH_SIZE) + iterator.try_advance(MAX_INITIAL_BATCH_SIZE - batch_size); - // If the batch is full, process it immediately - // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() - if (this->deferred_batch_.size() >= max_batch) { + // Flush immediately once enough is queued (not guaranteed every pass); + // partial batches go out via the batch timer or finalize_iterator_sync_() + if (this->deferred_batch_.size() >= MAX_INITIAL_BATCH_SIZE) { this->process_batch_(); } } @@ -2130,7 +2130,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio } #endif - psk_t psk{}; + noise::psk_t psk{}; if (msg.key_len == 0) { if (this->parent_->clear_noise_psk(true)) { resp.success = true; @@ -2139,7 +2139,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio } } else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) { ESP_LOGW(TAG, "Invalid encryption key length"); - } else if (APINoiseContext::is_all_zeros(psk)) { + } else if (noise::NoiseContext::is_all_zeros(psk)) { // Accepting the reserved provisioning PSK would report success without // enabling encryption (or silently clear an existing key) ESP_LOGW(TAG, "Rejecting all-zero encryption key"); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index bb51a13000..1b47c23cfe 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -53,11 +53,11 @@ void log_dropped_message(const char *tag, int line, const LogString *what); // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; -// Maximum number of entities to process in a single batch during initial state/info sending -static constexpr size_t MAX_INITIAL_PER_BATCH = 34; +// Deferred batch size cap during initial state/info sync +static constexpr size_t MAX_INITIAL_BATCH_SIZE = 34; // Verify MAX_MESSAGES_PER_BATCH (defined in api_frame_helper.h) can hold the initial batch -static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH, - "MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH"); +static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_BATCH_SIZE, + "MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_BATCH_SIZE"); #ifdef USE_BENCHMARK class APIConnection; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 9c49956bbd..1c60bb87a5 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -36,7 +36,7 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and oth static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1; // Maximum number of messages to batch in a single write operation -// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) +// Must be >= MAX_INITIAL_BATCH_SIZE in api_connection.h (enforced by static_assert there) static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; // Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 09e3ca2b9e..d7554e62c5 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -2,9 +2,9 @@ #ifdef USE_API #ifdef USE_API_NOISE #include "api_connection.h" // For ClientInfo struct +#include "esphome/components/noise/noise.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" -#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "proto.h" @@ -17,6 +17,14 @@ namespace esphome::api { +using noise::noise_err_to_logstr; + +// api_frame_helper.h keeps its own MAX_HANDSHAKE_SIZE because that header is +// also compiled in plaintext-only builds without the noise component; keep +// the two definitions from drifting apart. +static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE, + "api and noise component handshake size limits must match"); + static const char *const TAG = "api.noise"; #ifdef USE_ESP8266 static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; @@ -51,45 +59,6 @@ static constexpr size_t API_MAX_LOG_BYTES = 168; #define LOG_PACKET_RECEIVED(buffer) ((void) 0) #endif -/// Convert a noise error code to a readable error -const LogString *noise_err_to_logstr(int err) { - if (err == NOISE_ERROR_NO_MEMORY) - return LOG_STR("NO_MEMORY"); - if (err == NOISE_ERROR_UNKNOWN_ID) - return LOG_STR("UNKNOWN_ID"); - if (err == NOISE_ERROR_UNKNOWN_NAME) - return LOG_STR("UNKNOWN_NAME"); - if (err == NOISE_ERROR_MAC_FAILURE) - return LOG_STR("MAC_FAILURE"); - if (err == NOISE_ERROR_NOT_APPLICABLE) - return LOG_STR("NOT_APPLICABLE"); - if (err == NOISE_ERROR_SYSTEM) - return LOG_STR("SYSTEM"); - if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) - return LOG_STR("REMOTE_KEY_REQUIRED"); - if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) - return LOG_STR("LOCAL_KEY_REQUIRED"); - if (err == NOISE_ERROR_PSK_REQUIRED) - return LOG_STR("PSK_REQUIRED"); - if (err == NOISE_ERROR_INVALID_LENGTH) - return LOG_STR("INVALID_LENGTH"); - if (err == NOISE_ERROR_INVALID_PARAM) - return LOG_STR("INVALID_PARAM"); - if (err == NOISE_ERROR_INVALID_STATE) - return LOG_STR("INVALID_STATE"); - if (err == NOISE_ERROR_INVALID_NONCE) - return LOG_STR("INVALID_NONCE"); - if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) - return LOG_STR("INVALID_PRIVATE_KEY"); - if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) - return LOG_STR("INVALID_PUBLIC_KEY"); - if (err == NOISE_ERROR_INVALID_FORMAT) - return LOG_STR("INVALID_FORMAT"); - if (err == NOISE_ERROR_INVALID_SIGNATURE) - return LOG_STR("INVALID_SIGNATURE"); - return LOG_STR("UNKNOWN"); -} - /// Initialize the frame helper, returns OK if successful. APIError APINoiseFrameHelper::init() { APIError err = init_common_(); @@ -194,9 +163,9 @@ APIError APINoiseFrameHelper::loop() { */ APIError APINoiseFrameHelper::try_read_frame_() { // read header - if (rx_header_buf_len_ < 3) { + if (rx_header_buf_len_ < noise::FRAME_HEADER_SIZE) { // no header information yet - uint8_t to_read = 3 - rx_header_buf_len_; + uint8_t to_read = static_cast(noise::FRAME_HEADER_SIZE) - rx_header_buf_len_; ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); APIError err = handle_socket_read_result_(received); if (err != APIError::OK) { @@ -208,7 +177,7 @@ APIError APINoiseFrameHelper::try_read_frame_() { return APIError::WOULD_BLOCK; } - if (rx_header_buf_[0] != 0x01) { + if (rx_header_buf_[0] != noise::FRAME_INDICATOR) { state_ = State::FAILED; HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); return APIError::BAD_INDICATOR; @@ -348,15 +317,15 @@ APIError APINoiseFrameHelper::state_action_server_hello_() { return APIError::OK; } APIError APINoiseFrameHelper::state_action_handshake_() { - int action = noise_handshakestate_get_action(this->handshake_); - if (action == NOISE_ACTION_READ_MESSAGE) { + noise::NoiseResponderHandshake::Action action = this->handshake_.action(); + if (action == noise::NoiseResponderHandshake::Action::ACTION_READ) { return this->state_action_handshake_read_(); - } else if (action == NOISE_ACTION_WRITE_MESSAGE) { + } else if (action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) { return this->state_action_handshake_write_(); } // bad state for action this->state_ = State::FAILED; - HELPER_LOG("Bad action for handshake: %d", action); + HELPER_LOG("Bad action for handshake: %d", (int) action); return APIError::HANDSHAKESTATE_BAD_STATE; } APIError APINoiseFrameHelper::state_action_handshake_read_() { @@ -368,20 +337,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() { if (this->rx_buf_.empty()) { this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } else if (this->rx_buf_[0] != 0x00) { + } else if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) { HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; } - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); - int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr); + int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); if (err != 0) { // Special handling for MAC failure - this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") - : LOG_STR("Handshake error")); + this->send_explicit_handshake_reject_(noise::reject_reason_for(err)); return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), APIError::HANDSHAKESTATE_READ_FAILED); } @@ -390,18 +355,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() { } APIError APINoiseFrameHelper::state_action_handshake_write_() { uint8_t buffer[65]; - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); + size_t msg_len = 0; - int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr); + int err = this->handshake_.write_message(buffer + 1, sizeof(buffer) - 1, msg_len); APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), APIError::HANDSHAKESTATE_WRITE_FAILED); if (aerr != APIError::OK) return aerr; - buffer[0] = 0x00; // success + buffer[0] = noise::HANDSHAKE_STATUS_OK; - aerr = this->write_frame_(buffer, mbuf.size + 1); + aerr = this->write_frame_(buffer, msg_len + 1); if (aerr != APIError::OK) return aerr; return this->check_handshake_finished_(); @@ -409,33 +372,22 @@ APIError APINoiseFrameHelper::state_action_handshake_write_() { void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { // Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes uint8_t data[32]; - data[0] = 0x01; // failure - -#ifdef USE_STORE_LOG_STR_IN_FLASH - // On ESP8266 with flash strings, we need to use PROGMEM-aware functions - size_t reason_len = strlen_P(reinterpret_cast(reason)); - reason_len = std::min(reason_len, sizeof(data) - 1); - if (reason_len > 0) { - memcpy_P(data + 1, reinterpret_cast(reason), reason_len); - } -#else - // Normal memory access - const char *reason_str = LOG_STR_ARG(reason); - size_t reason_len = strlen(reason_str); - reason_len = std::min(reason_len, sizeof(data) - 1); - if (reason_len > 0) { - // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string - std::memcpy(data + 1, reason_str, reason_len); - } -#endif - - size_t data_size = reason_len + 1; + static_assert(sizeof(data) >= noise::MAC_FAILURE_PAYLOAD_SIZE, + "reject buffer must fit the MAC failure wire contract"); + size_t data_size = noise::format_reject_payload(data, sizeof(data), reason); // temporarily remove failed state auto orig_state = state_; state_ = State::EXPLICIT_REJECT; - write_frame_(data, data_size); - state_ = orig_state; + APIError aerr = write_frame_(data, data_size); + if (aerr != APIError::OK) { + // Best effort; the reject reason is a diagnosis aid, not a protocol step + ESP_LOGW(TAG, "Sending handshake reject failed: %d", (int) aerr); + } + if (state_ == State::EXPLICIT_REJECT) { + // write_frame_ may have moved the state to FAILED; keep that decision + state_ = orig_state; + } } APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError aerr = this->check_data_state_(); @@ -492,12 +444,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { // Returns APIError::OK on success. APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type, uint16_t &encrypted_len_out) { - // Write noise header - buf_start[0] = 0x01; // indicator - // buf_start[1], buf_start[2] to be set after encryption + // The noise frame header is written after encryption, when the size is known // Write message header (to be encrypted) - constexpr uint8_t msg_offset = 3; + constexpr uint8_t msg_offset = noise::FRAME_HEADER_SIZE; buf_start[msg_offset] = static_cast(message_type >> 8); // type high byte buf_start[msg_offset + 1] = static_cast(message_type); // type low byte buf_start[msg_offset + 2] = static_cast(payload_size >> 8); // data_len high byte @@ -515,11 +465,10 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_ if (aerr != APIError::OK) return aerr; - // Fill in the encrypted size - buf_start[1] = static_cast(mbuf.size >> 8); - buf_start[2] = static_cast(mbuf.size); + // Fill in the frame header now that the encrypted size is known + noise::write_frame_header(buf_start, static_cast(mbuf.size)); - encrypted_len_out = static_cast(3 + mbuf.size); // indicator + size + encrypted data + encrypted_len_out = static_cast(noise::FRAME_HEADER_SIZE + mbuf.size); return APIError::OK; } @@ -568,21 +517,19 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s } APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { - uint8_t header[3]; - header[0] = 0x01; // indicator - header[1] = (uint8_t) (len >> 8); - header[2] = (uint8_t) len; + uint8_t header[noise::FRAME_HEADER_SIZE]; + noise::write_frame_header(header, len); if (len == 0) { - return this->write_raw_buf_(header, 3); + return this->write_raw_buf_(header, noise::FRAME_HEADER_SIZE); } struct iovec iov[2]; iov[0].iov_base = header; - iov[0].iov_len = 3; + iov[0].iov_len = noise::FRAME_HEADER_SIZE; iov[1].iov_base = const_cast(data); iov[1].iov_len = len; - return this->write_raw_iov_(iov, 2, 3 + len); + return this->write_raw_iov_(iov, 2, noise::FRAME_HEADER_SIZE + len); } /** Initiate the data structures for the handshake. @@ -590,45 +537,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { * @return 0 on success, -1 on error (check errno) */ APIError APINoiseFrameHelper::init_handshake_() { - int err; - // Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack: - // noise_handshakestate_new_by_id copies it, so a member would waste - // 104 bytes per connection, and a static const would sit in RAM on - // ESP8266 (.rodata is DRAM there). - const NoiseProtocolId nid = { - .prefix_id = NOISE_PREFIX_STANDARD, - .pattern_id = NOISE_PATTERN_NN, - .modifier_ids = {NOISE_MODIFIER_PSK0}, - .dh_id = NOISE_DH_CURVE25519, - .cipher_id = NOISE_CIPHER_CHACHAPOLY, - .hash_id = NOISE_HASH_SHA256, - .hybrid_id = NOISE_DH_NONE, - }; - - err = noise_handshakestate_new_by_id(&handshake_, &nid, NOISE_ROLE_RESPONDER); - APIError aerr = - handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED); + int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size()); + APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; - - const auto &psk = this->ctx_.get_psk(); - err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); - aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), - APIError::HANDSHAKESTATE_SETUP_FAILED); - if (aerr != APIError::OK) - return aerr; - - err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); - aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED); - if (aerr != APIError::OK) - return aerr; - // set_prologue copies it into handshakestate, so we can get rid of it now + // init copies the prologue into the handshakestate, so we can get rid of it now prologue_.release(); - - err = noise_handshakestate_start(handshake_); - aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); - if (aerr != APIError::OK) - return aerr; return APIError::OK; } @@ -637,15 +551,17 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { assert(state_ == State::HANDSHAKE); #endif - int action = noise_handshakestate_get_action(handshake_); - if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) + noise::NoiseResponderHandshake::Action action = this->handshake_.action(); + if (action == noise::NoiseResponderHandshake::Action::ACTION_READ || + action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) return APIError::OK; - if (action != NOISE_ACTION_SPLIT) { + if (action != noise::NoiseResponderHandshake::Action::ACTION_SPLIT) { state_ = State::FAILED; - HELPER_LOG("Bad action for handshake: %d", action); + HELPER_LOG("Bad action for handshake: %d", (int) action); return APIError::HANDSHAKESTATE_BAD_STATE; } - int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); + // split() also frees the handshake state + int err = this->handshake_.split(send_cipher_, recv_cipher_); APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED); if (aerr != APIError::OK) @@ -654,17 +570,11 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); HELPER_LOG("Handshake complete!"); - noise_handshakestate_free(handshake_); - handshake_ = nullptr; state_ = State::DATA; return APIError::OK; } APINoiseFrameHelper::~APINoiseFrameHelper() { - if (handshake_ != nullptr) { - noise_handshakestate_free(handshake_); - handshake_ = nullptr; - } if (send_cipher_ != nullptr) { noise_cipherstate_free(send_cipher_); send_cipher_ = nullptr; @@ -675,16 +585,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() { } } -extern "C" { -// declare how noise generates random bytes (here with a good HWRNG based on the RF system) -void noise_rand_bytes(void *output, size_t len) { - if (!esphome::random_bytes(reinterpret_cast(output), len)) { - ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting"); - arch_restart(); - } -} -} - } // namespace esphome::api #endif // USE_API_NOISE #endif // USE_API diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 46bd366672..05060c77de 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -3,7 +3,7 @@ #ifdef USE_API #ifdef USE_API_NOISE #include "noise/protocol.h" -#include "api_noise_context.h" +#include "esphome/components/noise/noise_handshake.h" namespace esphome::api { @@ -14,9 +14,9 @@ class APINoiseFrameHelper final : public APIFrameHelper { // Pos 1-2: encrypted payload size (16-bit big-endian) // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) // Pos 7+: actual payload data - static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len + static constexpr uint8_t HEADER_PADDING = noise::FRAME_HEADER_SIZE + 2 + 2; // frame header + type + data_len - APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx) + APINoiseFrameHelper(std::unique_ptr socket, noise::NoiseContext &ctx) : APIFrameHelper(std::move(socket)), ctx_(ctx) { frame_header_padding_ = HEADER_PADDING; } @@ -52,13 +52,13 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError handle_handshake_frame_error_(APIError aerr); APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err); - // Pointers first (4 bytes each) - NoiseHandshakeState *handshake_{nullptr}; + // Pointers first (4 bytes each; the handshake wrapper holds one pointer) + noise::NoiseResponderHandshake handshake_; NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr}; // Reference to noise context (4 bytes on 32-bit) - APINoiseContext &ctx_; + noise::NoiseContext &ctx_; // Buffer for noise handshake prologue (released after handshake) APIBuffer prologue_; @@ -67,7 +67,7 @@ class APINoiseFrameHelper final : public APIFrameHelper { // Fixed-size header buffer for noise protocol: // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase - uint8_t rx_header_buf_[3]; + uint8_t rx_header_buf_[noise::FRAME_HEADER_SIZE]; uint8_t rx_header_buf_len_ = 0; // 4 bytes total, no padding }; diff --git a/esphome/components/api/api_noise_context.h b/esphome/components/api/api_noise_context.h deleted file mode 100644 index 44484ffa2c..0000000000 --- a/esphome/components/api/api_noise_context.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once -#include -#include -#include "esphome/core/defines.h" - -namespace esphome::api { - -#ifdef USE_API_NOISE -using psk_t = std::array; - -class APINoiseContext { - public: - // The all-zeros PSK is reserved: it marks the device as unprovisioned and - // doubles as the well-known provisioning PSK that unprovisioned devices - // accept for Noise handshakes (passive-sniffing protection only, no - // authentication). It is never a valid real key. - static bool is_all_zeros(const psk_t &psk) { - uint8_t acc = 0; - for (uint8_t b : psk) { - acc |= b; - } - return acc == 0; - } - void set_psk(psk_t psk) { - this->psk_ = psk; - this->has_psk_ = !is_all_zeros(psk); - } - const psk_t &get_psk() const { return this->psk_; } - bool has_psk() const { return this->has_psk_; } - - protected: - psk_t psk_{}; - bool has_psk_{false}; -}; -#endif // USE_API_NOISE - -} // namespace esphome::api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ef5b43d7b1..751f2e4c3b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -423,12 +423,6 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_ API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif -float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } - -void APIServer::set_port(uint16_t port) { this->port_ = port; } - -void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } - #ifdef USE_API_HOMEASSISTANT_SERVICES void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) { bool has_subscriber = false; @@ -553,10 +547,6 @@ const std::vector &APIServer::get_sta } #endif -uint16_t APIServer::get_port() const { return this->port_; } - -void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } - #ifdef USE_API_NOISE bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool make_active) { @@ -598,7 +588,7 @@ bool APIServer::load_and_apply_noise_psk_() { return true; } -bool APIServer::save_noise_psk(psk_t psk, bool make_active) { +bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) { #ifdef USE_API_NOISE_PSK_FROM_YAML // When PSK is set from YAML, this function should never be called // but if it is, reject the change diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 248b83a0ff..072a583901 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -5,7 +5,10 @@ #include "api_buffer.h" // Must precede clients_ so APIConnection is complete for default_delete (libc++). #include "api_connection.h" -#include "api_noise_context.h" +#ifdef USE_API_NOISE +// Only present in the build when the noise component is loaded +#include "esphome/components/noise/noise.h" +#endif #include "api_pb2.h" #include "api_pb2_service.h" #include "esphome/components/socket/socket.h" @@ -37,7 +40,7 @@ class UserServiceDescriptor; #ifdef USE_API_NOISE struct SavedNoisePsk { - psk_t psk; + noise::psk_t psk; } PACKED; // NOLINT #endif @@ -51,8 +54,8 @@ class APIServer final : public Component, public: APIServer(); void setup() override; - uint16_t get_port() const; - float get_setup_priority() const override; + uint16_t get_port() const { return this->port_; } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void loop() override; void dump_config() override; void on_shutdown() override; @@ -63,9 +66,9 @@ class APIServer final : public Component, #ifdef USE_CAMERA void on_camera_image(const std::shared_ptr &image) override; #endif - void set_port(uint16_t port); - void set_reboot_timeout(uint32_t reboot_timeout); - void set_batch_delay(uint16_t batch_delay); + void set_port(uint16_t port) { this->port_ = port; } + void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } + void set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } uint16_t get_batch_delay() const { return batch_delay_; } void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; } @@ -73,10 +76,10 @@ class APIServer final : public Component, APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } #ifdef USE_API_NOISE - bool save_noise_psk(psk_t psk, bool make_active = true); + bool save_noise_psk(noise::psk_t psk, bool make_active = true); bool clear_noise_psk(bool make_active = true); - void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); } - APINoiseContext &get_noise_ctx() { return this->noise_ctx_; } + void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); } + noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; } #endif // USE_API_NOISE void handle_disconnect(APIConnection *conn); @@ -354,7 +357,7 @@ class APIServer final : public Component, #endif #ifdef USE_API_NOISE - APINoiseContext noise_ctx_; + noise::NoiseContext noise_ctx_; ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE }; diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index f9e645b506..57ff616ca7 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -95,9 +95,17 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done( ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} #ifdef USE_API_USER_DEFINED_ACTIONS +// Yield after every Nth service; bounds direct (non-batched) writes per loop pass +static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3; + bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); - return this->client_->send_message(resp); + if (!this->client_->send_message(resp)) + return false; + // at_ is this service's index + if ((this->at_ + 1) % SERVICE_YIELD_INTERVAL == 0) + this->yield_after_step_(); + return true; } #endif diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py index 70015c53b9..bd02d22d1b 100644 --- a/esphome/components/as3935/__init__.py +++ b/esphome/components/as3935/__init__.py @@ -14,6 +14,8 @@ from esphome.const import ( CONF_TUNE_ANTENNA, CONF_WATCHDOG_THRESHOLD, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType MULTI_CONF = True @@ -42,7 +44,7 @@ AS3935_SCHEMA = cv.Schema( ) -async def setup_as3935(var, config): +async def setup_as3935(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) diff --git a/esphome/components/as3935/binary_sensor.py b/esphome/components/as3935/binary_sensor.py index 10004e69dc..929b653294 100644 --- a/esphome/components/as3935/binary_sensor.py +++ b/esphome/components/as3935/binary_sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv +from esphome.types import ConfigType from . import AS3935, CONF_AS3935_ID @@ -13,7 +14,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_AS3935_ID]) var = await binary_sensor.new_binary_sensor(config) cg.add(hub.set_thunder_alert_binary_sensor(var)) diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index 9b43155563..b727b8fdb9 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_KILOMETER, ) +from esphome.types import ConfigType from . import AS3935, CONF_AS3935_ID @@ -31,7 +32,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_AS3935_ID]) if distance_config := config.get(CONF_DISTANCE): diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py index c05e556376..780712c3bd 100644 --- a/esphome/components/as5600/__init__.py +++ b/esphome/components/as5600/__init__.py @@ -1,3 +1,6 @@ +from collections.abc import Callable +from typing import Any + from esphome import pins import esphome.codegen as cg from esphome.components import i2c @@ -11,6 +14,7 @@ from esphome.const import ( CONF_RANGE, CONF_WATCHDOG, ) +from esphome.types import ConfigType CODEOWNERS = ["@ammmze"] DEPENDENCIES = ["i2c"] @@ -72,13 +76,13 @@ POSITION_TO_ANGLE = 360 / RESOLUTION MIN_RANGE = round(18 * ANGLE_TO_POSITION) -def angle(min=-360, max=360): +def angle(min: float = -360, max: float = 360) -> Callable[[Any], Any]: return cv.All( cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max) ) -def angle_to_position(value, min=-360, max=360): +def angle_to_position(value: Any, min: float = -360, max: float = 360) -> int: try: value = angle(min=min, max=max)(value) return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION @@ -86,17 +90,17 @@ def angle_to_position(value, min=-360, max=360): raise cv.Invalid(f"When using angle, {e.error_message}") from e -def percent_to_position(value): +def percent_to_position(value: Any) -> int: value = cv.possibly_negative_percentage(value) return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION -def position(min=-MAX_POSITION, max=MAX_POSITION): +def position(min: int = -MAX_POSITION, max: int = MAX_POSITION) -> Callable[[Any], Any]: """Validate that the config option is a position. Accepts integers, degrees, or percentage (of 360 degrees). """ - def validator(value): + def validator(value: Any) -> int: if isinstance(value, str) and value.endswith("%"): value = percent_to_position(value) @@ -112,7 +116,7 @@ def position(min=-MAX_POSITION, max=MAX_POSITION): return validator -def position_range(): +def position_range() -> Callable[[Any], Any]: """Validate that value given is a valid range for the device. A valid range is one of the following: - a value of 0 (meaning full range) @@ -129,7 +133,7 @@ def position_range(): zero_validator, ) - def validator(value): + def validator(value: Any) -> Any: is_negative_str = isinstance(value, str) and value.startswith("-") is_negative_num = isinstance(value, (float, int)) and value < 0 if is_negative_str or is_negative_num: @@ -139,13 +143,13 @@ def position_range(): return validator -def has_valid_range_config(): +def has_valid_range_config() -> Callable[[ConfigType], ConfigType]: """Validate that that the config start + end position results in a valid positional range, which must be >= 18degrees """ range_validator = position_range() - def validator(config): + def validator(config: ConfigType) -> ConfigType: # if we don't have an end position, then there is nothing to do if CONF_END_POSITION not in config: return config @@ -203,7 +207,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index cf67a3f203..847b89f121 100644 --- a/esphome/components/as5600/sensor/__init__.py +++ b/esphome/components/as5600/sensor/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( ICON_ROTATE_RIGHT, STATE_CLASS_MEASUREMENT, ) +from esphome.types import ConfigType from .. import AS5600Component, as5600_ns @@ -77,7 +78,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_AS5600_ID]) await cg.register_component(var, config) diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 5031b72cce..193e62f615 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -4,6 +4,9 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv from esphome.const import CONF_FREQUENCY, CONF_ID +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@X-Ryl669"] DEPENDENCIES = ["i2c"] @@ -70,7 +73,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) @@ -91,7 +94,12 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio ), synchronous=True, ) -async def at581x_reset_to_code(config, action_id, template_arg, args): +async def at581x_reset_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -163,7 +171,12 @@ RADAR_SETTINGS_SCHEMA = cv.Schema( RADAR_SETTINGS_SCHEMA, synchronous=True, ) -async def at581x_settings_to_code(config, action_id, template_arg, args): +async def at581x_settings_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/at581x/switch/__init__.py b/esphome/components/at581x/switch/__init__.py index 8e1b82b356..7e45ed89ec 100644 --- a/esphome/components/at581x/switch/__init__.py +++ b/esphome/components/at581x/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import DEVICE_CLASS_SWITCH, ICON_WIFI +from esphome.types import ConfigType from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns @@ -22,7 +23,7 @@ CONFIG_SCHEMA = switch.switch_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: at581x_component = await cg.get_variable(config[CONF_AT581X_ID]) s = await switch.new_switch(config) await cg.register_parented(s, config[CONF_AT581X_ID]) diff --git a/esphome/components/atm90e32/button/__init__.py b/esphome/components/atm90e32/button/__init__.py index 19f62ccfbd..274cce6adb 100644 --- a/esphome/components/atm90e32/button/__init__.py +++ b/esphome/components/atm90e32/button/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import button import esphome.config_validation as cv from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE +from esphome.types import ConfigType from .. import atm90e32_ns from ..sensor import ATM90E32Component @@ -67,7 +68,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_ID]) if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION): diff --git a/esphome/components/atm90e32/number/__init__.py b/esphome/components/atm90e32/number/__init__.py index 848680b875..9c2865dde3 100644 --- a/esphome/components/atm90e32/number/__init__.py +++ b/esphome/components/atm90e32/number/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( UNIT_AMPERE, UNIT_VOLT, ) +from esphome.types import ConfigType from .. import atm90e32_ns from ..sensor import ATM90E32Component @@ -90,7 +91,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_ID]) if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE): diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index dc46138add..38b24c7cf6 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -41,6 +41,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.types import ConfigType from . import atm90e32_ns @@ -191,7 +192,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_instance_id(str(config[CONF_ID]))) await cg.register_component(var, config) diff --git a/esphome/components/atm90e32/text_sensor/__init__.py b/esphome/components/atm90e32/text_sensor/__init__.py index ab96f6c207..30585cb873 100644 --- a/esphome/components/atm90e32/text_sensor/__init__.py +++ b/esphome/components/atm90e32/text_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C +from esphome.types import ConfigType from ..sensor import ATM90E32Component @@ -34,7 +35,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_ID]) if phase_cfg := config.get(CONF_PHASE_STATUS): diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 1c522cbb5d..277df0506a 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,4 +1,6 @@ +from collections.abc import Callable from dataclasses import dataclass, field +from typing import Any import esphome.codegen as cg from esphome.components.esp32 import ( @@ -15,6 +17,7 @@ from esphome.const import ( ) from esphome.core import CORE import esphome.final_validate as fv +from esphome.types import ConfigType AUTO_LOAD = ["ring_buffer"] CODEOWNERS = ["@kahrendt"] @@ -125,10 +128,10 @@ CONF_THREADSAFE = "threadsafe" _MEMORY_LOCATION_VALIDATOR = cv.one_of(*MEMORY_LOCATIONS, lower=True) -def _maybe_empty_codec(schema): +def _maybe_empty_codec(schema: cv.Schema) -> Callable[[Any], Any]: """Wrap a codec dict schema so that a bare key (None value) is treated as an empty dict.""" - def validator(value): + def validator(value: Any) -> Any: if value is None: value = {} return schema(value) @@ -200,14 +203,14 @@ def set_stream_limits( max_channels: int = cv.UNDEFINED, min_sample_rate: int = cv.UNDEFINED, max_sample_rate: int = cv.UNDEFINED, -): +) -> Callable[[ConfigType], None]: """Sets the limits for the audio stream that audio component can handle When the component sinks audio (e.g., a speaker), these indicate the limits to the audio it can receive. When the component sources audio (e.g., a microphone), these indicate the limits to the audio it can send. """ - def set_limits_in_config(config): + def set_limits_in_config(config: ConfigType) -> None: if min_bits_per_sample is not cv.UNDEFINED: config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample if max_bits_per_sample is not cv.UNDEFINED: @@ -233,7 +236,7 @@ def final_validate_audio_schema( sample_rate: int = cv.UNDEFINED, enabled_channels: list[int] = cv.UNDEFINED, audio_device_issue: bool = False, -): +) -> cv.Schema: """Validates audio compatibility when passed between different components. The component derived from ``AUDIO_COMPONENT_SCHEMA`` should call ``set_stream_limits`` in a validator to specify its compatible settings @@ -251,7 +254,7 @@ def final_validate_audio_schema( audio_device_issue (bool, optional): Format the error message to indicate the problem is in the configuration for the ``audio_device`` component. Defaults to False. """ - def validate_audio_compatiblity(audio_config): + def validate_audio_compatiblity(audio_config: ConfigType) -> ConfigType: audio_schema = {} if bits_per_sample is not cv.UNDEFINED: @@ -329,7 +332,7 @@ def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> N add_idf_sdkconfig_option(internal_key, True) -async def to_code(config): +async def to_code(config: ConfigType) -> None: # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) include_builtin_idf_component("esp_http_client") diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index b0aa3c1abb..402e741059 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -86,7 +86,7 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url) // Match "audio/ogg" with a codecs parameter containing "opus" // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. // Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis) - if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { + if (strncasecmp(content_type, "audio/ogg", 9) == 0 && str_contains_ignore_case(content_type + 9, "opus")) { return AudioFileType::OPUS; } #endif diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py index 3c3a4988b5..c2bdfb6cb0 100644 --- a/esphome/components/audio_adc/__init__.py +++ b/esphome/components/audio_adc/__init__.py @@ -2,7 +2,9 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MIC_GAIN -from esphome.core import CoroPriority, coroutine_with_priority +from esphome.core import ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] IS_PLATFORM_COMPONENT = True @@ -28,7 +30,12 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value( SET_MIC_GAIN_ACTION_SCHEMA, synchronous=True, ) -async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): +async def audio_adc_set_mic_gain_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -39,6 +46,6 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_AUDIO_ADC") cg.add_global(audio_adc_ns.using) diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py index 46c277ce51..1351793afd 100644 --- a/esphome/components/audio_dac/__init__.py +++ b/esphome/components/audio_dac/__init__.py @@ -3,7 +3,9 @@ from esphome.automation import maybe_simple_id import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_VOLUME -from esphome.core import CoroPriority, coroutine_with_priority +from esphome.core import ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] IS_PLATFORM_COMPONENT = True @@ -37,7 +39,12 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( "audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True ) -async def audio_dac_mute_action_to_code(config, action_id, template_arg, args): +async def audio_dac_mute_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -48,7 +55,12 @@ async def audio_dac_mute_action_to_code(config, action_id, template_arg, args): SET_VOLUME_ACTION_SCHEMA, synchronous=True, ) -async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): +async def audio_dac_set_volume_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -59,6 +71,6 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_AUDIO_DAC") cg.add_global(audio_dac_ns.using) diff --git a/esphome/components/bedjet/__init__.py b/esphome/components/bedjet/__init__.py index d4bf813846..1b967e665a 100644 --- a/esphome/components/bedjet/__init__.py +++ b/esphome/components/bedjet/__init__.py @@ -2,6 +2,8 @@ import esphome.codegen as cg from esphome.components import ble_client, time import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_TIME_ID +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@jhansche"] DEPENDENCIES = ["ble_client"] @@ -32,12 +34,12 @@ BEDJET_CLIENT_SCHEMA = cv.Schema( ) -async def register_bedjet_child(var, config): +async def register_bedjet_child(var: MockObj, config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_BEDJET_ID]) cg.add(parent.register_child(var)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await ble_client.register_ble_node(var, config) diff --git a/esphome/components/bedjet/climate/__init__.py b/esphome/components/bedjet/climate/__init__.py index 4de9dcca0b..36650d643c 100644 --- a/esphome/components/bedjet/climate/__init__.py +++ b/esphome/components/bedjet/climate/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import climate import esphome.config_validation as cv from esphome.const import CONF_HEAT_MODE, CONF_TEMPERATURE_SOURCE +from esphome.types import ConfigType from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child @@ -37,7 +38,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await climate.new_climate(config) await cg.register_component(var, config) await register_bedjet_child(var, config) diff --git a/esphome/components/bedjet/fan/__init__.py b/esphome/components/bedjet/fan/__init__.py index a4a611fefc..f5dfe32f4c 100644 --- a/esphome/components/bedjet/fan/__init__.py +++ b/esphome/components/bedjet/fan/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv +from esphome.types import ConfigType from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child @@ -16,7 +17,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await fan.new_fan(config) await cg.register_component(var, config) await register_bedjet_child(var, config) diff --git a/esphome/components/bedjet/sensor/__init__.py b/esphome/components/bedjet/sensor/__init__.py index fa9ca7953e..595e798e49 100644 --- a/esphome/components/bedjet/sensor/__init__.py +++ b/esphome/components/bedjet/sensor/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) +from esphome.types import ConfigType from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child @@ -38,7 +39,7 @@ CONFIG_SCHEMA = cv.Schema( ).extend(BEDJET_CLIENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await register_bedjet_child(var, config) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 5800e0bd9e..1ab6f7103f 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -5,6 +5,7 @@ from esphome.automation import Condition, maybe_simple_id import esphome.codegen as cg from esphome.components import mqtt, web_server, zigbee from esphome.components.const import CONF_ON_STATE_CHANGE +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_DELAY, @@ -560,6 +561,11 @@ _CALLBACK_AUTOMATIONS = ( async def _build_binary_sensor_automations(var, config): await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) + if config.get(CONF_ON_CLICK) or config.get(CONF_ON_DOUBLE_CLICK): + cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER") + if config.get(CONF_ON_MULTI_CLICK): + cg.add_define("USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER") + for conf in config.get(CONF_ON_CLICK, []): trigger = cg.new_Pvariable( conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH] @@ -673,3 +679,15 @@ async def to_code(config): async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) + + +# automation.cpp only implements the click/double_click/multi_click triggers +FILTER_SOURCE_FILES = filter_source_files_from_defines( + { + "automation.cpp": ( + "USE_BINARY_SENSOR_CLICK_TRIGGER", + "USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER", + ), + "filter.cpp": "USE_BINARY_SENSOR_FILTER", + } +) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index b13e4a88dd..1a3c1f7536 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -1,8 +1,13 @@ +#include "esphome/core/defines.h" +#if defined(USE_BINARY_SENSOR_CLICK_TRIGGER) || defined(USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER) + #include "automation.h" #include "esphome/core/log.h" namespace esphome::binary_sensor { +#ifdef USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER + static const char *const TAG = "binary_sensor.automation"; // MultiClickTrigger timeout IDs. @@ -120,6 +125,9 @@ void MultiClickTriggerBase::trigger_() { this->trigger(); } +#endif // USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER + +#ifdef USE_BINARY_SENSOR_CLICK_TRIGGER bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) { if (max_length == 0) { return length >= min_length; @@ -127,4 +135,8 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) { return length >= min_length && length <= max_length; } } +#endif // USE_BINARY_SENSOR_CLICK_TRIGGER + } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_CLICK_TRIGGER || USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER diff --git a/esphome/components/bk72xx_ble/__init__.py b/esphome/components/bk72xx_ble/__init__.py index 81073c9b02..74b9cb5954 100644 --- a/esphome/components/bk72xx_ble/__init__.py +++ b/esphome/components/bk72xx_ble/__init__.py @@ -4,9 +4,12 @@ The platform analog of esp32_ble / rp2040_ble: owns the Beken BDK BLE stack bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build on this component and contain no SDK calls of their own. -Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253 -(BLE 5.2), and any future BLE-5.x SoC. Known non-5.x families are rejected in -to_code; unknown families are capability-checked at compile time via +Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7252N/BK7253 (BLE 5.2), +and any future BLE-5.x SoC. BK7238 (BLE 5.2) is blocked for now: with BLE +compiled in, the Beken SDK erases the bootloader flash sector at boot because +LibreTiny's partition table has no BLE bonding entry (esphome#18646, +libretiny-eu/libretiny#408). Known non-5.x families and BK7238 are rejected in +to_code. Unknown families are capability-checked at compile time via `__has_include("app_ble.h")`, a header only on the BLE 5.x include path (ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build fails with a clear #error. @@ -65,6 +68,14 @@ def _unsupported_family_message(family: str) -> str | None: ) if family == FAMILY_BK7231Q: return "bk72xx_ble does not support BK7231Q: this SoC has no BLE" + if family == FAMILY_BK7238: + return ( + "bk72xx_ble is disabled on BK7238: with BLE compiled in, the Beken SDK " + "erases the bootloader flash sector at boot and the device can no longer " + "start (see https://github.com/esphome/esphome/issues/18646); support " + "returns once the LibreTiny partition table fix " + "(libretiny-eu/libretiny#408) is released" + ) return None @@ -113,18 +124,7 @@ async def to_code(config: ConfigType) -> None: # BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is # derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++ # which path is available so it doesn't reference a missing symbol. - family = libretiny.get_libretiny_family() - if family == FAMILY_BK7231N: + if libretiny.get_libretiny_family() == FAMILY_BK7231N: cg.add_define("BK72XX_BLE_HAS_COMMON_BDADDR") - elif family == FAMILY_BK7238: - # ESPHome's LibreTiny disables BLE on BK7238 because the SDK can hang at - # WiFi STA startup when BLE init runs. This component re-enables BLE, so - # warn loudly: BK7238 is accepted but not hardware-verified and may be - # WiFi-unstable with BLE on. - _LOGGER.warning( - "bk72xx_ble on BK7238: enabling BLE is known to risk a WiFi STA startup " - "hang on this family and is not yet hardware-verified. Expect possible " - "instability." - ) cg.add_define("USE_BK72XX_BLE") diff --git a/esphome/components/bl0906/sensor.py b/esphome/components/bl0906/sensor.py index 059e10e962..1a0c2287ab 100644 --- a/esphome/components/bl0906/sensor.py +++ b/esphome/components/bl0906/sensor.py @@ -32,6 +32,9 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType # Import ICONS not included in esphome's const.py, from the local components const.py from .const import ICON_ENERGY, ICON_FREQUENCY, ICON_VOLTAGE @@ -145,13 +148,18 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ), synchronous=True, ) -async def reset_energy_to_code(config, action_id, template_arg, args): +async def reset_energy_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/bl0940/button/__init__.py b/esphome/components/bl0940/button/__init__.py index 04d11e6e30..e87a647392 100644 --- a/esphome/components/bl0940/button/__init__.py +++ b/esphome/components/bl0940/button/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import button import esphome.config_validation as cv from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART +from esphome.types import ConfigType from .. import CONF_BL0940_ID, bl0940_ns from ..sensor import BL0940 @@ -21,7 +22,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await button.new_button(config) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_BL0940_ID]) diff --git a/esphome/components/bl0940/number/__init__.py b/esphome/components/bl0940/number/__init__.py index 92ab2837b3..b5a66e682a 100644 --- a/esphome/components/bl0940/number/__init__.py +++ b/esphome/components/bl0940/number/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, UNIT_PERCENT, ) +from esphome.types import ConfigType from .. import CONF_BL0940_ID, bl0940_ns from ..sensor import BL0940 @@ -27,7 +28,7 @@ CalibrationNumber = bl0940_ns.class_( ) -def validate_min_max(config): +def validate_min_max(config: ConfigType) -> ConfigType: if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: raise cv.Invalid("max_value must be greater than min_value") return config @@ -69,7 +70,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: # Get the BL0940 component instance bl0940 = await cg.get_variable(config[CONF_BL0940_ID]) diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index 96445d5c38..7e6403c3bc 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -23,6 +23,7 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, ) +from esphome.types import ConfigType from . import bl0940_ns @@ -69,27 +70,29 @@ DEFAULT_BL0940_LEGACY_EREF = 3.6e6 / 297 # methods to calculate voltage and current reference values -def calculate_voltage_reference(vref, r_one, r_two): +def calculate_voltage_reference(vref: float, r_one: float, r_two: float) -> float: # formula: 79931 / Vref * (R1 * 1000) / (R1 + R2) return 79931 / vref * (r_one * 1000) / (r_one + r_two) -def calculate_current_reference(vref, r_shunt): +def calculate_current_reference(vref: float, r_shunt: float) -> float: # formula: 324004 * RL / Vref return 324004 * r_shunt / vref -def calculate_power_reference(voltage_reference, current_reference): +def calculate_power_reference( + voltage_reference: float, current_reference: float +) -> float: # calculate power reference based on voltage and current reference return voltage_reference * current_reference * 4046 / 324004 / 79931 -def calculate_energy_reference(power_reference): +def calculate_energy_reference(power_reference: float) -> float: # formula: power_reference * 3600000 / (1638.4 * 256) return power_reference * 3600000 / (1638.4 * 256) -def validate_legacy_mode(config): +def validate_legacy_mode(config: ConfigType) -> ConfigType: # Only allow schematic calibration options if legacy_mode is False if config.get(CONF_LEGACY_MODE, True): forbidden = [ @@ -106,7 +109,7 @@ def validate_legacy_mode(config): return config -def set_command_defaults(config): +def set_command_defaults(config: ConfigType) -> ConfigType: # Set defaults for read_command and write_command based on legacy_mode legacy = config.get(CONF_LEGACY_MODE, True) if legacy: @@ -118,7 +121,7 @@ def set_command_defaults(config): return config -def set_reference_values(config): +def set_reference_values(config: ConfigType) -> ConfigType: # Set default reference values based on legacy_mode if config.get(CONF_LEGACY_MODE, True): config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) @@ -223,7 +226,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/ble_device_base/__init__.py b/esphome/components/ble_device_base/__init__.py index 15a8b08139..43ec736727 100644 --- a/esphome/components/ble_device_base/__init__.py +++ b/esphome/components/ble_device_base/__init__.py @@ -206,32 +206,36 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType: interval = config[CONF_INTERVAL] window = config[CONF_WINDOW] - if window > interval: - raise cv.Invalid( - f"Scan window ({window}) needs to be smaller than scan interval ({interval})" - ) + # Labels are reused in every error below; the optional one names its key. + windows = [("Scan window", window)] + if (connection_window := config.get(CONF_CONNECTION_SCAN_WINDOW)) is not None: + windows.append((CONF_CONNECTION_SCAN_WINDOW, connection_window)) + + for name, value in windows: + if value > interval: + raise cv.Invalid( + f"{name} ({value}) needs to be smaller than scan interval ({interval})" + ) # BLE scan interval/window are programmed in 0.625 ms units as a 16-bit value; the # controller only accepts 2.5 ms .. 10240 ms (0x0004 .. 0x4000). Reject out-of-range # values here instead of letting the unit conversion silently overflow. - for name, value in (("interval", interval), ("window", window)): + for name, value in (("Scan interval", interval), *windows): if value.total_microseconds < 2500 or value.total_microseconds > 10_240_000: - raise cv.Invalid( - f"Scan {name} ({value}) must be between 2.5 ms and 10240 ms" - ) + raise cv.Invalid(f"{name} ({value}) must be between 2.5 ms and 10240 ms") # Validate what actually reaches the controller: both values are truncated to # whole 0.625 ms units, so a window/interval pair that differs by less than one # unit collapses to the same value — silently programming a 100 % duty cycle # (radio permanently on) from a config that asked for less. interval_units = to_ble_units(interval) - window_units = to_ble_units(window) - if window_units == interval_units and window < interval: - raise cv.Invalid( - f"Scan window ({window}) and interval ({interval}) both truncate to " - f"{interval_units} x 0.625 ms, which the controller scans at a 100 % duty " - f"cycle. Separate them by at least 0.625 ms." - ) + for name, value in windows: + if to_ble_units(value) == interval_units and value < interval: + raise cv.Invalid( + f"{name} ({value}) and interval ({interval}) both truncate to " + f"{interval_units} x 0.625 ms, which the controller scans at a 100 % duty " + f"cycle. Separate them by at least 0.625 ms." + ) if interval.total_microseconds * 3 > duration.total_microseconds: raise cv.Invalid( @@ -247,11 +251,14 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType: # their own; also the fallback for esp32's conditional default. DEFAULT_SCAN_WINDOW = "30ms" +CONF_CONNECTION_SCAN_WINDOW = "connection_scan_window" + def scan_parameters_schema( interval_default: str, *, window_default: str | Callable[[], TimePeriod] = DEFAULT_SCAN_WINDOW, + connection_window: bool = False, ) -> cv.All: """Build the scan_parameters value schema shared by all BLE trackers. @@ -263,7 +270,9 @@ def scan_parameters_schema( can adjust it once sibling keys are resolved). The `active` option (default on) is unconditional: active scanning is part of the tracker contract — every current proxy client assumes it, so a passive-only - tracker must not share this schema. + tracker must not share this schema. connection_window opts in to the + `connection_scan_window` option for trackers that can fall back to a + smaller window while a GATT connection is active. """ schema = { cv.Optional(CONF_DURATION, default="5min"): cv.positive_time_period_seconds, @@ -272,6 +281,8 @@ def scan_parameters_schema( cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean, cv.Optional(CONF_ACTIVE, default=True): cv.boolean, } + if connection_window: + schema[cv.Optional(CONF_CONNECTION_SCAN_WINDOW)] = cv.positive_time_period return cv.All(cv.Schema(schema), validate_scan_parameters) diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp index c8f97f207e..ddab4812cc 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp @@ -275,10 +275,15 @@ void BluetoothConnection::send_ack_(PendingAck kind, uint16_t handle, conn_err_t if (this->try_send_ack_(kind, handle, error)) return; // Report a newly owed reply and a displaced one; displacing is the case - // that loses a reply. Re-refusing the same one stays quiet. + // that loses a reply. Re-refusing the same one stays quiet, and so does a + // fresh deferral for the handle already warned about: a congested bulk + // transfer re-asks the same handle every cycle and each ack would warn. if (!this->has_pending_ack_()) { - ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X deferred, TCP buffer full", this->connection_index_, - this->address_str_, handle); + if (!this->ack_deferred_warned_ || this->pending_ack_handle_ != handle) { + ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X deferred, TCP buffer full", this->connection_index_, + this->address_str_, handle); + this->ack_deferred_warned_ = true; + } } else if (this->pending_ack_handle_ != handle || this->pending_ack_ != kind) { ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X dropped for handle 0x%04X", this->connection_index_, this->address_str_, this->pending_ack_handle_, handle); @@ -365,8 +370,16 @@ void BluetoothConnection::on_notify_data(uint16_t handle, const uint8_t *data, u resp.set_data(data, len); if (!api_connection->send_message(resp)) { // Not latched, same reason as the read reply. Notify data is lossy: the - // peripheral will not resend it. - ESP_LOGW(TAG, "[%d] [%s] Failed to send notify data response", this->connection_index_, this->address_str_); + // peripheral will not resend it. Warn on the first drop only; a congested + // link drops a whole stream and one line per notify floods the log. + if (!this->notify_drop_warned_) { + ESP_LOGW(TAG, "[%d] [%s] Failed to send notify data response, handle 0x%04X", this->connection_index_, + this->address_str_, handle); + this->notify_drop_warned_ = true; + } else { + ESP_LOGV(TAG, "[%d] [%s] Failed to send notify data response, handle 0x%04X", this->connection_index_, + this->address_str_, handle); + } } } diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h index f87d545f7d..47181e81a7 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h @@ -164,6 +164,8 @@ class BluetoothConnection final : public ble_device_base::GattClientListener { this->pending_ack_ = PendingAck::PENDING_ACK_NONE; this->batch_stalled_ = false; this->connected_reply_owed_ = false; + this->ack_deferred_warned_ = false; + this->notify_drop_warned_ = false; } /// Sole construction site for these replies, shared by send and retry. bool try_send_ack_(PendingAck kind, uint16_t handle, conn_err_t error); @@ -238,7 +240,7 @@ class BluetoothConnection final : public ble_device_base::GattClientListener { // Group 5: bit-packed tail. The first two bytes were already full, so the // first added bit forced a third and took the 8-aligned object 48 -> 56; - // the handle, error and retry counter ride in that padding. Four bitfield + // the handle, error and retry counter ride in that padding. Two bitfield // bits left; another byte-sized member costs 8 per slot. static_assert(static_cast(ClientState::ESTABLISHED) < (1 << 3), "state_ bitfield too narrow"); static_assert(static_cast(ConnectionType::V3_WITHOUT_CACHE) < (1 << 2), @@ -258,6 +260,11 @@ class BluetoothConnection final : public ble_device_base::GattClientListener { bool batch_stalled_ : 1 {false}; /// An owed connected=true reply; the proxy's paced drain re-offers it. bool connected_reply_owed_ : 1 {false}; + /// Set once the deferred warn fired; with an unchanged pending_ack_handle_ + /// it keeps re-deferrals of the same handle quiet (see send_ack_). + bool ack_deferred_warned_ : 1 {false}; + /// Set on the first dropped notify; later drops log at verbose only. + bool notify_drop_warned_ : 1 {false}; // Plain byte after the bitfields: takes the padding byte instead of // straddling pending_ack_'s storage unit and growing the object. static_assert(PENDING_ACK_RETRY_LIMIT <= 0xFF, "retry counter too narrow"); diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py index ba264f00bf..5ef162bb7c 100644 --- a/esphome/components/bm8563/time.py +++ b/esphome/components/bm8563/time.py @@ -3,6 +3,9 @@ import esphome.codegen as cg from esphome.components import i2c, time import esphome.config_validation as cv from esphome.const import CONF_DURATION, CONF_ID +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -35,7 +38,12 @@ CONFIG_SCHEMA = ( ), synchronous=True, ) -async def bm8563_write_time_to_code(config, action_id, template_arg, args): +async def bm8563_write_time_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -52,7 +60,12 @@ async def bm8563_write_time_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def bm8563_start_timer_to_code(config, action_id, template_arg, args): +async def bm8563_start_timer_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) @@ -70,13 +83,18 @@ async def bm8563_start_timer_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def bm8563_read_time_to_code(config, action_id, template_arg, args): +async def bm8563_read_time_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index e1e01facd0..35df2a7ea3 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -3,6 +3,7 @@ from esphome.components import esp32, i2c from esphome.components.const import CONF_STATE_SAVE_INTERVAL import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework +from esphome.types import ConfigType CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] @@ -76,7 +77,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py index bdc8d8f2d3..153890b57f 100644 --- a/esphome/components/bme680_bsec/sensor.py +++ b/esphome/components/bme680_bsec/sensor.py @@ -29,6 +29,8 @@ from esphome.const import ( UNIT_PARTS_PER_MILLION, UNIT_PERCENT, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_BME680_BSEC_ID, SAMPLE_RATE_OPTIONS, BME680BSECComponent @@ -110,7 +112,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def setup_conf(config, key, hub): +async def setup_conf(config: ConfigType, key: str, hub: MockObj) -> None: if sensor_config := config.get(key): sens = await sensor.new_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_sensor")(sens)) @@ -120,7 +122,7 @@ async def setup_conf(config, key, hub): ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_BME680_BSEC_ID]) for key in TYPES: await setup_conf(config, key, hub) diff --git a/esphome/components/bme680_bsec/text_sensor.py b/esphome/components/bme680_bsec/text_sensor.py index 1fbb9e2aeb..6da1c9d287 100644 --- a/esphome/components/bme680_bsec/text_sensor.py +++ b/esphome/components/bme680_bsec/text_sensor.py @@ -2,6 +2,8 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_IAQ_ACCURACY +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_BME680_BSEC_ID, BME680BSECComponent @@ -21,13 +23,13 @@ CONFIG_SCHEMA = cv.Schema( ) -async def setup_conf(config, key, hub): +async def setup_conf(config: ConfigType, key: str, hub: MockObj) -> None: if sensor_config := config.get(key): sens = await text_sensor.new_text_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_BME680_BSEC_ID]) for key in TYPES: await setup_conf(config, key, hub) diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index c12eb39d2d..8208672b6a 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, ) +from esphome.cpp_generator import MockObj from esphome.external_files import RemoteFile from esphome.types import ConfigType @@ -94,7 +95,7 @@ def _compute_url(config: dict) -> str: return f"https://raw.githubusercontent.com/boschsensortec/Bosch-BSEC2-Library/{BSEC2_LIBRARY_VERSION}/src/config/{model}/{model}_{algo}_{volts}_{sample_rate}_{operating_age}/{filename}.txt" -def download_bme68x_blob(config): +def download_bme68x_blob(config: ConfigType) -> ConfigType: url = _compute_url(config) path = _compute_local_file_path(url) external_files.download_content(url, path) @@ -138,7 +139,7 @@ def _extract_blob_ref(entry: ConfigType) -> RemoteFile | None: PREFETCH_FILES = external_files.single_stage_prefetch(_extract_blob_ref) -def validate_bme68x(config): +def validate_bme68x(config: ConfigType) -> ConfigType: if CONF_ALGORITHM_OUTPUT not in config: return config @@ -178,7 +179,7 @@ CONFIG_SCHEMA_BASE = ( ) -async def to_code_base(config): +async def to_code_base(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/bme68x_bsec2/sensor.py b/esphome/components/bme68x_bsec2/sensor.py index 52587dba99..863cd9d601 100644 --- a/esphome/components/bme68x_bsec2/sensor.py +++ b/esphome/components/bme68x_bsec2/sensor.py @@ -29,6 +29,8 @@ from esphome.const import ( UNIT_PARTS_PER_MILLION, UNIT_PERCENT, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_BME68X_BSEC2_ID, SAMPLE_RATE_OPTIONS, BME68xBSEC2Component @@ -119,7 +121,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def setup_conf(config, key, hub): +async def setup_conf(config: ConfigType, key: str, hub: MockObj) -> None: if conf := config.get(key): sens = await sensor.new_sensor(conf) cg.add(getattr(hub, f"set_{key}_sensor")(sens)) @@ -127,7 +129,7 @@ async def setup_conf(config, key, hub): cg.add(getattr(hub, f"set_{key}_sample_rate")(sample_rate)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) for key in TYPES: await setup_conf(config, key, hub) diff --git a/esphome/components/bme68x_bsec2/text_sensor.py b/esphome/components/bme68x_bsec2/text_sensor.py index fce00afe34..5c6f9f696c 100644 --- a/esphome/components/bme68x_bsec2/text_sensor.py +++ b/esphome/components/bme68x_bsec2/text_sensor.py @@ -2,6 +2,8 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_IAQ_ACCURACY +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_BME68X_BSEC2_ID, BME68xBSEC2Component @@ -21,13 +23,13 @@ CONFIG_SCHEMA = cv.Schema( ) -async def setup_conf(config, key, hub): +async def setup_conf(config: ConfigType, key: str, hub: MockObj) -> None: if conf := config.get(key): sens = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) for key in TYPES: await setup_conf(config, key, hub) diff --git a/esphome/components/bthome_mithermometer/__init__.py b/esphome/components/bthome_mithermometer/__init__.py index 4be7ca8268..ed0cbaa9e1 100644 --- a/esphome/components/bthome_mithermometer/__init__.py +++ b/esphome/components/bthome_mithermometer/__init__.py @@ -3,6 +3,8 @@ from esphome.components import ble_device_base import esphome.config_validation as cv from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS from esphome.core import HexInt +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@nagyrobi"] AUTO_LOAD = ["ble_device_base"] @@ -14,7 +16,9 @@ BTHomeMiThermometer = bthome_mithermometer_ns.class_( ) -def bthome_mithermometer_base_schema(extra_schema=None): +def bthome_mithermometer_base_schema( + extra_schema: cv.Schema | dict | None = None, +) -> cv.All: if extra_schema is None: extra_schema = {} return cv.All( @@ -32,7 +36,7 @@ def bthome_mithermometer_base_schema(extra_schema=None): ) -async def setup_bthome_mithermometer(var, config): +async def setup_bthome_mithermometer(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) await ble_device_base.register_ble_device(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) diff --git a/esphome/components/bthome_mithermometer/sensor.py b/esphome/components/bthome_mithermometer/sensor.py index 02551391ad..f559d0aa9b 100644 --- a/esphome/components/bthome_mithermometer/sensor.py +++ b/esphome/components/bthome_mithermometer/sensor.py @@ -20,6 +20,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_VOLT, ) +from esphome.types import ConfigType from . import bthome_mithermometer_base_schema, setup_bthome_mithermometer @@ -67,7 +68,7 @@ CONFIG_SCHEMA = bthome_mithermometer_base_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await setup_bthome_mithermometer(var, config) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index a4245f43e6..ee24002b8a 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -16,14 +16,15 @@ from esphome.const import ( DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_device_class, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -88,7 +89,7 @@ _CALLBACK_AUTOMATIONS = ( @setup_entity("button") -async def setup_button_core_(var, config): +async def setup_button_core_(var: MockObj, config: ConfigType) -> None: await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) setup_device_class(config) @@ -101,7 +102,7 @@ async def setup_button_core_(var, config): await web_server.add_entity_config(var, web_server_config) -async def register_button(var, config): +async def register_button(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) queue_entity_register("button", config) @@ -109,7 +110,7 @@ async def register_button(var, config): await setup_button_core_(var, config) -async def new_button(config, *args): +async def new_button(config: ConfigType, *args: SafeExpType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID], *args) await register_button(var, config) return var @@ -125,11 +126,16 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id( @automation.register_action( "button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True ) -async def button_press_to_code(config, action_id, template_arg, args): +async def button_press_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(button_ns.using) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index fcd342ad38..b7de235dd1 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -1,10 +1,13 @@ import re +from typing import Any from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@mvturnho", "@danielschramm"] IS_PLATFORM_COMPONENT = True @@ -18,7 +21,7 @@ CONF_BIT_RATE = "bit_rate" CONF_ON_FRAME = "on_frame" -def validate_id(config): +def validate_id(config: ConfigType) -> ConfigType: if CONF_CAN_ID in config: can_id = config[CONF_CAN_ID] id_ext = config[CONF_USE_EXTENDED_ID] @@ -27,7 +30,7 @@ def validate_id(config): return config -def validate_raw_data(value): +def validate_raw_data(value: Any) -> bytes | list: if isinstance(value, str): return value.encode("utf-8") if isinstance(value, list): @@ -71,7 +74,7 @@ CAN_SPEEDS = { } -def get_rate(value): +def get_rate(value: str) -> int: match = re.match(r"(\d+)(?:K(\d+)?)?BPS", value, re.IGNORECASE) if not match: raise ValueError(f"Invalid rate format: {value}") @@ -103,7 +106,7 @@ CANBUS_SCHEMA = cv.Schema( CANBUS_SCHEMA.add_extra(validate_id) -async def setup_canbus_core_(var, config): +async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) cg.add(var.set_can_id([config[CONF_CAN_ID]])) cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]])) @@ -134,7 +137,7 @@ async def setup_canbus_core_(var, config): ) -async def register_canbus(var, config): +async def register_canbus(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.new_Pvariable(config[CONF_ID], var) await setup_canbus_core_(var, config) @@ -157,7 +160,12 @@ async def register_canbus(var, config): ), synchronous=True, ) -async def canbus_action_to_code(config, action_id, template_arg, args): +async def canbus_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_CANBUS_ID]) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index fce11594bf..bca8e41801 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -6,6 +6,7 @@ #include "esphome/core/string_ref.h" #include "esphome/components/wifi/wifi_component.h" #include "captive_index.h" +#include "scan_list.h" namespace esphome::captive_portal { @@ -33,8 +34,10 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { // Invariant: only bounded in-memory work under the lock; the network send // happens later in request->send() wifi::ScanResultsLock lock(wifi::global_wifi_component); - for (const auto &scan : wifi::global_wifi_component->get_scan_result()) { - if (scan.get_is_hidden()) + const auto &results = wifi::global_wifi_component->get_scan_result(); + for (const auto &scan : results) { + bool with_auth = false; + if (!should_show_scan_entry(results, scan, with_auth)) continue; json_escape_into_buffer(escaped_ssid, scan.get_ssid()); @@ -44,10 +47,10 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { stream->print(ESPHOME_F("\",\"rssi\":")); stream->print(scan.get_rssi()); stream->print(ESPHOME_F(",\"lock\":")); - stream->print(scan.get_with_auth()); + stream->print(with_auth); stream->print(ESPHOME_F("}")); #else - stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), scan.get_with_auth()); + stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), with_auth); #endif } } diff --git a/esphome/components/captive_portal/scan_list.h b/esphome/components/captive_portal/scan_list.h new file mode 100644 index 0000000000..d24a88a670 --- /dev/null +++ b/esphome/components/captive_portal/scan_list.h @@ -0,0 +1,28 @@ +#pragma once +#include + +namespace esphome::captive_portal { + +// A scan lists every BSSID, so one SSID can appear several times. Returns true for +// the strongest entry per SSID (earliest on ties), never for hidden entries. scan +// must be an element of results. with_auth is written only when returning true and +// is set if any entry with that SSID needs a key. Templated for host tests. +template +bool should_show_scan_entry(const Results &results, const Entry &scan, bool &with_auth) { + if (scan.get_is_hidden()) + return false; + const int8_t rssi = scan.get_rssi(); + bool any_auth = false; + for (const auto &other : results) { + if (other.get_is_hidden() || !other.ssid_equals(scan)) + continue; + // Same array, so address order is index order. scan fails both checks against itself. + if (other.get_rssi() > rssi || (other.get_rssi() == rssi && &other < &scan)) + return false; + any_auth |= other.get_with_auth(); + } + with_auth = any_auth; + return true; +} + +} // namespace esphome::captive_portal diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index fe050fca22..80dd913fba 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.components import mqtt, web_server @@ -48,13 +50,19 @@ from esphome.const import ( CONF_VISUAL, CONF_WEB_SERVER, ) -from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_entity, ) -from esphome.cpp_generator import LambdaExpression, MockObjClass +from esphome.cpp_generator import ( + LambdaExpression, + MockObj, + MockObjClass, + TemplateArgsType, +) +from esphome.types import ConfigType, SafeExpType IS_PLATFORM_COMPONENT = True @@ -132,7 +140,7 @@ VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema( ) -def visual_temperature_step(value): +def visual_temperature_step(value: Any) -> ConfigType: # Allow defining target/current temperature steps separately if isinstance(value, dict): return VISUAL_TEMPERATURE_STEP_SCHEMA(value) @@ -273,7 +281,7 @@ def climate_schema( @setup_entity("climate") -async def setup_climate_core_(var, config): +async def setup_climate_core_(var: MockObj, config: ConfigType) -> None: visual = config.get(CONF_VISUAL, {}) if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") @@ -443,7 +451,7 @@ async def setup_climate_core_(var, config): await web_server.add_entity_config(var, web_server_config) -async def register_climate(var, config): +async def register_climate(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) queue_entity_register("climate", config) @@ -451,7 +459,7 @@ async def register_climate(var, config): await setup_climate_core_(var, config) -async def new_climate(config, *args): +async def new_climate(config: ConfigType, *args: SafeExpType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID], *args) await register_climate(var, config) return var @@ -485,7 +493,12 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( CLIMATE_CONTROL_ACTION_SCHEMA, synchronous=True, ) -async def climate_control_to_code(config, action_id, template_arg, args): +async def climate_control_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) # All configured fields are folded into a single stateless lambda whose @@ -549,5 +562,5 @@ async def climate_control_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(climate_ns.using) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index b41ca4a540..0f01443bd0 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -511,29 +511,6 @@ ClimateTraits Climate::get_traits() { return traits; } -#ifdef USE_CLIMATE_VISUAL_OVERRIDES -void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) { - this->visual_min_temperature_override_ = visual_min_temperature_override; -} - -void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) { - this->visual_max_temperature_override_ = visual_max_temperature_override; -} - -void Climate::set_visual_temperature_step_override(float target, float current) { - this->visual_target_temperature_step_override_ = target; - this->visual_current_temperature_step_override_ = current; -} - -void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { - this->visual_min_humidity_override_ = visual_min_humidity_override; -} - -void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { - this->visual_max_humidity_override_ = visual_max_humidity_override; -} -#endif - ClimateCall Climate::make_call() { return ClimateCall(this); } ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 04f653a2b0..a906897235 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -228,11 +228,22 @@ class Climate : public EntityBase { ClimateTraits get_traits(); #ifdef USE_CLIMATE_VISUAL_OVERRIDES - void set_visual_min_temperature_override(float visual_min_temperature_override); - void set_visual_max_temperature_override(float visual_max_temperature_override); - void set_visual_temperature_step_override(float target, float current); - void set_visual_min_humidity_override(float visual_min_humidity_override); - void set_visual_max_humidity_override(float visual_max_humidity_override); + void set_visual_min_temperature_override(float visual_min_temperature_override) { + this->visual_min_temperature_override_ = visual_min_temperature_override; + } + void set_visual_max_temperature_override(float visual_max_temperature_override) { + this->visual_max_temperature_override_ = visual_max_temperature_override; + } + void set_visual_temperature_step_override(float target, float current) { + this->visual_target_temperature_step_override_ = target; + this->visual_current_temperature_step_override_ = current; + } + void set_visual_min_humidity_override(float visual_min_humidity_override) { + this->visual_min_humidity_override_ = visual_min_humidity_override; + } + void set_visual_max_humidity_override(float visual_max_humidity_override) { + this->visual_max_humidity_override_ = visual_max_humidity_override; + } #endif /// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits). diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py index 3c82fac977..936c5fc673 100644 --- a/esphome/components/cm1106/sensor.py +++ b/esphome/components/cm1106/sensor.py @@ -13,6 +13,9 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["uart"] CODEOWNERS = ["@andrewjswan"] @@ -44,7 +47,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config) -> None: +async def to_code(config: ConfigType) -> None: """Code generation entry point.""" var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -67,7 +70,12 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None: +async def cm1106_calibration_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: """Service code generation entry point.""" paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py index c39c5924af..70240eff07 100644 --- a/esphome/components/color/__init__.py +++ b/esphome/components/color/__init__.py @@ -1,5 +1,8 @@ +from typing import Any + from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE +from esphome.types import ConfigType ColorStruct = cg.esphome_ns.struct("Color") @@ -14,7 +17,7 @@ CONF_WHITE_INT = "white_int" CONF_HEX = "hex" -def hex_color(value): +def hex_color(value: Any) -> tuple[int, int, int]: if isinstance(value, int): value = str(value) if not isinstance(value, str): @@ -39,7 +42,7 @@ components = { } -def validate_color(config): +def validate_color(config: ConfigType) -> ConfigType: has_components = set(config) & components has_hex = CONF_HEX in config if has_hex and has_components: @@ -68,7 +71,7 @@ CONFIG_SCHEMA = cv.All( ) -def from_rgbw(config): +def from_rgbw(config: ConfigType) -> tuple[int, int, int, int]: r = 0 if CONF_RED in config: r = int(config[CONF_RED] * 255) @@ -96,7 +99,7 @@ def from_rgbw(config): return (r, g, b, w) -async def to_code(config): +async def to_code(config: ConfigType) -> None: if CONF_HEX in config: r, g, b = config[CONF_HEX] w = 0 diff --git a/esphome/components/copy/binary_sensor/__init__.py b/esphome/components/copy/binary_sensor/__init__.py index 840200409f..cc8492f21e 100644 --- a/esphome/components/copy/binary_sensor/__init__.py +++ b/esphome/components/copy/binary_sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_SOURCE_ID, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -33,7 +34,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/button/__init__.py b/esphome/components/copy/button/__init__.py index 8028d6a217..768131bbe5 100644 --- a/esphome/components/copy/button/__init__.py +++ b/esphome/components/copy/button/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_SOURCE_ID, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -32,7 +33,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await button.register_button(var, config) await cg.register_component(var, config) diff --git a/esphome/components/copy/cover/__init__.py b/esphome/components/copy/cover/__init__.py index ff5bef5668..d23602fa74 100644 --- a/esphome/components/copy/cover/__init__.py +++ b/esphome/components/copy/cover/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_SOURCE_ID, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -31,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await cover.new_cover(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/fan/__init__.py b/esphome/components/copy/fan/__init__.py index a208e5f80a..ffa414c5f2 100644 --- a/esphome/components/copy/fan/__init__.py +++ b/esphome/components/copy/fan/__init__.py @@ -3,6 +3,7 @@ from esphome.components import fan import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -25,7 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await fan.new_fan(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/lock/__init__.py b/esphome/components/copy/lock/__init__.py index 46bc08273e..8d9c4b6eca 100644 --- a/esphome/components/copy/lock/__init__.py +++ b/esphome/components/copy/lock/__init__.py @@ -3,6 +3,7 @@ from esphome.components import lock import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -25,7 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await lock.new_lock(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/number/__init__.py b/esphome/components/copy/number/__init__.py index 3e2bbf2aae..9659a605f9 100644 --- a/esphome/components/copy/number/__init__.py +++ b/esphome/components/copy/number/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -33,7 +34,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await number.new_number(config, min_value=0, max_value=0, step=0) await cg.register_component(var, config) diff --git a/esphome/components/copy/select/__init__.py b/esphome/components/copy/select/__init__.py index d7ddc52c44..97776b1edd 100644 --- a/esphome/components/copy/select/__init__.py +++ b/esphome/components/copy/select/__init__.py @@ -3,6 +3,7 @@ from esphome.components import select import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -25,7 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await select.register_select(var, config, options=[]) await cg.register_component(var, config) diff --git a/esphome/components/copy/sensor/__init__.py b/esphome/components/copy/sensor/__init__.py index 57ca06aca7..5468798047 100644 --- a/esphome/components/copy/sensor/__init__.py +++ b/esphome/components/copy/sensor/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -37,7 +38,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/switch/__init__.py b/esphome/components/copy/switch/__init__.py index ee27e38c5f..0e714540f9 100644 --- a/esphome/components/copy/switch/__init__.py +++ b/esphome/components/copy/switch/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_SOURCE_ID, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -31,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await switch.new_switch(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/text/__init__.py b/esphome/components/copy/text/__init__.py index f1ca404b7b..59fdce6c96 100644 --- a/esphome/components/copy/text/__init__.py +++ b/esphome/components/copy/text/__init__.py @@ -3,6 +3,7 @@ from esphome.components import text import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_MODE, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -26,7 +27,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await text.new_text(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/text_sensor/__init__.py b/esphome/components/copy/text_sensor/__init__.py index 7b38ff1a64..146beae5ea 100644 --- a/esphome/components/copy/text_sensor/__init__.py +++ b/esphome/components/copy/text_sensor/__init__.py @@ -3,6 +3,7 @@ from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -25,7 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 7639e15334..011b2c2f04 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -46,7 +46,7 @@ from esphome.core.entity_helpers import ( setup_entity, ) from esphome.cpp_generator import LambdaExpression, MockObj, MockObjClass -from esphome.types import ConfigType, TemplateArgsType +from esphome.types import ConfigType, SafeExpType, TemplateArgsType IS_PLATFORM_COMPONENT = True @@ -162,7 +162,7 @@ _COVER_SCHEMA = ( _COVER_SCHEMA.add_extra(entity_duplicate_validator("cover")) -def _validate_mqtt_state_topics(config): +def _validate_mqtt_state_topics(config: ConfigType) -> ConfigType: if config.get(CONF_MQTT_JSON_STATE_PAYLOAD): if CONF_POSITION_STATE_TOPIC in config: raise cv.Invalid( @@ -201,7 +201,7 @@ def cover_schema( @setup_entity("cover") -async def setup_cover_core_(var, config): +async def setup_cover_core_(var: MockObj, config: ConfigType) -> None: setup_device_class(config) if CONF_ON_OPEN in config: @@ -235,7 +235,7 @@ async def setup_cover_core_(var, config): await web_server.add_entity_config(var, web_server_config) -async def register_cover(var, config): +async def register_cover(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) queue_entity_register("cover", config) @@ -243,7 +243,7 @@ async def register_cover(var, config): await setup_cover_core_(var, config) -async def new_cover(config, *args): +async def new_cover(config: ConfigType, *args: SafeExpType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID], *args) await register_cover(var, config) return var @@ -259,7 +259,12 @@ COVER_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( "cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True ) -async def cover_open_to_code(config, action_id, template_arg, args): +async def cover_open_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -267,7 +272,12 @@ async def cover_open_to_code(config, action_id, template_arg, args): @automation.register_action( "cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True ) -async def cover_close_to_code(config, action_id, template_arg, args): +async def cover_close_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -275,7 +285,12 @@ async def cover_close_to_code(config, action_id, template_arg, args): @automation.register_action( "cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True ) -async def cover_stop_to_code(config, action_id, template_arg, args): +async def cover_stop_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -283,7 +298,12 @@ async def cover_stop_to_code(config, action_id, template_arg, args): @automation.register_action( "cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True ) -async def cover_toggle_to_code(config, action_id, template_arg, args): +async def cover_toggle_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -421,5 +441,5 @@ automation.register_condition( @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(cover_ns.using) diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index e98a555fe5..dc2db3bf32 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -135,10 +135,6 @@ CoverCall &CoverCall::set_stop(bool stop) { this->stop_ = stop; return *this; } -bool CoverCall::get_stop() const { return this->stop_; } - -CoverCall Cover::make_call() { return {this}; } - void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f); @@ -184,9 +180,6 @@ optional Cover::restore_state_() { return recovered; } -bool Cover::is_fully_open() const { return this->position == COVER_OPEN; } -bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; } - CoverCall CoverRestoreState::to_call(Cover *cover) { auto call = cover->make_call(); auto traits = cover->get_traits(); diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 9a75e68487..8bf45cfb57 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -50,7 +50,7 @@ class CoverCall { void perform(); const optional &get_position() const; - bool get_stop() const; + bool get_stop() const { return this->stop_; } const optional &get_tilt() const; const optional &get_toggle() const; @@ -123,7 +123,7 @@ class Cover : public EntityBase { float tilt{COVER_OPEN}; /// Construct a new cover call used to control the cover. - CoverCall make_call(); + CoverCall make_call() { return {this}; } template void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward(f)); } @@ -139,9 +139,9 @@ class Cover : public EntityBase { virtual CoverTraits get_traits() = 0; /// Helper method to check if the cover is fully open. Equivalent to comparing .position against 1.0 - bool is_fully_open() const; + bool is_fully_open() const { return this->position == COVER_OPEN; } /// Helper method to check if the cover is fully closed. Equivalent to comparing .position against 0.0 - bool is_fully_closed() const; + bool is_fully_closed() const { return this->position == COVER_CLOSED; } protected: friend CoverCall; diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index 0c6ae0d821..5f14457101 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -17,6 +17,9 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@balrog-kun"] DEPENDENCIES = ["spi"] @@ -40,7 +43,7 @@ CONF_VOLTAGE_HPF = "voltage_hpf" CONF_PULSE_ENERGY = "pulse_energy" -def validate_config(config): +def validate_config(config: ConfigType) -> ConfigType: current_gain = abs(config[CONF_CURRENT_GAIN]) * ( 1.0 if config[CONF_PGA_GAIN] == "10X" else 5.0 ) @@ -105,7 +108,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await spi.register_spi_device(var, config) @@ -138,6 +141,11 @@ async def to_code(config): ), synchronous=True, ) -async def restart_action_to_code(config, action_id, template_arg, args): +async def restart_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/daly_bms/__init__.py b/esphome/components/daly_bms/__init__.py index 87f00ce507..ba0be4d3a5 100644 --- a/esphome/components/daly_bms/__init__.py +++ b/esphome/components/daly_bms/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID +from esphome.types import ConfigType CODEOWNERS = ["@s1lvi0"] MULTI_CONF = True @@ -26,7 +27,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/daly_bms/binary_sensor.py b/esphome/components/daly_bms/binary_sensor.py index 95a2ae3b44..2b6ceffff1 100644 --- a/esphome/components/daly_bms/binary_sensor.py +++ b/esphome/components/daly_bms/binary_sensor.py @@ -1,6 +1,8 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_BMS_DALY_ID, DalyBmsComponent @@ -27,13 +29,13 @@ CONFIG_SCHEMA = cv.All( ) -async def setup_conf(config, key, hub): +async def setup_conf(config: ConfigType, key: str, hub: MockObj) -> None: if sensor_config := config.get(key): var = await binary_sensor.new_binary_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_binary_sensor")(var)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_BMS_DALY_ID]) for key in TYPES: await setup_conf(config, key, hub) diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index aa92cfa86a..3e91fb280a 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -23,6 +23,8 @@ from esphome.const import ( UNIT_PERCENT, UNIT_VOLT, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_BMS_DALY_ID, DalyBmsComponent @@ -222,13 +224,13 @@ CONFIG_SCHEMA = cv.All( ) -async def setup_conf(config, key, hub): +async def setup_conf(config: ConfigType, key: str, hub: MockObj) -> None: if sensor_config := config.get(key): sens = await sensor.new_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_sensor")(sens)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_BMS_DALY_ID]) for key in TYPES: await setup_conf(config, key, hub) diff --git a/esphome/components/daly_bms/text_sensor.py b/esphome/components/daly_bms/text_sensor.py index 9f4e2df85a..1a91081bbf 100644 --- a/esphome/components/daly_bms/text_sensor.py +++ b/esphome/components/daly_bms/text_sensor.py @@ -2,6 +2,8 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_STATUS +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_BMS_DALY_ID, DalyBmsComponent @@ -23,13 +25,13 @@ CONFIG_SCHEMA = cv.All( ) -async def setup_conf(config, key, hub): +async def setup_conf(config: ConfigType, key: str, hub: MockObj) -> None: if sensor_config := config.get(key): sens = await text_sensor.new_text_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_BMS_DALY_ID]) for key in TYPES: await setup_conf(config, key, hub) diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 31559a514c..c27669d77e 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -2,6 +2,7 @@ import base64 from pathlib import Path import re import secrets +from typing import Any import requests from ruamel.yaml import YAML @@ -13,6 +14,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ESPHOME, CONF_PROJECT, CONF_REF, CONF_WIFI import esphome.final_validate as fv from esphome.happy_eyeballs import ensure_happy_eyeballs +from esphome.types import ConfigType from esphome.yaml_util import dump dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import") @@ -23,14 +25,14 @@ DEPENDENCIES = ["api"] CODEOWNERS = ["@esphome/core"] -def validate_import_url(value): +def validate_import_url(value: Any) -> str: value = cv.string_strict(value) value = cv.Length(max=255)(value) validate_source_shorthand(value) return value -def validate_full_url(config): +def validate_full_url(config: ConfigType) -> ConfigType: if not config[CONF_IMPORT_FULL_CONFIG]: return config source = validate_source_shorthand(config[CONF_PACKAGE_IMPORT_URL]) @@ -55,7 +57,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: full_config = fv.full_config.get()[CONF_ESPHOME] if CONF_PROJECT not in full_config: raise cv.Invalid( @@ -73,7 +75,7 @@ wifi: """ -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_DASHBOARD_IMPORT") url = config[CONF_PACKAGE_IMPORT_URL] if config[CONF_IMPORT_FULL_CONFIG]: diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 87997daa3d..f8b6446006 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -21,13 +21,14 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_YEAR, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@rfdarter", "@jesserockz"] @@ -65,7 +66,7 @@ DATETIME_MODES = [ ] -def _validate_time_present(config): +def _validate_time_present(config: ConfigType) -> ConfigType: config = config.copy() if CONF_ON_TIME in config and CONF_TIME_ID not in config: time_id = cv.use_id(time.RealTimeClock)(None) @@ -139,7 +140,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema: @setup_entity("datetime") -async def setup_datetime_core_(var, config): +async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None: if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) @@ -160,7 +161,7 @@ async def setup_datetime_core_(var, config): await cg.register_parented(trigger, var) -async def register_datetime(var, config): +async def register_datetime(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) entity_type = config[CONF_TYPE].lower() @@ -169,14 +170,14 @@ async def register_datetime(var, config): await setup_datetime_core_(var, config) -async def new_datetime(config, *args): +async def new_datetime(config: ConfigType, *args: SafeExpType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID], *args) await register_datetime(var, config) return var @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(datetime_ns.using) @@ -193,7 +194,12 @@ async def to_code(config): ), synchronous=True, ) -async def datetime_date_set_to_code(config, action_id, template_arg, args): +async def datetime_date_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: action_var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(action_var, config[CONF_ID]) @@ -226,7 +232,12 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def datetime_time_set_to_code(config, action_id, template_arg, args): +async def datetime_time_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: action_var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(action_var, config[CONF_ID]) @@ -259,7 +270,12 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def datetime_datetime_set_to_code(config, action_id, template_arg, args): +async def datetime_datetime_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: action_var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(action_var, config[CONF_ID]) diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index 997aec3f69..b99b89259f 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -37,8 +37,6 @@ void DateEntity::publish_state() { #endif } -DateCall DateEntity::make_call() { return DateCall(this); } - void DateCall::validate_() { if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) { ESP_LOGE(TAG, "Year must be between 1970 and 3000"); diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index 9b86c12228..93ce1411f8 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -96,6 +96,8 @@ class DateCall { optional day_; }; +inline DateCall DateEntity::make_call() { return DateCall(this); } + template class DateSetAction final : public Action, public Parented { public: TEMPLATABLE_VALUE(ESPTime, date) diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index a8e00d6eb3..8f180fd081 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -53,8 +53,6 @@ void DateTimeEntity::publish_state() { #endif } -DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } - ESPTime DateTimeEntity::state_as_esptime() const { ESPTime obj; obj.year = this->year_; diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 159e4ccc6f..fec620b5ba 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -121,6 +121,8 @@ class DateTimeCall { optional second_; }; +inline DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } + template class DateTimeSetAction final : public Action, public Parented { public: TEMPLATABLE_VALUE(ESPTime, datetime) diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 1cc9eaf2fb..da4c9eb31e 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -33,8 +33,6 @@ void TimeEntity::publish_state() { #endif } -TimeCall TimeEntity::make_call() { return TimeCall(this); } - void TimeCall::validate_() { if (this->hour_.has_value() && this->hour_ > 23) { ESP_LOGE(TAG, "Hour must be between 0 and 23"); diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index 643f4bd176..736e26f4a7 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -98,6 +98,8 @@ class TimeCall { optional second_; }; +inline TimeCall TimeEntity::make_call() { return TimeCall(this); } + template class TimeSetAction final : public Action, public Parented { public: TEMPLATABLE_VALUE(ESPTime, time) diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 3e94d04f21..a889d13329 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["logger"] @@ -45,7 +46,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: if CORE.using_zephyr: zephyr_add_prj_conf("HWINFO", True) # gdb thread support diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index a018ce5c3b..72e2efebc2 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -21,6 +21,7 @@ from esphome.const import ( UNIT_MILLISECOND, UNIT_PERCENT, ) +from esphome.types import ConfigType from . import ( # noqa: F401 pylint: disable=unused-import CONF_DEBUG_ID, @@ -111,7 +112,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: debug_component = await cg.get_variable(config[CONF_DEBUG_ID]) if free_conf := config.get(CONF_FREE): diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py index c69b8d9461..9d4fcc1b42 100644 --- a/esphome/components/debug/text_sensor.py +++ b/esphome/components/debug/text_sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( ICON_CHIP, ICON_RESTART, ) +from esphome.types import ConfigType from . import ( # noqa: F401 pylint: disable=unused-import CONF_DEBUG_ID, @@ -33,7 +34,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: debug_component = await cg.get_variable(config[CONF_DEBUG_ID]) if CONF_DEVICE in config: diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 3b70f947d2..dc03708645 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -38,7 +38,8 @@ from esphome.const import ( PLATFORM_NRF52, PlatformFramework, ) -from esphome.core import CORE +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType WAKEUP_PINS = { @@ -162,6 +163,11 @@ def validate_config(config: ConfigType) -> ConfigType: "You need to remove the global wakeup_pin_mode and define it per pin" ) if wakeup_pins: + if CONF_WAKEUP_PIN_MODE in wakeup_pins[0]: + raise cv.Invalid( + "Specify wakeup_pin_mode either at the top level under deep_sleep " + "or under the pin entry, not both" + ) wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE) elif ( isinstance(config.get(CONF_WAKEUP_PIN), list) @@ -174,7 +180,7 @@ def validate_config(config: ConfigType) -> ConfigType: return config -def _validate_ex1_wakeup_mode(value): +def _validate_ex1_wakeup_mode(value: str) -> str: if value == "ALL_LOW": esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value) if value == "ANY_LOW": @@ -345,7 +351,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -458,7 +464,12 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All( DEEP_SLEEP_ENTER_SCHEMA, synchronous=True, ) -async def deep_sleep_enter_to_code(config, action_id, template_arg, args): +async def deep_sleep_enter_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if CONF_SLEEP_DURATION in config: @@ -487,7 +498,12 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), synchronous=True, ) -async def deep_sleep_action_to_code(config, action_id, template_arg, args): +async def deep_sleep_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index e7ce70b60c..9a3e537e05 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -43,10 +43,6 @@ void DeepSleepComponent::loop() { this->begin_sleep(); } -void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } - -void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } - void DeepSleepComponent::begin_sleep(bool manual) { if (this->prevent_ && !manual) { this->next_enter_deep_sleep_ = true; @@ -76,8 +72,4 @@ void DeepSleepComponent::begin_sleep(bool manual) { float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } -void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } - -void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; } - } // namespace esphome::deep_sleep diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index a620d52a02..208f88d707 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -132,7 +132,7 @@ template class PreventDeepSleepAction; class DeepSleepComponent final : public Component { public: /// Set the duration in ms the component should sleep once it's in deep sleep mode. - void set_sleep_duration(uint32_t time_ms); + void set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } #if defined(USE_ESP32) /** Set the pin to wake up to on the ESP32 once it's in deep sleep mode. * Use the inverted property to set the wakeup level. @@ -157,7 +157,7 @@ class DeepSleepComponent final : public Component { #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ !defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2) - void set_touch_wakeup(bool touch_wakeup); + void set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } #endif // Set the duration in ms for how long the code should run before entering @@ -166,7 +166,7 @@ class DeepSleepComponent final : public Component { #endif // USE_ESP32 /// Set a duration in ms for how long the code should run before entering deep sleep mode. - void set_run_duration(uint32_t time_ms); + void set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } void setup() override; void dump_config() override; @@ -176,8 +176,8 @@ class DeepSleepComponent final : public Component { /// Helper to enter deep sleep mode void begin_sleep(bool manual = false); - void prevent_deep_sleep(); - void allow_deep_sleep(); + void prevent_deep_sleep() { this->prevent_ = true; } + void allow_deep_sleep() { this->prevent_ = false; } protected: // Returns nullopt if no run duration is set. Otherwise, returns the run diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index f64e1f37e1..3fa1a1f1ed 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -74,12 +74,6 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } #endif -#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ - !defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ - !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2) -void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } -#endif - void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; } diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index 943c510279..51562f923c 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -1,9 +1,14 @@ +from typing import Any + from esphome import automation from esphome.automation import maybe_simple_id import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_SENSITIVITY +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@niklasweber"] DEPENDENCIES = ["uart"] @@ -38,7 +43,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) @@ -54,14 +59,19 @@ async def to_code(config): ), synchronous=True, ) -async def dfrobot_sen0395_reset_to_code(config, action_id, template_arg, args): +async def dfrobot_sen0395_reset_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -def range_segment_list(input): +def range_segment_list(input: Any) -> list: """Validate input is a list of ranges which can be used to configure the dfrobot mmwave radar A list of segments should be provided. A minimum of one segment is required and a maximum of @@ -154,7 +164,12 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema( MMWAVE_SETTINGS_SCHEMA, synchronous=True, ) -async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args): +async def dfrobot_sen0395_settings_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/dfrobot_sen0395/binary_sensor.py b/esphome/components/dfrobot_sen0395/binary_sensor.py index 193ef925a4..e299c35a42 100644 --- a/esphome/components/dfrobot_sen0395/binary_sensor.py +++ b/esphome/components/dfrobot_sen0395/binary_sensor.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import DEVICE_CLASS_MOTION +from esphome.types import ConfigType from . import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component @@ -16,7 +17,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID]) binary_sens = await binary_sensor.new_binary_sensor(config) diff --git a/esphome/components/dfrobot_sen0395/switch/__init__.py b/esphome/components/dfrobot_sen0395/switch/__init__.py index 8e492080de..22aaa1640c 100644 --- a/esphome/components/dfrobot_sen0395/switch/__init__.py +++ b/esphome/components/dfrobot_sen0395/switch/__init__.py @@ -3,6 +3,7 @@ from esphome.components import switch import esphome.config_validation as cv from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG from esphome.cpp_generator import MockObjClass +from esphome.types import ConfigType from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component @@ -55,7 +56,7 @@ CONFIG_SCHEMA = cv.typed_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID]) var = await switch.new_switch(config) await cg.register_component(var, config) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 115adf503a..c2d45dbb60 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -685,9 +685,6 @@ void Display::show_page(DisplayPage *page) { } } -void Display::show_next_page() { this->page_->show_next(); } -void Display::show_prev_page() { this->page_->show_prev(); } - void Display::do_update_() { if (this->auto_clear_enabled_) { this->clear(); @@ -892,9 +889,6 @@ void DisplayPage::show_prev() { this->prev_->show(); } -void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; } -void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } -void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } const display_writer_t &DisplayPage::get_writer() const { return this->writer_; } const LogString *text_align_to_string(TextAlign textalign) { diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 3a136937f6..a9ffda422d 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -802,9 +802,9 @@ class DisplayPage final { void show(); void show_next(); void show_prev(); - void set_parent(Display *parent); - void set_prev(DisplayPage *prev); - void set_next(DisplayPage *next); + void set_parent(Display *parent) { this->parent_ = parent; } + void set_prev(DisplayPage *prev) { this->prev_ = prev; } + void set_next(DisplayPage *next) { this->next_ = next; } const display_writer_t &get_writer() const; protected: @@ -814,6 +814,9 @@ class DisplayPage final { DisplayPage *next_{nullptr}; }; +inline void Display::show_next_page() { this->page_->show_next(); } +inline void Display::show_prev_page() { this->page_->show_prev(); } + template class DisplayPageShowAction final : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) diff --git a/esphome/components/dlms_meter/__init__.py b/esphome/components/dlms_meter/__init__.py index b747f73a14..00a1694cc3 100644 --- a/esphome/components/dlms_meter/__init__.py +++ b/esphome/components/dlms_meter/__init__.py @@ -1,5 +1,6 @@ import logging import re +from typing import Any import esphome.codegen as cg from esphome.components import esp32, uart @@ -12,6 +13,7 @@ from esphome.const import ( CONF_RECEIVE_TIMEOUT, ) from esphome.core import CORE +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -33,13 +35,13 @@ DlmsMeterComponent = dlms_meter_component_ns.class_( ) -def obis_code(value): +def obis_code(value: Any) -> str: # Normalize the OBIS code to the strict A.B.C.D.E.F format bytes_list = parse_obis_code_bytes(value) return ".".join(str(b) for b in bytes_list) -def parse_obis_code_bytes(value): +def parse_obis_code_bytes(value: Any) -> list[int]: value = cv.string(value) normalized = re.sub(r"[\-\:\*]", ".", value) parts = normalized.split(".") @@ -57,19 +59,19 @@ def parse_obis_code_bytes(value): return bytes_list -def custom_pattern_dict(value): +def custom_pattern_dict(value: Any) -> ConfigType: if isinstance(value, str): return {CONF_PATTERN: value} return value -def validate_custom_pattern(value): +def validate_custom_pattern(value: ConfigType) -> ConfigType: if CONF_DEFAULT_OBIS in value and CONF_NAME not in value: raise cv.Invalid(f"'{CONF_DEFAULT_OBIS}' requires '{CONF_NAME}' to be set") return value -def validate_provider_deprecation(config): +def validate_provider_deprecation(config: ConfigType) -> ConfigType: if CONF_PROVIDER in config: provider = str(config[CONF_PROVIDER]).lower() if provider == "netznoe": @@ -154,7 +156,7 @@ CONFIG_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("dlms_meter", require_rx=True) -async def to_code(config): +async def to_code(config: ConfigType) -> None: dec_key_expr = cg.RawExpression("std::nullopt") if dec_key := config.get(CONF_DECRYPTION_KEY): key_bytes = [str(int(dec_key[i : i + 2], 16)) for i in range(0, 32, 2)] diff --git a/esphome/components/dlms_meter/binary_sensor/__init__.py b/esphome/components/dlms_meter/binary_sensor/__init__.py index f9bc1d9df7..a15e58b957 100644 --- a/esphome/components/dlms_meter/binary_sensor/__init__.py +++ b/esphome/components/dlms_meter/binary_sensor/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv +from esphome.types import ConfigType from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code @@ -14,7 +15,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) var = await binary_sensor.new_binary_sensor(config) cg.add(hub.register_binary_sensor(config[CONF_OBIS_CODE], var)) diff --git a/esphome/components/dlms_meter/sensor/__init__.py b/esphome/components/dlms_meter/sensor/__init__.py index ec4639351d..8ded150cd0 100644 --- a/esphome/components/dlms_meter/sensor/__init__.py +++ b/esphome/components/dlms_meter/sensor/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.types import ConfigType from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code @@ -47,7 +48,7 @@ DYNAMIC_SCHEMA = sensor.sensor_schema().extend( ) -def deprecation_warning(config): +def deprecation_warning(config: ConfigType) -> ConfigType: _LOGGER.warning( "The dlms_meter sensor schema using predefined keys (e.g., 'voltage_l1') is deprecated and will be removed in 2026.11.0. " "Please update your configuration to use the new schema with 'obis_code'." @@ -145,7 +146,7 @@ OLD_SCHEMA = cv.All( CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) if obis := config.get(CONF_OBIS_CODE): diff --git a/esphome/components/dlms_meter/text_sensor/__init__.py b/esphome/components/dlms_meter/text_sensor/__init__.py index 0bfb43a285..c2ff0779ee 100644 --- a/esphome/components/dlms_meter/text_sensor/__init__.py +++ b/esphome/components/dlms_meter/text_sensor/__init__.py @@ -3,6 +3,7 @@ import logging import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv +from esphome.types import ConfigType from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code @@ -23,7 +24,7 @@ DYNAMIC_SCHEMA = text_sensor.text_sensor_schema().extend( ) -def deprecation_warning(config): +def deprecation_warning(config: ConfigType) -> ConfigType: _LOGGER.warning( "The dlms_meter text_sensor schema using predefined keys (e.g., 'timestamp') is deprecated and will be removed in 2026.11.0. " "Please update your configuration to use the new schema with 'obis_code'." @@ -46,7 +47,7 @@ OLD_SCHEMA = cv.All( CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) if obis := config.get(CONF_OBIS_CODE): diff --git a/esphome/components/ds1307/time.py b/esphome/components/ds1307/time.py index 0e7bb976a2..a3ae3eb5af 100644 --- a/esphome/components/ds1307/time.py +++ b/esphome/components/ds1307/time.py @@ -3,6 +3,9 @@ import esphome.codegen as cg from esphome.components import i2c, time import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@badbadc0ffee"] DEPENDENCIES = ["i2c"] @@ -29,7 +32,12 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ), synchronous=True, ) -async def ds1307_write_time_to_code(config, action_id, template_arg, args): +async def ds1307_write_time_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -45,13 +53,18 @@ async def ds1307_write_time_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def ds1307_read_time_to_code(config, action_id, template_arg, args): +async def ds1307_read_time_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/ds248x/__init__.py b/esphome/components/ds248x/__init__.py index 5a26ceab50..a2e2a87ed0 100644 --- a/esphome/components/ds248x/__init__.py +++ b/esphome/components/ds248x/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_SLEEP_PIN, CONF_TYPE +from esphome.types import ConfigType CODEOWNERS = ["@tomwellnitz"] MULTI_CONF = True @@ -35,7 +36,7 @@ ds248x_ns = cg.esphome_ns.namespace("ds248x") DS248xComponent = ds248x_ns.class_("DS248xComponent", cg.Component, i2c.I2CDevice) -def _component_schema(*extras): +def _component_schema(*extras: dict) -> cv.Schema: schema = cv.Schema( { cv.GenerateID(): cv.declare_id(DS248xComponent), @@ -79,11 +80,11 @@ CONFIG_SCHEMA = cv.typed_schema( ) -def get_channel_count(config): +def get_channel_count(config: ConfigType) -> int: return CHANNEL_COUNTS[config[CONF_TYPE]] -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ds248x/one_wire.py b/esphome/components/ds248x/one_wire.py index 19861eae36..b028958132 100644 --- a/esphome/components/ds248x/one_wire.py +++ b/esphome/components/ds248x/one_wire.py @@ -12,6 +12,7 @@ import esphome.codegen as cg from esphome.components.one_wire import OneWireBus import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID +from esphome.types import ConfigType from . import CONF_DS248X_ID, DS248xComponent, ds248x_ns, get_channel_count @@ -29,7 +30,7 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: """Validate that the channel is within the parent's channel count.""" fconf = fv.full_config.get() path = fconf.get_path_for_id(config[CONF_DS248X_ID])[:-1] @@ -47,7 +48,7 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py index 456859f8e4..6d878a80a5 100644 --- a/esphome/components/duty_time/sensor.py +++ b/esphome/components/duty_time/sensor.py @@ -19,6 +19,9 @@ from esphome.const import ( STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CONF_LAST_TIME = "last_time" @@ -66,7 +69,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) cg.add(var.set_restore(config[CONF_RESTORE])) @@ -93,7 +96,12 @@ DUTY_TIME_ID_SCHEMA = maybe_simple_id( @register_action( "sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA, synchronous=True ) -async def sensor_runtime_start_to_code(config, action_id, template_arg, args): +async def sensor_runtime_start_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -102,7 +110,12 @@ async def sensor_runtime_start_to_code(config, action_id, template_arg, args): @register_action( "sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA, synchronous=True ) -async def sensor_runtime_stop_to_code(config, action_id, template_arg, args): +async def sensor_runtime_stop_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -111,7 +124,12 @@ async def sensor_runtime_stop_to_code(config, action_id, template_arg, args): @register_action( "sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA, synchronous=True ) -async def sensor_runtime_reset_to_code(config, action_id, template_arg, args): +async def sensor_runtime_reset_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -120,7 +138,12 @@ async def sensor_runtime_reset_to_code(config, action_id, template_arg, args): @register_condition( "sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA ) -async def duty_time_is_running_to_code(config, condition_id, template_arg, args): +async def duty_time_is_running_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren, True) @@ -128,6 +151,11 @@ async def duty_time_is_running_to_code(config, condition_id, template_arg, args) @register_condition( "sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA ) -async def duty_time_is_not_running_to_code(config, condition_id, template_arg, args): +async def duty_time_is_not_running_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren, False) diff --git a/esphome/components/emc2101/__init__.py b/esphome/components/emc2101/__init__.py index 323195e99a..639847345f 100644 --- a/esphome/components/emc2101/__init__.py +++ b/esphome/components/emc2101/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_INVERTED, CONF_RESOLUTION +from esphome.types import ConfigType CODEOWNERS = ["@ellull"] @@ -68,7 +69,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/emc2101/output/__init__.py b/esphome/components/emc2101/output/__init__.py index 586f0800a6..a8820345e2 100644 --- a/esphome/components/emc2101/output/__init__.py +++ b/esphome/components/emc2101/output/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType from .. import CONF_EMC2101_ID, EMC2101_COMPONENT_SCHEMA, emc2101_ns @@ -16,7 +17,7 @@ CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_EMC2101_ID]) var = cg.new_Pvariable(config[CONF_ID], paren) await output.register_output(var, config) diff --git a/esphome/components/emc2101/sensor/__init__.py b/esphome/components/emc2101/sensor/__init__.py index b6a2c8a333..cc8901cf38 100644 --- a/esphome/components/emc2101/sensor/__init__.py +++ b/esphome/components/emc2101/sensor/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_REVOLUTIONS_PER_MINUTE, ) +from esphome.types import ConfigType from .. import CONF_EMC2101_ID, EMC2101_COMPONENT_SCHEMA, emc2101_ns @@ -53,7 +54,7 @@ CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend( ).extend(cv.polling_component_schema("60s")) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_EMC2101_ID]) var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) diff --git a/esphome/components/emontx/__init__.py b/esphome/components/emontx/__init__.py index 3f83578926..7dde794f0b 100644 --- a/esphome/components/emontx/__init__.py +++ b/esphome/components/emontx/__init__.py @@ -11,7 +11,8 @@ from esphome.const import ( CONF_RX_BUFFER_SIZE, CONF_UART_ID, ) -from esphome.core import CORE +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv from esphome.types import ConfigType @@ -143,8 +144,11 @@ EMONTX_SEND_COMMAND_ACTION_SCHEMA = cv.Schema( synchronous=True, ) async def emontx_send_command_action_to_code( - config: ConfigType, action_id, template_arg, args -) -> None: + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.std_string) diff --git a/esphome/components/emontx/sensor/__init__.py b/esphome/components/emontx/sensor/__init__.py index 83a972c5e0..967bc4e699 100644 --- a/esphome/components/emontx/sensor/__init__.py +++ b/esphome/components/emontx/sensor/__init__.py @@ -68,6 +68,7 @@ PATTERN_CONFIGS = { "PULSE": { CONF_UNIT_OF_MEASUREMENT: UNIT_PULSES, CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, CONF_ACCURACY_DECIMALS: 0, }, "PF": { @@ -78,12 +79,13 @@ PATTERN_CONFIGS = { }, } -# Create a base schema that's flexible for any tag -BASE_SCHEMA = sensor.sensor_schema( - EmonTxSensor, - state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=0, -).extend( +# BASE_SCHEMA intentionally omits state_class and accuracy_decimals defaults. +# Passing them to sensor_schema() would register them via cv.Optional(key, default=...), +# making them always present in the validated config dict and preventing +# apply_tag_defaults from overriding them with the correct per-prefix values. +# They are injected by apply_tag_defaults below, after running through +# sensor.validate_state_class() so the value is code-generation-ready. +BASE_SCHEMA = sensor.sensor_schema(EmonTxSensor).extend( { cv.GenerateID(CONF_EMONTX_ID): cv.use_id(EmonTx), cv.Required(CONF_TAG_NAME): cv.string, @@ -91,34 +93,43 @@ BASE_SCHEMA = sensor.sensor_schema( ) +def _apply_defaults(config: ConfigType, defaults: dict) -> None: + """Inject defaults into config, skipping keys already set by the user. + state_class values are run through validate_state_class so they are + code-generation-ready, matching what sensor_schema() would normally do.""" + for key, value in defaults.items(): + if key not in config: + if key == CONF_STATE_CLASS: + value = sensor.validate_state_class(value) + config[key] = value + + def apply_tag_defaults(config: ConfigType) -> ConfigType: """Apply defaults based on tag prefix if applicable, but don't restrict any tags.""" tag = config[CONF_TAG_NAME] - # Skip if tag is too short - if len(tag) < 2: - return config + if len(tag) >= 2: + tag_upper = tag.upper() - # Check if this tag starts with a known prefix - tag_upper = tag.upper() + for pattern, pattern_config in PATTERN_CONFIGS.items(): + if tag_upper.startswith(pattern): + _apply_defaults(config, pattern_config) + return config - for pattern, pattern_config in PATTERN_CONFIGS.items(): - if tag_upper.startswith(pattern): - # Apply pattern defaults if not overridden by user - for key, value in pattern_config.items(): - if key not in config: - config[key] = value + # Only apply defaults for known prefixes with numeric indices (e.g. E1, V2, T3) + prefix = tag_upper[0] + if prefix in SENSOR_CONFIGS and tag[1:].isdigit(): + _apply_defaults(config, SENSOR_CONFIGS[prefix]) return config - # Only apply defaults for known prefixes with numeric indices - prefix = tag_upper[0] - if prefix in SENSOR_CONFIGS and len(tag) > 1 and tag[1:].isdigit(): - # Apply defaults for known tag types, but only if not overridden by user - defaults = SENSOR_CONFIGS[prefix] - for key, value in defaults.items(): - if key not in config: - config[key] = value - + # Fall back to generic defaults for tags with no known prefix + _apply_defaults( + config, + { + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_ACCURACY_DECIMALS: 0, + }, + ) return config diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 3065cdadad..cde0cfd68b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -12,6 +12,7 @@ from typing import Any from esphome import yaml_util import esphome.codegen as cg from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_ADVANCED, @@ -204,21 +205,36 @@ COMPILER_OPTIMIZATIONS = { # ESP-IDF components excluded by default to reduce compile time. # Components can be re-enabled by calling include_builtin_idf_component() in to_code(). # -# Cannot be excluded (dependencies of required components): -# - "console": espressif/mdns unconditionally depends on it -# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain +# Note: excluding a component only removes it from the initial build set. +# ESP-IDF's requirement expansion adds an excluded component back when any +# component still in the build REQUIRES it (e.g. espressif/mdns pulls +# "console" back in, esp_http_client pulls "tcp_transport" back in), so +# exclusions here are safe for such components and simply become no-ops in +# builds that need them. DEFAULT_EXCLUDED_IDF_COMPONENTS = ( + "app_trace", # CPU trace/SystemView support - unused by ESPHome "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing + "console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured "driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers + "esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf "esp_adc", # ADC driver - only needed by adc component + "esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back "esp_driver_dac", # DAC driver - only needed by esp32_dac component + "esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs + "esp_driver_i2c", # I2C driver - re-included by i2c; esp32-camera pulls it back itself "esp_driver_i2s", # I2S driver - only needed by i2s_audio component + "esp_driver_ledc", # LEDC PWM driver - re-included by ledc; esp32-camera pulls it back itself "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM "esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus + "esp_driver_sdio", # SDIO device-mode driver - unused by ESPHome + "esp_driver_sdm", # Sigma-delta modulation driver - unused by ESPHome + "esp_driver_sdmmc", # SD/MMC host driver - unused by ESPHome + "esp_driver_sdspi", # SD-over-SPI driver - unused by ESPHome "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component "esp_eth", # Ethernet driver - only needed by ethernet component + "esp_gdbstub", # GDB stub panic handler - unused by ESPHome; bt pulls it back "esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality "esp_http_client", # HTTP client - only needed by http_request component "esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation @@ -227,11 +243,16 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API "espcoredump", # Core dump support - ESPHome has its own debug component "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage + "json", # cJSON library - ESPHome uses ArduinoJson instead "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation "openthread", # Thread protocol - only needed by openthread component "perfmon", # Xtensa performance monitor - ESPHome has its own debug component + "protobuf-c", # Protobuf runtime - only used by provisioning components (also excluded) "protocomm", # Protocol communication for provisioning - unused by ESPHome + "rt", # POSIX realtime extensions - unused by ESPHome + "sdmmc", # SD/MMC protocol layer - only used by SD drivers and fatfs (also excluded) "spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only) + "tcp_transport", # Transport layer - esp_http_client/mqtt pull it back when re-included "ulp", # ULP coprocessor - not currently used by any ESPHome component "unity", # Unit testing framework - ESPHome doesn't use IDF's testing "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused @@ -738,6 +759,17 @@ def include_builtin_idf_component(name: str) -> None: CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name) +def get_excluded_builtin_components() -> list[str]: + """Return the sorted built-in IDF components excluded from the build. + + The set reaches both build writers as the ``EXCLUDE_COMPONENTS`` CMake + arg (registered via ``cg.add_cmake_arg`` at FINAL priority); the native + ESP-IDF writer also reads it directly to filter the built-in component + list. + """ + return sorted(CORE.data.get(KEY_ESP32, {}).get(KEY_EXCLUDE_COMPONENTS, ())) + + def _enable_arduino_library(name: str) -> None: """Enable an Arduino library that is disabled by default. @@ -2119,17 +2151,16 @@ def _configure_lwip_max_sockets(conf: dict) -> None: add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) +def register_exclude_components_cmake_arg() -> None: + """Register the current exclusion set as the EXCLUDE_COMPONENTS cmake arg.""" + if excluded := get_excluded_builtin_components(): + cg.add_cmake_arg("EXCLUDE_COMPONENTS", ";".join(excluded)) + + @coroutine_with_priority(CoroPriority.FINAL) async def _write_exclude_components() -> None: """Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions.""" - if KEY_ESP32 not in CORE.data: - return - excluded = CORE.data[KEY_ESP32].get(KEY_EXCLUDE_COMPONENTS) - if excluded: - exclude_list = ";".join(sorted(excluded)) - cg.add_platformio_option( - "board_build.cmake_extra_args", f"-DEXCLUDE_COMPONENTS={exclude_list}" - ) + register_exclude_components_cmake_arg() @coroutine_with_priority(CoroPriority.FINAL) @@ -3421,3 +3452,10 @@ def process_stacktrace(config, line, backtrace_state): _decode_pc(config, addr.group()) return backtrace_state + + +# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which +# are instantiated solely by the pin schema codegen (esp32_pin_to_code) +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"gpio.cpp": "USE_ESP32_INTERNAL_GPIO"} +) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index b61dad7386..6f65243aaa 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -124,6 +124,15 @@ static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *ou // Version is uint32_t because it would be padded to 4 bytes anyway before the next // uint32_t field, so we use the full width rather than wasting 3 bytes of padding. static constexpr uint32_t CRASH_DATA_VERSION = 4; +#if CONFIG_IDF_TARGET_ARCH_XTENSA +// EXCCAUSE is a 6-bit register; larger recorded values mean the frame's +// cause/vaddr slots were never written (not a real exception frame). +static constexpr uint32_t XTENSA_EXCCAUSE_COUNT = XCHAL_EXCCAUSE_NUM; +#elif CONFIG_IDF_TARGET_ARCH_RISCV +// Synchronous mcause exception codes are small and have no interrupt bit; +// anything else in a non-pseudo record is a stale slot. +static constexpr uint32_t RISCV_EXCEPTION_CAUSE_COUNT = 32; +#endif struct RawCrashData { uint32_t version; uint32_t magic; @@ -198,10 +207,28 @@ void crash_handler_clear() { s_raw_crash_data.magic = 0; } +// Whether the cause slot was written by a real exception frame. +static bool cause_slot_was_written() { +#if CONFIG_IDF_TARGET_ARCH_XTENSA + return s_raw_crash_data.cause < XTENSA_EXCCAUSE_COUNT; +#else + return s_raw_crash_data.cause < RISCV_EXCEPTION_CAUSE_COUNT; +#endif +} + // Look up the exception cause as a human-readable string. // Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays // not exposed via any public API. static const char *get_exception_reason() { + uint8_t exception = s_raw_crash_data.exception; + if (exception == PANIC_EXCEPTION_ABORT || exception == PANIC_EXCEPTION_TWDT) { + // Abort-class panics carry no cause register + return nullptr; + } + if (!cause_slot_was_written()) { + // Garbage from old-build or corrupt records; report just the type + return nullptr; + } #if CONFIG_IDF_TARGET_ARCH_XTENSA if (s_raw_crash_data.pseudo_excause) { // SoC-level panic: watchdog, cache error, etc. @@ -354,10 +381,11 @@ static const char *const FAULT_ADDR_REG = "MTVAL"; static const char *const FAULT_ADDR_REG_LOWER = "mtval"; #endif -// Whether the fault address is meaningful — real CPU faults only, not -// aborts/watchdogs or SoC-level pseudo exceptions. +// Whether the fault address is meaningful: real CPU faults with a validly +// written frame only. static bool has_fault_addr() { - return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause; + return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause && + cause_slot_was_written(); } // The record was captured by a different firmware build (it survives soft @@ -458,6 +486,10 @@ void crash_handler_log() { // into NOINIT memory before the normal panic handler runs. // extern "C" { +// Set by IDF's task watchdog (task_wdt.c, no header) before it simulates an +// abort; weak so builds without the task watchdog still link. +extern bool g_twdt_isr __attribute__((weak)); + // NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) // Names are mandated by the --wrap linker mechanism extern void __real_esp_panic_handler(panic_info_t *info); @@ -470,6 +502,14 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { s_raw_crash_data.exception = (uint8_t) info->exception; s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0; s_raw_crash_data.crashed_core = (uint8_t) info->core; + if (g_panic_abort) { + // IDF reclassifies to ABORT only inside esp_panic_handler(), after this + // wrapper captured info->exception; correct it here. TWDT is our own + // distinction (IDF never assigns PANIC_EXCEPTION_TWDT). The abort text is + // not stored; the symbolized backtrace already identifies the site. + bool is_twdt = &g_twdt_isr != nullptr && g_twdt_isr; + s_raw_crash_data.exception = (uint8_t) (is_twdt ? PANIC_EXCEPTION_TWDT : PANIC_EXCEPTION_ABORT); + } // Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot s_raw_crash_data.cause = 0; s_raw_crash_data.fault_addr = 0; @@ -487,8 +527,12 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { // Xtensa: walk the backtrace using the public API if (info->frame != nullptr) { auto *xt_frame = (XtExcFrame *) info->frame; - s_raw_crash_data.cause = xt_frame->exccause; - s_raw_crash_data.fault_addr = xt_frame->excvaddr; + if (!g_panic_abort) { + // Abort-class frames carry no useful cause/vaddr: TWDT task snapshots + // never wrote them and abort() traps describe only the synthetic trap. + s_raw_crash_data.cause = xt_frame->exccause; + s_raw_crash_data.fault_addr = xt_frame->excvaddr; + } s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE); } @@ -510,8 +554,11 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { // RISC-V: capture MEPC + RA, then scan stack for code addresses if (info->frame != nullptr) { auto *rv_frame = (RvExcFrame *) info->frame; - s_raw_crash_data.cause = rv_frame->mcause; - s_raw_crash_data.fault_addr = rv_frame->mtval; + if (!g_panic_abort) { + // See the Xtensa branch: abort-class frames carry no valid cause/vaddr. + s_raw_crash_data.cause = rv_frame->mcause; + s_raw_crash_data.fault_addr = rv_frame->mtval; + } s_raw_crash_data.backtrace_count = capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count); } diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 4b53d3a172..74665f3126 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -1,4 +1,7 @@ -#ifdef USE_ESP32 +#include "esphome/core/defines.h" +// Also defines the core ISRInternalGPIOPin methods; those are only reachable +// via ESP32InternalGPIOPin::to_isr(), so the same define gates both safely. +#if defined(USE_ESP32) && defined(USE_ESP32_INTERNAL_GPIO) #include "gpio.h" #include "esphome/core/log.h" @@ -204,4 +207,4 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { } // namespace esphome -#endif // USE_ESP32 +#endif // USE_ESP32 && USE_ESP32_INTERNAL_GPIO diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 321dd3d498..98aac209ec 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -257,6 +257,7 @@ ESP32_PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA) async def esp32_pin_to_code(config): + cg.add_define("USE_ESP32_INTERNAL_GPIO") var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index f099c68e57..7e97111686 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -22,6 +22,7 @@ from esphome.components.esp32 import ( request_bluetooth, ) from esphome.components.esp32.const import VARIANT_ESP32C2 +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_ON_BOOT, @@ -31,7 +32,8 @@ from esphome.const import ( CONF_NAME, CONF_NAME_ADD_MAC_SUFFIX, ) -from esphome.core import CORE, TimePeriod +from esphome.core import CORE, ID, TimePeriod +from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv from esphome.types import ConfigType @@ -383,7 +385,7 @@ def _validate_key_sizes(config: ConfigType) -> ConfigType: CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes) -def validate_variant(_): +def validate_variant(_: ConfigType) -> None: variant = get_esp32_variant() if variant in NO_BLUETOOTH_VARIANTS: raise cv.Invalid(f"{variant} does not support Bluetooth") @@ -443,7 +445,7 @@ def validate_connection_slots(max_connections: int) -> None: ) -def final_validation(config) -> None: +def final_validation(config: ConfigType) -> None: validate_variant(config) if (name := config.get(CONF_NAME)) is not None: full_config = fv.full_config.get() @@ -518,7 +520,7 @@ def final_validation(config) -> None: FINAL_VALIDATE_SCHEMA = final_validation -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) @@ -605,19 +607,41 @@ async def to_code(config): @automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({})) -async def ble_enabled_to_code(config, condition_id, template_arg, args): +async def ble_enabled_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: return cg.new_Pvariable(condition_id, template_arg) @automation.register_action( "ble.enable", BLEEnableAction, cv.Schema({}), synchronous=True ) -async def ble_enable_to_code(config, action_id, template_arg, args): +async def ble_enable_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: return cg.new_Pvariable(action_id, template_arg) @automation.register_action( "ble.disable", BLEDisableAction, cv.Schema({}), synchronous=True ) -async def ble_disable_to_code(config, action_id, template_arg, args): +async def ble_disable_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: return cg.new_Pvariable(action_id, template_arg) + + +# ble_advertising.cpp is fully #ifdef'd on USE_ESP32_BLE_ADVERTISING, set +# when advertising is enabled here or by esp32_ble_server / esp32_ble_beacon. +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"ble_advertising.cpp": "USE_ESP32_BLE_ADVERTISING"} +) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index e2d79173ff..6e6fb0e30d 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -643,8 +643,28 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa App.wake_loop_threadsafe(); return; + // Log the result of connection parameter updates: a peer can reject or + // never answer an update, and without this the link silently stays on the + // old parameters (visible only as unexplained supervision timeouts). + case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: { + if (param->update_conn_params.status != ESP_BT_STATUS_SUCCESS) { + char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(param->update_conn_params.bda, mac_s); + ESP_LOGW(TAG, "[%s] Conn param update failed, status=%d", mac_s, param->update_conn_params.status); + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + else { + char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(param->update_conn_params.bda, mac_s); + ESP_LOGV(TAG, "[%s] Conn params updated: interval=%u (x1.25ms) latency=%u timeout=%u (x10ms)", mac_s, + param->update_conn_params.conn_int, param->update_conn_params.latency, + param->update_conn_params.timeout); + } +#endif + return; + } + // Ignore these GAP events as they are not relevant for our use case - case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 28c8c7fcf1..c6e34f37ca 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -7,6 +7,7 @@ import logging from esphome import automation import esphome.codegen as cg from esphome.components import ble_device_base, esp32_ble, ota +from esphome.components.ble_device_base import CONF_CONNECTION_SCAN_WINDOW from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW from esphome.components.esp32 import ( add_idf_sdkconfig_option, @@ -38,7 +39,8 @@ from esphome.const import ( CONF_SERVICE_UUID, CONF_TRIGGER_ID, ) -from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, TimePeriod, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.enum import StrEnum from esphome.types import ConfigType @@ -72,8 +74,9 @@ def _get_required_features() -> set[BLEFeatures]: # Slot counters sizing the tracker's StaticVector storage; one request per # registered listener or client. +CLIENT_COUNT_DEFINE = "ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT" _request_listener_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") -_request_client_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") +_request_client_slot = cg.slot_counter(CLIENT_COUNT_DEFINE) def register_ble_features(features: set[BLEFeatures]) -> None: @@ -146,6 +149,7 @@ class TrackerData: """Per-run validation state, namespaced under DOMAIN in CORE.data.""" scan_window_defaulted: bool = False + connection_window_injected: bool = False def _get_data() -> TrackerData: @@ -174,17 +178,34 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType: honors the window strictly (>= 5.5.5); without the arbiter a full-duty scan would starve wifi outright, and a user-set window is never touched. Raising to the interval cannot invalidate the already-validated - parameters, so no re-validation is needed. + parameters, so no re-validation is needed. The connection window is + checked against the window here, after the raise. """ + params = config[CONF_SCAN_PARAMETERS] if ( _get_data().scan_window_defaulted and config.get(CONF_SOFTWARE_COEXISTENCE) and idf_version() >= IDF_SCAN_WINDOW_FIX_VERSION ): - params = config[CONF_SCAN_PARAMETERS] # Copy so the config dump shows a plain value instead of a YAML # anchor/alias pair pointing at the interval. params[CONF_WINDOW] = copy.copy(params[CONF_INTERVAL]) + # Arm the connection-time fallback unless the user set one. Injected + # after validation; safe because it equals the validated window default. + if CONF_CONNECTION_SCAN_WINDOW not in params: + params[CONF_CONNECTION_SCAN_WINDOW] = cv.positive_time_period( + ble_device_base.DEFAULT_SCAN_WINDOW + ) + _get_data().connection_window_injected = True + if ( + connection_window := params.get(CONF_CONNECTION_SCAN_WINDOW) + ) is not None and connection_window > params[CONF_WINDOW]: + # A larger value would widen the scan during connections. + raise cv.Invalid( + f"{CONF_CONNECTION_SCAN_WINDOW} ({connection_window}) needs to be " + f"smaller than the scan window ({params[CONF_WINDOW]})", + path=[CONF_SCAN_PARAMETERS, CONF_CONNECTION_SCAN_WINDOW], + ) return config @@ -193,7 +214,7 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType: # window/interval pairs that collapse to the same 0.625 ms unit count. # The window default is conditional (see _scan_window_default above). SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema( - "320ms", window_default=_scan_window_default + "320ms", window_default=_scan_window_default, connection_window=True ) # Codegen helpers are owned by ble_device_base; kept under the historical names @@ -262,7 +283,7 @@ ESP_BLE_DEVICE_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: # Register the loggers this component needs esp32_ble.register_bt_logger(BTLoggers.BLE_SCAN) @@ -287,6 +308,25 @@ async def to_code(config): cg.add(var.set_scan_duration(params[CONF_DURATION])) cg.add(var.set_scan_interval(ble_device_base.to_ble_units(params[CONF_INTERVAL]))) cg.add(var.set_scan_window(ble_device_base.to_ble_units(params[CONF_WINDOW]))) + if (connection_window := params.get(CONF_CONNECTION_SCAN_WINDOW)) is not None: + # Emitted at FINAL so a scan-only build, where the guarded C++ path + # compiles out, skips the call entirely. + window_units = ble_device_base.to_ble_units(connection_window) + + @coroutine_with_priority(CoroPriority.FINAL) + async def _emit_connection_scan_window() -> None: + if cg.get_slot_count(CLIENT_COUNT_DEFINE): + cg.add(var.set_connection_scan_window(window_units)) + elif not _get_data().connection_window_injected: + # Warn only for a user-set value; the injected default drops silently. + _LOGGER.warning( + "'%s' has no effect because this build has no BLE client " + "components (for example bluetooth_proxy with active " + "connections, or ble_client)", + CONF_CONNECTION_SCAN_WINDOW, + ) + + CORE.add_job(_emit_connection_scan_window) cg.add(var.set_scan_active(params[CONF_ACTIVE])) cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS])) @@ -360,7 +400,7 @@ async def to_code(config): # chance to call register_ble_tracker and register_client before the list is checked # and added to the global defines list. @coroutine_with_priority(CoroPriority.FINAL) -async def _add_ble_features(): +async def _add_ble_features() -> None: # Add feature-specific defines based on what's needed required_features = _get_required_features() # Sensors registered through the neutral ble_device_base path (BLEHub) need @@ -389,8 +429,11 @@ ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( synchronous=True, ) async def esp32_ble_tracker_start_scan_action_to_code( - config, action_id, template_arg, args -): + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_CONTINUOUS], args, cg.bool_) @@ -414,8 +457,11 @@ ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id( synchronous=True, ) async def esp32_ble_tracker_stop_scan_action_to_code( - config, action_id, template_arg, args -): + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 798fd6e0ca..5339565a32 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -122,6 +122,9 @@ void ESP32BLETracker::loop() { // - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_() // - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or // connecting client finishes (state change), or scanner reaches RUNNING/IDLE + // - connection-window restart: scan_params_ is only written in start_scan_() + // (which changes scanner state via set_scanner_state_()), and + // counts.active/disconnecting only change on client state changes // // All conditions that affect the logic below are tied to state changes that increment // state_version_, so the fast path is safe. @@ -144,6 +147,19 @@ void ESP32BLETracker::loop() { (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) { this->handle_scanner_failure_(); } + +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + // The programmed window no longer matches the connection state (typically + // the last connection dropped): restart so the right window applies now + // instead of at the end of the scan period. Continuous only (a user-started + // scan would not restart); !disconnecting matches the restart gate below. + if (this->scanner_state_ == ScannerState::RUNNING && this->scan_continuous_ && !counts.disconnecting && + this->scan_params_.scan_window != this->desired_scan_window_(counts.active)) { + // Same logical scan period continues: no on_scan_end sweeps for this + // restart. Only armed when the stop was issued. + this->skip_next_scan_end_ = this->stop_scan_(); + } +#endif /* Avoid starting the scanner if: @@ -195,19 +211,23 @@ void ESP32BLETracker::stop_scan() { // reason at D themselves, and the user-facing stop action is deliberate. ESP_LOGV(TAG, "Stopping scan."); this->scan_continuous_ = false; +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + // The window-change restart is abandoned with continuous scanning. + this->skip_next_scan_end_ = false; +#endif this->stop_scan_(); } void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); } -void ESP32BLETracker::stop_scan_() { +bool ESP32BLETracker::stop_scan_() { if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { // IDLE means there is nothing to stop; STOPPING means a stop is already in // flight and will finish on its own. Neither is an error. if (this->scanner_state_ != ScannerState::IDLE && this->scanner_state_ != ScannerState::STOPPING) { ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); } - return; + return false; } // Reset timeout state machine when stopping scan this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; @@ -215,8 +235,9 @@ void ESP32BLETracker::stop_scan_() { esp_err_t err = esp_ble_gap_stop_scanning(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_stop_scanning failed: %d", err); - return; + return false; } + return true; } void ESP32BLETracker::start_scan_(bool first) { @@ -230,16 +251,11 @@ void ESP32BLETracker::start_scan_(bool first) { } this->set_scanner_state_(ScannerState::STARTING); ESP_LOGV(TAG, "Starting scan, set scanner state to STARTING."); - if (!first) { -#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT - for (auto *listener : this->listeners_) - listener->on_scan_end(); + if (!first) + this->notify_scan_end_(); +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + this->skip_next_scan_end_ = false; #endif -#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT - for (auto *listener : this->neutral_listeners_) - listener->on_scan_end(); -#endif - } #ifdef USE_ESP32_BLE_DEVICE this->discovered_log_.clear(); #endif @@ -247,7 +263,17 @@ void ESP32BLETracker::start_scan_(bool first) { this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; this->scan_params_.scan_interval = this->scan_interval_; - this->scan_params_.scan_window = this->scan_window_; +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + // Count fresh: an automation can start a scan before loop() refreshes the counts. + const uint32_t window = this->desired_scan_window_(this->count_client_states_().active); + if (window != this->scan_window_) { + // Guarantee the connection airtime instead of scanning wall to wall. + ESP_LOGV(TAG, "Connection active, using %" PRIu32 " unit scan window", window); + } +#else + const uint32_t window = this->scan_window_; +#endif + this->scan_params_.scan_window = window; // Start timeout monitoring in loop() instead of using scheduler // This prevents false reboots when the loop is blocked @@ -408,6 +434,11 @@ void ESP32BLETracker::dump_config() { " Continuous Scanning: %s", this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f, this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + if (this->connection_scan_window_ != 0) { + ESP_LOGCONFIG(TAG, " Connection Scan Window: %.1f ms", this->connection_scan_window_ * 0.625f); + } +#endif ESP_LOGCONFIG(TAG, " Scanner State: %s\n" " Connecting: %d, discovered: %d, disconnecting: %d, active: %d", @@ -487,6 +518,18 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { // Reset timeout state machine instead of cancelling scheduler timeout this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; + this->notify_scan_end_(); + + this->set_scanner_state_(ScannerState::IDLE); +} + +void ESP32BLETracker::notify_scan_end_() { +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + // Window-change restart continues the same scan period; the flag stays set + // across the stop and is cleared by the restart in start_scan_. + if (this->skip_next_scan_end_) + return; +#endif #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT for (auto *listener : this->listeners_) listener->on_scan_end(); @@ -495,8 +538,6 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { for (auto *listener : this->neutral_listeners_) listener->on_scan_end(); #endif - - this->set_scanner_state_(ScannerState::IDLE); } void ESP32BLETracker::handle_scanner_failure_() { @@ -534,6 +575,8 @@ void ESP32BLETracker::try_promote_discovered_clients_() { } ESP_LOGD(TAG, "Promoting client to connect"); + // A connect ends the scan period a window-change restart was continuing. + this->skip_next_scan_end_ = false; #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE this->update_coex_preference_(true); #endif diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 7c3e5538fd..618444e626 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -169,6 +169,9 @@ class ESP32BLETracker final : public Component, void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; } +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + void set_connection_scan_window(uint32_t scan_window) { connection_scan_window_ = scan_window; } +#endif void set_scan_active(bool scan_active) { scan_active_ = scan_active; } bool get_scan_active() const { return scan_active_; } void set_scan_continuous(bool scan_continuous) { scan_continuous_ = scan_continuous; } @@ -226,7 +229,10 @@ class ESP32BLETracker final : public Component, ScannerState get_scanner_state() const { return this->scanner_state_; } protected: - void stop_scan_(); + /// Returns true when a stop was issued to the controller. + bool stop_scan_(); + /// Fire on_scan_end on every listener unless a window-change restart suppressed it. + void notify_scan_end_(); /// Start a single scan by setting up the parameters and doing some esp-idf calls. void start_scan_(bool first); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. @@ -313,6 +319,15 @@ class ESP32BLETracker final : public Component, uint32_t scan_duration_; uint32_t scan_interval_; uint32_t scan_window_; +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + /// Window used while a GATT connection is active; set by the user, or + /// defaulted when the window was raised to full duty (0 = no fallback). + uint32_t connection_scan_window_{0}; + /// The window to scan at for the given number of active GATT connections. + uint32_t desired_scan_window_(uint8_t active) const { + return (this->connection_scan_window_ != 0 && active > 0) ? this->connection_scan_window_ : this->scan_window_; + } +#endif esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; @@ -330,15 +345,20 @@ class ESP32BLETracker final : public Component, /// state_version_ to detect if any state changed since last iteration. uint8_t last_processed_version_{0}; ScannerState scanner_state_{ScannerState::IDLE}; - bool scan_continuous_; - bool scan_active_; + // Packed 1-bit flags. + bool scan_continuous_ : 1; + bool scan_active_ : 1; #ifdef USE_OTA_STATE_LISTENER - bool scan_continuous_before_ota_{false}; + bool scan_continuous_before_ota_ : 1 {false}; +#endif + bool ble_was_disabled_ : 1 {true}; + bool parse_advertisements_ : 1 {false}; +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + /// Suppress the window-change restart's on_scan_end sweeps (stop and start). + bool skip_next_scan_end_ : 1 {false}; #endif - bool ble_was_disabled_{true}; - bool parse_advertisements_{false}; #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE - bool coex_prefer_ble_{false}; + bool coex_prefer_ble_ : 1 {false}; #endif // Scan timeout state machine enum class ScanTimeoutState : uint8_t { @@ -346,10 +366,10 @@ class ESP32BLETracker final : public Component, MONITORING, // Actively monitoring for timeout EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot }; + ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE}; uint32_t scan_start_time_{0}; /// Precomputed timeout value: scan_duration_ * 2000 uint32_t scan_timeout_ms_{0}; - ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE}; }; // NOLINTNEXTLINE diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 7dc61ce382..ab9455250c 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -64,7 +64,7 @@ SDIO_SCHEMA = BASE_SCHEMA.extend( ) -def _validate_sdio(config): +def _validate_sdio(config: ConfigType) -> ConfigType: if config[CONF_BUS_WIDTH] == 4: for pin in (CONF_D1_PIN, CONF_D2_PIN, CONF_D3_PIN): if pin not in config: @@ -98,7 +98,7 @@ SPI_SCHEMA = BASE_SCHEMA.extend( ) -def _validate_spi(config): +def _validate_spi(config: ConfigType) -> ConfigType: variant = config[CONF_VARIANT] defaults = _SPI_VARIANT_DEFAULTS.get(variant, _SPI_DEFAULT) @@ -141,7 +141,7 @@ def _final_validate(config: ConfigType) -> None: FINAL_VALIDATE_SCHEMA = _final_validate -def _configure_sdio(config): +def _configure_sdio(config: ConfigType) -> None: slot = config[CONF_SLOT] esp32.add_idf_sdkconfig_option( f"CONFIG_ESP_HOSTED_SDIO_SLOT_{slot}", @@ -183,7 +183,7 @@ def _configure_sdio(config): ) -def _configure_spi(config): +def _configure_spi(config: ConfigType) -> None: esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_HOST_INTERFACE", True) # SPI mode is set via per-variant choice options variant = config[CONF_VARIANT] @@ -231,7 +231,7 @@ def _configure_spi(config): esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_LOW", True) -async def to_code(config): +async def to_code(config: ConfigType) -> None: add_define("USE_ESP32_HOSTED") transport = config[CONF_TYPE] transport_prefix = "SDIO" if transport == "sdio" else "SPI" diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 351b0869b0..4eb5d1745b 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -135,6 +135,10 @@ void Esp32HostedUpdate::setup() { // Publish state this->status_clear_error(); this->publish_state(); + // Defer so the automation runs on the main loop after setup, not during App.setup() + if (this->state_ == update::UPDATE_STATE_AVAILABLE && this->update_available_trigger_) { + this->defer([this]() { this->update_available_trigger_->trigger(this->update_info_); }); + } #else // HTTP mode: check every 10s until network is ready (max 6 attempts) // Only if update interval is > 1 minute to avoid redundant checks @@ -185,6 +189,8 @@ void Esp32HostedUpdate::check() { return; } + const bool was_available = this->state_ == update::UPDATE_STATE_AVAILABLE; + // Compare versions if (this->update_info_.latest_version.empty() || this->update_info_.latest_version == this->update_info_.current_version) { @@ -197,6 +203,9 @@ void Esp32HostedUpdate::check() { this->update_info_.progress = 0.0f; this->status_clear_error(); this->publish_state(); + if (this->state_ == update::UPDATE_STATE_AVAILABLE && !was_available && this->update_available_trigger_) { + this->update_available_trigger_->trigger(this->update_info_); + } #endif } diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 1076bcabdc..a213a78778 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,17 +1,23 @@ +from collections.abc import Callable, Iterable +from typing import Any + from esphome.components import esp32 import esphome.config_validation as cv from esphome.core import CORE +from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} -def validate_rmt_not_supported(rmt_only_keys): +def validate_rmt_not_supported( + rmt_only_keys: Iterable[str], +) -> Callable[[ConfigType], ConfigType]: """Validate that RMT-only config keys are not used on variants without RMT hardware.""" rmt_only_keys = set(rmt_only_keys) - def _validator(config): + def _validator(config: ConfigType) -> ConfigType: if CORE.is_esp32: variant = esp32.get_esp32_variant() if variant in VARIANTS_NO_RMT: @@ -26,8 +32,8 @@ def validate_rmt_not_supported(rmt_only_keys): return _validator -def validate_clock_resolution(): - def _validator(value): +def validate_clock_resolution() -> Callable[[Any], int]: + def _validator(value: Any) -> int: cv.only_on_esp32(value) value = cv.int_(value) variant = esp32.get_esp32_variant() diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index 10ad339b12..ede6beb9b6 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -1,4 +1,6 @@ +from collections.abc import Callable, Iterable import logging +from typing import Any import esphome.codegen as cg from esphome.components import esp32 @@ -23,6 +25,7 @@ from esphome.const import ( CONF_VOLTAGE_ATTENUATION, ) from esphome.core import TimePeriod +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -181,7 +184,7 @@ EFFECTIVE_HIGH_VOLTAGE = { } -def validate_touch_pad(value): +def validate_touch_pad(value: Any) -> int: value = gpio.gpio_pin_number_validator(value) variant = get_esp32_variant() pads = TOUCH_PADS.get(variant) @@ -192,7 +195,7 @@ def validate_touch_pad(value): return pads[value] # Return integer channel ID -def validate_variant_vars(config): +def validate_variant_vars(config: ConfigType) -> ConfigType: variant = get_esp32_variant() invalid_vars = set() if variant == VARIANT_ESP32: @@ -219,8 +222,8 @@ def validate_variant_vars(config): return config -def validate_voltage(values): - def validator(value): +def validate_voltage(values: Iterable[str]) -> Callable[[Any], str]: + def validator(value: Any) -> str: if isinstance(value, float) and value.is_integer(): value = int(value) value = cv.string(value) @@ -300,7 +303,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: # New unified touch sensor driver include_builtin_idf_component("esp_driver_touch_sens") diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index 75560d71b1..2489c2abc1 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PIN, CONF_THRESHOLD +from esphome.types import ConfigType from . import ESP32TouchComponent, esp32_touch_ns, validate_touch_pad @@ -24,7 +25,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(ESP32TouchBinarySensor).exten ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_ESP32_TOUCH_ID]) var = cg.new_Pvariable( config[CONF_ID], diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 2161a902cb..3dd9750c6f 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -3,6 +3,7 @@ from pathlib import Path import platform import re import subprocess +from typing import Any import esphome.codegen as cg import esphome.config_validation as cv @@ -31,6 +32,7 @@ from esphome.core import ( from esphome.core.config import BOARD_MAX_LENGTH from esphome.helpers import IS_MACOS, copy_file_if_changed from esphome.platformio.toolchain import copy_ccache_script +from esphome.storage_json import StorageJSON from esphome.types import ConfigType from .boards import BOARDS, ESP8266_LD_SCRIPTS @@ -88,7 +90,7 @@ def lambdas_use_scanf_float(config: ConfigType) -> bool: return False -def set_core_data(config): +def set_core_data(config: ConfigType) -> ConfigType: CORE.data[KEY_ESP8266] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266 CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" @@ -102,7 +104,7 @@ def set_core_data(config): return config -def get_download_types(storage_json): +def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: """Binary-download entries for a built ESP8266 firmware. Used by device-builder (esphome/device-builder), via @@ -157,7 +159,7 @@ ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1) -def _arduino_check_versions(value): +def _arduino_check_versions(value: ConfigType) -> ConfigType: value = value.copy() lookups = { "dev": (cv.Version(3, 1, 2), "https://github.com/esp8266/Arduino.git"), @@ -200,7 +202,7 @@ def _arduino_check_versions(value): return value -def _parse_platform_version(value): +def _parse_platform_version(value: Any) -> str: try: # if platform version is a valid version constraint, prefix the default package cv.platformio_version_constraint(value) @@ -275,7 +277,7 @@ def check_rosetta() -> None: @coroutine_with_priority(CoroPriority.PLATFORM) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add(esp8266_ns.setup_preferences()) cg.add_platformio_option("lib_ldf_mode", "off") @@ -504,7 +506,7 @@ ESP8266_EXCEPTION_CODES = { } -def _decode_pc(config, addr): +def _decode_pc(config: ConfigType, addr: str) -> None: from esphome.platformio import toolchain idedata = toolchain.get_idedata(config) @@ -525,7 +527,7 @@ def _decode_pc(config, addr): _LOGGER.warning("Decoded %s", translation) -def _parse_register(config, regex, line): +def _parse_register(config: ConfigType, regex: re.Pattern[str], line: str) -> None: match = regex.match(line) if match is not None: _decode_pc(config, match.group(1)) @@ -549,7 +551,7 @@ STACKTRACE_BAD_ALLOC_RE = re.compile( STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") -def process_stacktrace(config, line, backtrace_state): +def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool: line = line.strip() # ESP8266 Exception type match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 91b0cf9082..dc79043f21 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -118,8 +118,6 @@ static const LogString *get_exception_cause(uint32_t cause) { } static const LogString *get_reset_reason(uint32_t reason) { - if (reason == REASON_WDT_RST) - return LOG_STR("Hardware WDT"); if (reason == REASON_EXCEPTION_RST) return LOG_STR("Exception"); if (reason == REASON_SOFT_WDT_RST) @@ -162,13 +160,20 @@ void crash_handler_log() { if (!is_crash_reason(resetInfo.reason)) return; + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + if (resetInfo.reason == REASON_WDT_RST) { + // A hardware WDT reset happens entirely in hardware: the postmortem hook + // never runs, so rst_info epc1/exccause and the RTC backtrace are + // leftovers from an earlier crash. Don't misattribute them (#18596). + ESP_LOGE(TAG, " Reason: Hardware WDT (no crash state is recorded for hardware WDT resets)"); + return; + } + // Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost). // Both resetInfo and RTC data survive until the next reset, so this can be // called multiple times (logger init + API subscribe) with the same result. uint32_t backtrace[MAX_BACKTRACE]; uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE); - - ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); // GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific // ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match // the Arduino core's postmortem handler behavior. diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 64be4a6495..356af6e006 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -1,5 +1,6 @@ from dataclasses import dataclass import logging +from typing import Any from esphome import pins import esphome.codegen as cg @@ -18,6 +19,8 @@ from esphome.const import ( PLATFORM_ESP8266, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import boards from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns @@ -27,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin) -def _lookup_pin(value): +def _lookup_pin(value: str) -> int: board = CORE.data[KEY_ESP8266][KEY_BOARD] board_pins = boards.ESP8266_BOARD_PINS.get(board, {}) @@ -42,7 +45,7 @@ def _lookup_pin(value): raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.") -def _translate_pin(value): +def _translate_pin(value: Any) -> int: if isinstance(value, dict) or value is None: raise cv.Invalid( "This variable only supports pin numbers, not full pin schemas " @@ -69,7 +72,7 @@ _ESP_SDIO_PINS = { } -def validate_gpio_pin(value): +def validate_gpio_pin(value: Any) -> int: value = _translate_pin(value) if value < 0 or value > 17: raise cv.Invalid(f"ESP8266: Invalid pin number: {value}") @@ -86,7 +89,7 @@ def validate_gpio_pin(value): return value -def validate_supports(value): +def validate_supports(value: ConfigType) -> ConfigType: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] @@ -160,7 +163,7 @@ class PinInitialState: @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA) -async def esp8266_pin_to_code(config): +async def esp8266_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] mode = config[CONF_MODE] @@ -192,7 +195,7 @@ async def esp8266_pin_to_code(config): @coroutine_with_priority(CoroPriority.WORKAROUNDS) -async def add_pin_initial_states_array(): +async def add_pin_initial_states_array() -> None: # Add includes at the very end, so that they override everything initial_states: list[PinInitialState] = CORE.data[KEY_ESP8266][ KEY_PIN_INITIAL_STATES diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index f119a6ba9f..dd151a3e04 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -4,11 +4,14 @@ from esphome.components import output from esphome.components.esp8266.const import require_waveform import esphome.config_validation as cv from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NUMBER, CONF_PIN +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["esp8266"] -def valid_pwm_pin(value): +def valid_pwm_pin(value: ConfigType) -> ConfigType: num = value[CONF_NUMBER] cv.one_of(0, 1, 2, 3, 4, 5, 9, 10, 12, 13, 14, 15, 16)(num) return value @@ -35,7 +38,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config) -> None: +async def to_code(config: ConfigType) -> None: require_waveform() var = cg.new_Pvariable(config[CONF_ID]) @@ -59,7 +62,12 @@ async def to_code(config) -> None: ), synchronous=True, ) -async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): +async def esp8266_set_frequency_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_) diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index a489651b59..46810d422d 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -1,9 +1,14 @@ +from typing import Any + from esphome.automation import Action, register_action import esphome.codegen as cg from esphome.components.esp32 import VARIANT_ESP32P4, only_on_variant import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID, CONF_VOLTAGE +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.final_validate import full_config +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] @@ -22,7 +27,7 @@ CONF_PASSTHROUGH = "passthrough" adjusted_ids = set() -def validate_ldo_voltage(value): +def validate_ldo_voltage(value: Any) -> str | float: if isinstance(value, str) and value.lower() == CONF_PASSTHROUGH: return CONF_PASSTHROUGH value = cv.voltage(value) @@ -33,7 +38,7 @@ def validate_ldo_voltage(value): ) -def validate_ldo_config(config): +def validate_ldo_config(config: ConfigType) -> ConfigType: channel = config[CONF_CHANNEL] allow_internal = config[CONF_ALLOW_INTERNAL_CHANNEL] if allow_internal and channel not in CHANNELS_INTERNAL: @@ -77,7 +82,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(configs): +async def to_code(configs: list[ConfigType]) -> None: for config in configs: var = cg.new_Pvariable(config[CONF_ID], config[CONF_CHANNEL]) await cg.register_component(var, config) @@ -89,7 +94,7 @@ async def to_code(configs): cg.add(var.set_adjustable(config[CONF_ADJUSTABLE])) -def final_validate(configs): +def final_validate(configs: list[ConfigType]) -> None: for channel in CHANNELS: used = [config for config in configs if config[CONF_CHANNEL] == channel] if len(used) > 1: @@ -112,7 +117,7 @@ def final_validate(configs): FINAL_VALIDATE_SCHEMA = final_validate -def adjusted_ldo_id(value): +def adjusted_ldo_id(value: Any) -> ID: value = cv.use_id(EspLdo)(value) adjusted_ids.add(value) return value @@ -131,7 +136,12 @@ def adjusted_ldo_id(value): ), synchronous=True, ) -async def ldo_voltage_adjust_to_code(config, action_id, template_arg, args): +async def ldo_voltage_adjust_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: parent = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, parent) template_ = await cg.templatable(config[CONF_VOLTAGE], args, cg.float_) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index cab725f704..74f84b71fb 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -398,7 +398,7 @@ void ESPHomeOTAComponent::handle_data_() { this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif - // begin() may block for a few seconds while it locks flash. + // begin() returns quickly; flash sectors are erased incrementally during write(). error_code = this->backend_->begin(ota_size, ota_type); if (error_code != ota::OTA_RESPONSE_OK) goto error; // NOLINT(cppcoreguidelines-avoid-goto) @@ -588,8 +588,6 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { } float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } -uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } -void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 0053ca6969..979e3f2d7d 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -39,14 +39,14 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { #endif // USE_OTA_PASSWORD /// Manually set the port OTA should listen on - void set_port(uint16_t port); + void set_port(uint16_t port) { this->port_ = port; } void setup() override; void dump_config() override; float get_setup_priority() const override; void loop() override; - uint16_t get_port() const; + uint16_t get_port() const { return this->port_; } protected: void handle_handshake_(); diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 373ef345d1..ee3732c406 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation, core import esphome.codegen as cg from esphome.components import wifi @@ -14,6 +16,7 @@ from esphome.const import ( CONF_WIFI, ) from esphome.core import CORE, HexInt +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -78,7 +81,7 @@ CONF_CONTINUE_ON_ERROR = "continue_on_error" CONF_WAIT_FOR_SENT = "wait_for_sent" -def _validate_max_payload_size(value: int) -> int: +def _validate_max_payload_size(value: Any) -> int: if value > ESPNOW_PAYLOAD_V1: return cv.require_framework_version( esp_idf=cv.Version(5, 4, 0), @@ -88,7 +91,7 @@ def _validate_max_payload_size(value: int) -> int: return value -def validate_channel(value): +def validate_channel(value: Any) -> int: if value is None: raise cv.Invalid("channel is required if wifi is not configured") return wifi.validate_channel(value) @@ -129,7 +132,7 @@ CONFIG_SCHEMA = cv.All( ) -async def _trigger_to_code(config): +async def _trigger_to_code(config: ConfigType) -> MockObj: if address := config.get(CONF_ADDRESS): address = address.parts trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], address) @@ -145,7 +148,7 @@ async def _trigger_to_code(config): return trigger -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -180,13 +183,13 @@ async def to_code(config): # ========================================== A C T I O N S ================================================ -def validate_peer(value): +def validate_peer(value: Any) -> Any: if isinstance(value, cv.Lambda): return cv.returning_lambda(value) return cv.mac_address(value) -def _validate_raw_data(value): +def _validate_raw_data(value: Any) -> str | list: if isinstance(value, str): if len(value) > MAX_ESPNOW_PACKET_SIZE: raise cv.Invalid( @@ -204,7 +207,9 @@ def _validate_raw_data(value): ) -async def register_peer(var, config, args): +async def register_peer( + var: MockObj, config: ConfigType, args: TemplateArgsType +) -> None: peer = config[CONF_ADDRESS] if isinstance(peer, core.MACAddress): peer = [HexInt(p) for p in peer.parts] @@ -231,7 +236,7 @@ SEND_SCHEMA = PEER_SCHEMA.extend( ) -def _validate_send_action(config): +def _validate_send_action(config: ConfigType) -> ConfigType: if not config[CONF_WAIT_FOR_SENT] and not config[CONF_CONTINUE_ON_ERROR]: raise cv.Invalid( f"'{CONF_CONTINUE_ON_ERROR}' cannot be false if '{CONF_WAIT_FOR_SENT}' is false as the automation will not wait for the failed result.", @@ -267,7 +272,7 @@ async def send_action( action_id: core.ID, template_arg: cg.TemplateArguments, args: list[tuple], -): +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -316,7 +321,7 @@ async def peer_action( action_id: core.ID, template_arg: cg.TemplateArguments, args: list[tuple], -): +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) await register_peer(var, config, args) @@ -341,7 +346,7 @@ async def channel_action( action_id: core.ID, template_arg: cg.TemplateArguments, args: list[tuple], -): +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index df9a1b8668..ecf0f79e4a 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -129,14 +129,17 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int ESPNowComponent::ESPNowComponent() { global_esp_now = this; } void ESPNowComponent::dump_config() { - uint32_t version = 0; - esp_now_get_version(&version); - ESP_LOGCONFIG(TAG, "espnow:"); - if (this->is_disabled()) { - ESP_LOGCONFIG(TAG, " Disabled"); + // Only report driver details once enabled; with enable_on_boot: false the + // Wi-Fi driver is not initialized yet and esp_now_get_version() would crash, + // and after a failed enable_() the values would be meaningless. + if (this->state_ != ESPNOW_STATE_ENABLED) { + // OFF here means enable_() failed; the core logs the FAILED marker separately + ESP_LOGCONFIG(TAG, " %s", this->is_disabled() ? LOG_STR_LITERAL("Disabled") : LOG_STR_LITERAL("Not enabled")); return; } + uint32_t version = 0; + esp_now_get_version(&version); char own_addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(this->own_address_, own_addr_buf); ESP_LOGCONFIG(TAG, diff --git a/esphome/components/espnow/packet_transport/__init__.py b/esphome/components/espnow/packet_transport/__init__.py index e6d66440db..ee4706ca1c 100644 --- a/esphome/components/espnow/packet_transport/__init__.py +++ b/esphome/components/espnow/packet_transport/__init__.py @@ -9,6 +9,7 @@ from esphome.components.packet_transport import ( import esphome.config_validation as cv from esphome.core import HexInt from esphome.cpp_types import PollingComponent +from esphome.types import ConfigType from .. import ESPNowComponent, espnow_ns @@ -28,7 +29,7 @@ CONFIG_SCHEMA = transport_schema(ESPNowTransport).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: """Set up the ESP-NOW transport component.""" var, _ = await new_packet_transport(config) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 5eda0fc12c..cd5904f501 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -48,10 +48,12 @@ from esphome.const import ( ) from esphome.core import ( CORE, + ID, CoroPriority, TimePeriodMilliseconds, coroutine_with_priority, ) +from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv from esphome.types import ConfigType @@ -276,7 +278,7 @@ def _validate_spi_interface(config: ConfigType) -> ConfigType: return config -def _validate(config): +def _validate(config: ConfigType) -> ConfigType: if CONF_USE_ADDRESS not in config: if CONF_MANUAL_IP in config: use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP]) @@ -441,7 +443,7 @@ GENERIC_SCHEMA = cv.All( ) -def _spi_schema(default_clock: str = "26.67MHz", max_clock: int = int(80e6)): +def _spi_schema(default_clock: str = "26.67MHz", max_clock: int = int(80e6)) -> cv.All: return cv.All( BASE_SCHEMA.extend( cv.Schema( @@ -517,7 +519,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate_spi(config): +def _final_validate_spi(config: ConfigType) -> None: if not CORE.is_esp32: return # SPI interface validation is ESP32-only if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: @@ -537,7 +539,7 @@ def _final_validate_spi(config): ) -def manual_ip(config): +def manual_ip(config: ConfigType) -> cg.StructInitializer: return cg.StructInitializer( ManualIP, ("static_ip", ip_address_literal(config[CONF_STATIC_IP])), @@ -548,7 +550,7 @@ def manual_ip(config): ) -def phy_register(address: int, value: int, page: int): +def phy_register(address: int, value: int, page: int) -> cg.StructInitializer: return cg.StructInitializer( PHYRegister, ("address", address), @@ -558,7 +560,7 @@ def phy_register(address: int, value: int, page: int): @coroutine_with_priority(CoroPriority.COMMUNICATION) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) # Apply network priority before register_component (which emits the user's @@ -610,7 +612,7 @@ async def to_code(config): CORE.add_job(final_step) -async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: +async def _to_code_esp32(var: cg.MockObj, config: ConfigType) -> None: from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, @@ -698,7 +700,7 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: add_idf_component(name=component.name, ref=component.version) -async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: +async def _to_code_rp2040(var: cg.MockObj, config: ConfigType) -> None: cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN])) @@ -793,7 +795,7 @@ FINAL_VALIDATE_SCHEMA = _final_validate @coroutine_with_priority(CoroPriority.FINAL) -async def final_step(): +async def final_step() -> None: """Final code generation step to configure optional Ethernet features.""" if ip_state_count := CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0): cg.add_define("USE_ETHERNET_IP_STATE_LISTENERS") @@ -811,6 +813,10 @@ _platform_filter = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO, }, + "w5500_custom_spi.cpp": { + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP32_ARDUINO, + }, } ) @@ -830,13 +836,23 @@ def _filter_source_files() -> list[str]: # to avoid shadowing. Native IDF builds always need the custom driver. if cv.Version(5, 4, 2) <= idf_version() < cv.Version(6, 0, 0): excluded.append("esp_eth_phy_jl1101.c") + # The custom W5500 SPI driver is fully #ifdef'd on USE_ESP32 and + # USE_ETHERNET_W5500 (the platform filter map above handles non-ESP32); + # skip it entirely for the other ethernet types. + if eth_type != "W5500": + excluded.append("w5500_custom_spi.cpp") return excluded FILTER_SOURCE_FILES = _filter_source_files -async def _new_pvariable_to_code(config, id_, template_arg, args): +async def _new_pvariable_to_code( + config: ConfigType, + id_: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: return cg.new_Pvariable(id_, template_arg) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 42cb0b3cfc..14a4fd660b 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -10,14 +10,6 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non- EthernetComponent::EthernetComponent() { global_eth_component = this; } -float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } - -void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } - -#ifdef USE_ETHERNET_MANUAL_IP -void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } -#endif - #ifdef USE_ETHERNET_IP_STATE_LISTENERS void EthernetComponent::notify_ip_state_listeners_() { auto ips = this->get_ip_addresses(); diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 646e0af8e6..1482e7a828 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -125,7 +125,7 @@ class EthernetComponent final : public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::ETHERNET; } void on_powerdown() override { powerdown(); } bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } @@ -146,9 +146,9 @@ class EthernetComponent final : public Component { esp_netif_t *get_esp_netif() { return this->eth_netif_; } #endif - void set_type(EthernetType type); + void set_type(EthernetType type) { this->type_ = type; } #ifdef USE_ETHERNET_MANUAL_IP - void set_manual_ip(const ManualIP &manual_ip); + void set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } #endif void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } @@ -159,9 +159,6 @@ class EthernetComponent final : public Component { const char *get_use_address() const { return this->use_address_; } void set_use_address(const char *use_address) { this->use_address_ = use_address; } void get_eth_mac_address_raw(uint8_t *mac); - // Remove before 2026.9.0 - ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") - std::string get_eth_mac_address_pretty(); const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); eth_speed_t get_link_speed(); @@ -171,35 +168,35 @@ class EthernetComponent final : public Component { esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; } #ifdef USE_ETHERNET_SPI - void set_clk_pin(uint8_t clk_pin); - void set_miso_pin(uint8_t miso_pin); - void set_mosi_pin(uint8_t mosi_pin); - void set_cs_pin(uint8_t cs_pin); - void set_interrupt_pin(uint8_t interrupt_pin); - void set_reset_pin(uint8_t reset_pin); - void set_clock_speed(int clock_speed); - void set_interface(spi_host_device_t interface); + void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } + void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } + void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } + void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } + void set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } + void set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } + void set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } + void set_interface(spi_host_device_t interface) { this->interface_ = interface; } #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT - void set_polling_interval(uint32_t polling_interval); + void set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } #endif #else - void set_phy_addr(uint8_t phy_addr); - void set_power_pin(int power_pin); - void set_mdc_pin(uint8_t mdc_pin); - void set_mdio_pin(uint8_t mdio_pin); - void set_clk_pin(uint8_t clk_pin); - void set_clk_mode(emac_rmii_clock_mode_t clk_mode); + void set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } + void set_power_pin(int power_pin) { this->power_pin_ = power_pin; } + void set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } + void set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } + void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } + void set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } void add_phy_register(PHYRegister register_value); #endif // USE_ETHERNET_SPI #endif // USE_ESP32 #ifdef USE_RP2 - void set_clk_pin(uint8_t clk_pin); - void set_miso_pin(uint8_t miso_pin); - void set_mosi_pin(uint8_t mosi_pin); - void set_cs_pin(uint8_t cs_pin); - void set_interrupt_pin(int8_t interrupt_pin); - void set_reset_pin(int8_t reset_pin); + void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } + void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } + void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } + void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } + void set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } + void set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; } #endif // USE_RP2 #ifdef USE_ETHERNET_IP_STATE_LISTENERS diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index 0220d6a19b..069478e70c 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -908,25 +908,7 @@ void EthernetComponent::dump_connect_params_() { #endif /* USE_NETWORK_IPV6 */ } -#ifdef USE_ETHERNET_SPI -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } -void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } -void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } -void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } -void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } -void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } -void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; } -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT -void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } -#endif -#else -void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } -void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } -void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } -void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } +#ifndef USE_ETHERNET_SPI void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif @@ -946,11 +928,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); } -std::string EthernetComponent::get_eth_mac_address_pretty() { - char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); -} - const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( std::span buf) { uint8_t mac[MAC_ADDRESS_SIZE]; diff --git a/esphome/components/ethernet/ethernet_component_rp2.cpp b/esphome/components/ethernet/ethernet_component_rp2.cpp index 119e447689..94d84cc891 100644 --- a/esphome/components/ethernet/ethernet_component_rp2.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2.cpp @@ -249,11 +249,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { } } -std::string EthernetComponent::get_eth_mac_address_pretty() { - char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); -} - const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( std::span buf) { uint8_t mac[MAC_ADDRESS_SIZE]; @@ -355,13 +350,6 @@ void EthernetComponent::dump_connect_params_() { this->get_eth_mac_address_pretty_into_buffer(mac_buf)); } -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } -void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } -void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } -void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } -void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; } - void EthernetComponent::enable() { // RP2040 uses arduino-pico's LwipIntfDev which manages link state internally; // there is no clean enable/disable hook today. The YAML option is accepted on diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index e205e4b910..881107b713 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -16,14 +16,15 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_MOTION, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_device_class, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@nohat"] IS_PLATFORM_COMPONENT = True @@ -93,7 +94,9 @@ _CALLBACK_AUTOMATIONS = ( @setup_entity("event") -async def setup_event_core_(var, config, *, event_types: list[str]): +async def setup_event_core_( + var: MockObj, config: ConfigType, *, event_types: list[str] +) -> None: await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) cg.add(var.set_event_types(event_types)) @@ -108,7 +111,9 @@ async def setup_event_core_(var, config, *, event_types: list[str]): await web_server.add_entity_config(var, web_server_config) -async def register_event(var, config, *, event_types: list[str]): +async def register_event( + var: MockObj, config: ConfigType, *, event_types: list[str] +) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) queue_entity_register("event", config) @@ -116,7 +121,7 @@ async def register_event(var, config, *, event_types: list[str]): await setup_event_core_(var, config, event_types=event_types) -async def new_event(config, *, event_types: list[str]): +async def new_event(config: ConfigType, *, event_types: list[str]) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) await register_event(var, config, event_types=event_types) return var @@ -133,7 +138,12 @@ TRIGGER_EVENT_SCHEMA = cv.Schema( @automation.register_action( "event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA, synchronous=True ) -async def event_fire_to_code(config, action_id, template_arg, args): +async def event_fire_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) templ = await cg.templatable(config[CONF_EVENT_TYPE], args, cg.std_string) @@ -142,5 +152,5 @@ async def event_fire_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(event_ns.using) diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index d5d5d2ecb5..a9064eb18f 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.final_validate import full_config +from esphome.types import ConfigType CODEOWNERS = ["@anatoly-savchenkov"] @@ -23,7 +24,7 @@ CONF_RESETS_REQUIRED = "resets_required" CONF_ON_INCREMENT = "on_increment" -def _validate(config): +def _validate(config: ConfigType) -> ConfigType: if CONF_RESETS_REQUIRED in config: return cv.only_on( [ @@ -60,7 +61,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config) -> None: +def _final_validate(config: ConfigType) -> None: if CORE.is_esp8266 and CONF_RESETS_REQUIRED in config: fconfig = full_config.get() if not fconfig.get_config_for_path([KEY_ESP8266, CONF_RESTORE_FROM_FLASH]): @@ -81,7 +82,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: if reset_count := config.get(CONF_RESETS_REQUIRED): var = cg.new_Pvariable( config[CONF_ID], diff --git a/esphome/components/factory_reset/button/__init__.py b/esphome/components/factory_reset/button/__init__.py index 61df5f297b..040614c151 100644 --- a/esphome/components/factory_reset/button/__init__.py +++ b/esphome/components/factory_reset/button/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import factory_reset_ns @@ -22,7 +23,7 @@ CONFIG_SCHEMA = button.button_schema( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await button.register_button(var, config) diff --git a/esphome/components/factory_reset/switch/__init__.py b/esphome/components/factory_reset/switch/__init__.py index a384a57f80..69a635a917 100644 --- a/esphome/components/factory_reset/switch/__init__.py +++ b/esphome/components/factory_reset/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT +from esphome.types import ConfigType from .. import factory_reset_ns @@ -17,6 +18,6 @@ CONFIG_SCHEMA = switch.switch_schema( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await switch.new_switch(config) await cg.register_component(var, config) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 853bf94ffe..7dc0b5c6fe 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -153,11 +153,6 @@ void FanRestoreState::apply(Fan &fan) { fan.publish_state(); } -FanCall Fan::turn_on() { return this->make_call().set_state(true); } -FanCall Fan::turn_off() { return this->make_call().set_state(false); } -FanCall Fan::toggle() { return this->make_call().set_state(!this->state); } -FanCall Fan::make_call() { return FanCall(*this); } - const char *Fan::find_preset_mode_(const char *preset_mode) { return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0); } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 3d731e6eb0..106e6e74cd 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -115,10 +115,10 @@ class Fan : public EntityBase { /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; - FanCall turn_on(); - FanCall turn_off(); - FanCall toggle(); - FanCall make_call(); + FanCall turn_on() { return this->make_call().set_state(true); } + FanCall turn_off() { return this->make_call().set_state(false); } + FanCall toggle() { return this->make_call().set_state(!this->state); } + FanCall make_call() { return FanCall(*this); } /// Register a callback that will be called each time the state changes. template void add_on_state_callback(F &&callback) { diff --git a/esphome/components/file/image.py b/esphome/components/file/image.py index feced063d0..7cef7c754a 100644 --- a/esphome/components/file/image.py +++ b/esphome/components/file/image.py @@ -5,6 +5,7 @@ import io import logging from pathlib import Path import re +from typing import Any from PIL import Image, UnidentifiedImageError @@ -75,12 +76,12 @@ def compute_local_image_path(value: str | ConfigType) -> Path: return external_files.compute_local_file_path(DOMAIN, url) -def local_path(value): +def local_path(value: str | ConfigType) -> str: value = value[CONF_PATH] if isinstance(value, dict) else value return str(CORE.relative_config_path(value)) -def download_file(url, path): +def download_file(url: str, path: Path) -> str: # The shared NETWORK_TIMEOUT applies; a per-caller timeout would be # silently ignored on a per-run memo hit anyway (memos key by path). external_files.download_content(url, path) @@ -98,7 +99,7 @@ def download_gh_svg(value: str | ConfigType, source: str) -> str: return download_file(url, path) -def download_image(value): +def download_image(value: str | ConfigType) -> str: value = value[CONF_URL] if isinstance(value, dict) else value return download_file(value, compute_local_image_path(value)) @@ -146,7 +147,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None: PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref) -def validate_file_shorthand(value): +def validate_file_shorthand(value: Any) -> str: value = cv.string_strict(value) if (remote := _parse_remote_shorthand(value)) is not None: return download_file(remote.url, remote.path) @@ -163,8 +164,8 @@ LOCAL_SCHEMA = cv.All( ) -def mdi_schema(source): - def validate_mdi(value): +def mdi_schema(source: str) -> cv.All: + def validate_mdi(value: ConfigType) -> str: return download_gh_svg(value, source) return cv.All( @@ -259,7 +260,9 @@ async def new_image(config: ConfigType) -> MockObj: return var -async def write_image(config, all_frames=False): +async def write_image( + config: ConfigType, all_frames: bool = False +) -> tuple[MockObj, int, int, MockObj, MockObj, int]: path = Path(config[CONF_FILE]) if not path.is_file(): raise core.EsphomeError(f"Could not load image file {path}") diff --git a/esphome/components/gdk101/__init__.py b/esphome/components/gdk101/__init__.py index 878f27bc44..f98af3f863 100644 --- a/esphome/components/gdk101/__init__.py +++ b/esphome/components/gdk101/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType CODEOWNERS = ["@Szewcson"] @@ -26,7 +27,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/gdk101/binary_sensor.py b/esphome/components/gdk101/binary_sensor.py index a80487977f..14f5fa0e1c 100644 --- a/esphome/components/gdk101/binary_sensor.py +++ b/esphome/components/gdk101/binary_sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( ENTITY_CATEGORY_DIAGNOSTIC, ICON_VIBRATE, ) +from esphome.types import ConfigType from . import CONF_GDK101_ID, GDK101Component @@ -24,7 +25,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_GDK101_ID]) var = await binary_sensor.new_binary_sensor(config[CONF_VIBRATIONS]) cg.add(hub.set_vibration_binary_sensor(var)) diff --git a/esphome/components/gdk101/sensor.py b/esphome/components/gdk101/sensor.py index 6cf89e0fd4..4ed081a7be 100644 --- a/esphome/components/gdk101/sensor.py +++ b/esphome/components/gdk101/sensor.py @@ -15,6 +15,7 @@ from esphome.const import ( UNIT_MICROSILVERTS_PER_HOUR, UNIT_SECOND, ) +from esphome.types import ConfigType from . import CONF_GDK101_ID, GDK101Component @@ -59,7 +60,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_GDK101_ID]) if radiation_dose_per_1m := config.get(CONF_RADIATION_DOSE_PER_1M): diff --git a/esphome/components/gdk101/text_sensor.py b/esphome/components/gdk101/text_sensor.py index 703e68493a..bdef2466df 100644 --- a/esphome/components/gdk101/text_sensor.py +++ b/esphome/components/gdk101/text_sensor.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP +from esphome.types import ConfigType from . import CONF_GDK101_ID, GDK101Component @@ -17,7 +18,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_GDK101_ID]) var = await text_sensor.new_text_sensor(config[CONF_VERSION]) cg.add(hub.set_fw_version_text_sensor(var)) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index 46725fe6dd..bd6bc5f783 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -8,7 +8,8 @@ from esphome.const import ( CONF_TYPE, CONF_VALUE, ) -from esphome.core import CoroPriority, coroutine_with_priority +from esphome.core import ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -62,7 +63,7 @@ CONFIG_SCHEMA = _globals_schema # Run with low priority so that namespaces are registered first @coroutine_with_priority(CoroPriority.LATE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: type_ = cg.RawExpression(config[CONF_TYPE]) restore = config[CONF_RESTORE_VALUE] @@ -104,7 +105,12 @@ async def to_code(config): ), synchronous=True, ) -async def globals_set_to_code(config, action_id, template_arg, args): +async def globals_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) template_arg = cg.TemplateArguments(full_id.type, *template_arg) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 703806670c..8a40e4e732 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_PIN, ) from esphome.core import CORE +from esphome.types import ConfigType from .. import gpio_ns @@ -68,7 +69,7 @@ def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool: return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users) -def _final_validate(config) -> None: +def _final_validate(config: ConfigType) -> None: use_interrupt = config[CONF_USE_INTERRUPT] if not use_interrupt: return @@ -124,7 +125,7 @@ def _final_validate(config) -> None: FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) @@ -132,6 +133,7 @@ async def to_code(config): cg.add(var.set_pin(pin)) if config[CONF_USE_INTERRUPT]: + cg.add_define("USE_GPIO_BINARY_SENSOR_INTERRUPT") cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) else: cg.add(var.set_use_interrupt(False)) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index ff07d76901..9d044dca2d 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -7,6 +7,7 @@ namespace esphome::gpio { static const char *const TAG = "gpio.binary_sensor"; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT // Interrupt type strings indexed by edge-triggered InterruptType values: // indices 1-3: RISING_EDGE, FALLING_EDGE, ANY_EDGE; other values (e.g. level-triggered) map to UNKNOWN (index 0). PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_EDGE", "ANY_EDGE"); @@ -19,7 +20,9 @@ static const LogString *gpio_mode_to_string(bool use_interrupt) { return use_interrupt ? LOG_STR("interrupt") : LOG_STR("polling"); } #endif +#endif +#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { bool new_state = arg->isr_pin_.digital_read(); if (new_state != arg->state_) { @@ -43,28 +46,36 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) { // Attach interrupt - from this point on, any changes will be caught by the interrupt pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, this->interrupt_type_); } +#endif // USE_GPIO_BINARY_SENSOR_INTERRUPT void GPIOBinarySensor::setup() { +#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT if (this->store_.use_interrupt_) { auto *internal_pin = static_cast(this->pin_); this->store_.setup(internal_pin, this); this->publish_initial_state(this->store_.get_state()); - } else { - this->pin_->setup(); - this->publish_initial_state(this->pin_->digital_read()); + return; } +#endif + this->pin_->setup(); + this->publish_initial_state(this->pin_->digital_read()); } void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); +#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_))); if (this->store_.use_interrupt_) { ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->store_.interrupt_type_))); } +#else + ESP_LOGCONFIG(TAG, " Mode: polling"); +#endif } void GPIOBinarySensor::loop() { +#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT if (this->store_.use_interrupt_) { if (this->store_.is_changed()) { // Clear the flag immediately to minimize the window where we might miss changes @@ -78,9 +89,10 @@ void GPIOBinarySensor::loop() { // No changes, disable the loop until the next interrupt this->disable_loop(); } - } else { - this->publish_state(this->pin_->digital_read()); + return; } +#endif + this->publish_state(this->pin_->digital_read()); } float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 100edb4cca..956443fab5 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" @@ -10,6 +11,7 @@ namespace esphome::gpio { // Store class for ISR data and configuration (no vtables, ISR-safe) class GPIOBinarySensorStore { public: +#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT void setup(InternalGPIOPin *pin, Component *component); static void gpio_intr(GPIOBinarySensorStore *arg); @@ -29,15 +31,18 @@ class GPIOBinarySensorStore { // Separate method to clear the flag this->changed_ = false; } +#endif protected: friend class GPIOBinarySensor; +#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT ISRInternalGPIOPin isr_pin_; Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() volatile bool state_{false}; volatile bool changed_{false}; - bool use_interrupt_{true}; gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; + bool use_interrupt_{true}; +#endif }; class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component { @@ -46,8 +51,14 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon // Interrupts are only detached on reboot when memory is cleared anyway. void set_pin(GPIOPin *pin) { this->pin_ = pin; } +#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; } void set_interrupt_type(gpio::InterruptType type) { this->store_.interrupt_type_ = type; } +#else + // Polling-only build: codegen still emits set_use_interrupt(false) calls, + // so keep the setter as an inlined no-op instead of storing the flag. + void set_use_interrupt(bool /*use_interrupt*/) {} +#endif // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup pin diff --git a/esphome/components/gpio/one_wire/__init__.py b/esphome/components/gpio/one_wire/__init__.py index e2bb94dd66..feb8b53dff 100644 --- a/esphome/components/gpio/one_wire/__init__.py +++ b/esphome/components/gpio/one_wire/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components.one_wire import OneWireBus import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PIN +from esphome.types import ConfigType from .. import gpio_ns @@ -18,7 +19,7 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/gpio/output/__init__.py b/esphome/components/gpio/output/__init__.py index 786e04bac0..ab242c643f 100644 --- a/esphome/components/gpio/output/__init__.py +++ b/esphome/components/gpio/output/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PIN +from esphome.types import ConfigType from .. import gpio_ns @@ -16,7 +17,7 @@ CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await output.register_output(var, config) await cg.register_component(var, config) diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index 9462cd0161..2e0b0969bc 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import CONF_INTERLOCK, CONF_PIN +from esphome.types import ConfigType from .. import gpio_ns @@ -24,7 +25,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await switch.new_switch(config) await cg.register_component(var, config) diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py index 0749d7e2a3..1b99491f9c 100644 --- a/esphome/components/graph/__init__.py +++ b/esphome/components/graph/__init__.py @@ -29,6 +29,7 @@ from esphome.const import ( CONF_X_GRID, CONF_Y_GRID, ) +from esphome.types import ConfigType CODEOWNERS = ["@synco"] @@ -115,7 +116,9 @@ GRAPH_SCHEMA = cv.Schema( ) -def _relocate_fields_to_subfolder(config, subfolder, subschema): +def _relocate_fields_to_subfolder( + config: ConfigType, subfolder: str, subschema: cv.Schema +) -> ConfigType: fields = [k.schema for k in subschema.schema] fields.remove(CONF_ID) if subfolder in config: @@ -138,7 +141,7 @@ def _relocate_fields_to_subfolder(config, subfolder, subschema): return config -def _relocate_trace(config): +def _relocate_trace(config: ConfigType) -> ConfigType: return _relocate_fields_to_subfolder(config, CONF_TRACES, GRAPH_TRACE_SCHEMA) @@ -148,7 +151,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_duration(config[CONF_DURATION])) cg.add(var.set_width(config[CONF_WIDTH])) diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 8ea8677ba2..2cf1693b47 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -13,6 +13,9 @@ from esphome.const import ( CONF_PRESET_MODES, CONF_SPEED_COUNT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType from .. import hbridge_ns @@ -54,12 +57,17 @@ CONFIG_SCHEMA = ( maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}), synchronous=True, ) -async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): +async def fan_hbridge_brake_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await fan.new_fan( config, config[CONF_SPEED_COUNT], diff --git a/esphome/components/hbridge/light/__init__.py b/esphome/components/hbridge/light/__init__.py index f9451e2594..f7866cb990 100644 --- a/esphome/components/hbridge/light/__init__.py +++ b/esphome/components/hbridge/light/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import light, output import esphome.config_validation as cv from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B, CONF_UPDATE_INTERVAL +from esphome.types import ConfigType from .. import hbridge_ns @@ -21,7 +22,7 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) cg.add(var.set_update_interval(config.pop(CONF_UPDATE_INTERVAL))) await cg.register_component(var, config) diff --git a/esphome/components/hbridge/switch/__init__.py b/esphome/components/hbridge/switch/__init__.py index e26bd6b1d8..294be6ed5f 100644 --- a/esphome/components/hbridge/switch/__init__.py +++ b/esphome/components/hbridge/switch/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import CONF_OPTIMISTIC, CONF_PULSE_LENGTH, CONF_WAIT_TIME +from esphome.types import ConfigType from .. import hbridge_ns @@ -30,7 +31,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await switch.new_switch(config) await cg.register_component(var, config) diff --git a/esphome/components/hc8/sensor.py b/esphome/components/hc8/sensor.py index 29b428e310..616162eb40 100644 --- a/esphome/components/hc8/sensor.py +++ b/esphome/components/hc8/sensor.py @@ -12,6 +12,9 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["uart"] @@ -47,7 +50,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) @@ -73,7 +76,12 @@ CALIBRATION_ACTION_SCHEMA = cv.Schema( CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -async def hc8_calibration_to_code(config, action_id, template_arg, args): +async def hc8_calibration_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_BASELINE], args, cg.uint16) diff --git a/esphome/components/hdc302x/sensor.py b/esphome/components/hdc302x/sensor.py index a6265b9b98..6d91c3df7c 100644 --- a/esphome/components/hdc302x/sensor.py +++ b/esphome/components/hdc302x/sensor.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation from esphome.automation import maybe_simple_id import esphome.codegen as cg @@ -16,6 +18,9 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PERCENT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -62,7 +67,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) @@ -86,7 +91,7 @@ HDC302X_HEATER_POWER_MAP = { } -def heater_power_value(value): +def heater_power_value(value: Any) -> cv.Lambda | int: """Accept enum names or raw uint16 values""" if isinstance(value, cv.Lambda): return value @@ -119,7 +124,12 @@ HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id( HDC302X_HEATER_ON_ACTION_SCHEMA, synchronous=True, ) -async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): +async def hdc302x_heater_on_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_POWER], args, cg.uint16) @@ -135,7 +145,12 @@ async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): HDC302X_ACTION_SCHEMA, synchronous=True, ) -async def hdc302x_heater_off_to_code(config, action_id, template_arg, args): +async def hdc302x_heater_off_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index cf3c594f36..a2e1f8054a 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -1,3 +1,6 @@ +from collections.abc import Callable +from typing import Any + import esphome.codegen as cg from esphome.components import i2c, sensor import esphome.config_validation as cv @@ -17,6 +20,8 @@ from esphome.const import ( UNIT_DEGREES, UNIT_MICROTESLA, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -59,14 +64,16 @@ HMC5883L_RANGES = { } -def validate_enum(enum_values, units=None, int=True): +def validate_enum( + enum_values: dict[Any, Any], units: str | list[str] | None = None, int: bool = True +) -> Callable[[Any], Any]: _units = [] if units is not None: _units = units if isinstance(units, list) else [units] _units = [str(x) for x in _units] enum_bound = cv.enum(enum_values, int=int) - def validate_enum_bound(value): + def validate_enum_bound(value: Any) -> Any: value = cv.string(value) for unit in _units: if value.endswith(unit): @@ -112,7 +119,7 @@ CONFIG_SCHEMA = ( ) -def auto_data_rate(config): +def auto_data_rate(config: ConfigType) -> MockObj: interval_msec = config[CONF_UPDATE_INTERVAL].total_milliseconds interval_hz = 1000.0 / interval_msec for datarate in sorted(HMC5883LDatarates.keys()): @@ -121,7 +128,7 @@ def auto_data_rate(config): return HMC5883LDatarates[75] -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 7b23775b47..1b66842f1e 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -1,13 +1,19 @@ +from collections.abc import Callable, Iterable + import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter", "@esphome/core"] homeassistant_ns = cg.esphome_ns.namespace("homeassistant") -def validate_entity_domain(platform, supported_domains): - def validator(config): +def validate_entity_domain( + platform: str, supported_domains: Iterable[str] +) -> Callable[[ConfigType], ConfigType]: + def validator(config: ConfigType) -> ConfigType: domain = config[CONF_ENTITY_ID].split(".", 1)[0] if domain not in supported_domains: raise cv.Invalid( @@ -34,7 +40,7 @@ HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema( ) -def setup_home_assistant_entity(var, config): +def setup_home_assistant_entity(var: MockObj, config: ConfigType) -> None: cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) if CONF_ATTRIBUTE in config: cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) diff --git a/esphome/components/homeassistant/binary_sensor/__init__.py b/esphome/components/homeassistant/binary_sensor/__init__.py index a943368dd7..6ea17b6831 100644 --- a/esphome/components/homeassistant/binary_sensor/__init__.py +++ b/esphome/components/homeassistant/binary_sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import binary_sensor +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_SCHEMA, @@ -18,7 +19,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).ex ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/number/__init__.py b/esphome/components/homeassistant/number/__init__.py index 8f760772c3..ab1389e13a 100644 --- a/esphome/components/homeassistant/number/__init__.py +++ b/esphome/components/homeassistant/number/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, @@ -22,7 +23,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") var = await number.new_number( config, diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index 6437476827..abee957fda 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_SCHEMA, @@ -18,7 +19,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).e ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/switch/__init__.py b/esphome/components/homeassistant/switch/__init__.py index c299a731f2..55854cd659 100644 --- a/esphome/components/homeassistant/switch/__init__.py +++ b/esphome/components/homeassistant/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, @@ -36,7 +37,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/homeassistant/text_sensor/__init__.py b/esphome/components/homeassistant/text_sensor/__init__.py index b59f9d23df..265250c695 100644 --- a/esphome/components/homeassistant/text_sensor/__init__.py +++ b/esphome/components/homeassistant/text_sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import text_sensor +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_SCHEMA, @@ -18,7 +19,7 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/time/__init__.py b/esphome/components/homeassistant/time/__init__.py index 05ca86a26e..146b8278ea 100644 --- a/esphome/components/homeassistant/time/__init__.py +++ b/esphome/components/homeassistant/time/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import time as time_ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TIMEZONE +from esphome.types import ConfigType from .. import homeassistant_ns @@ -16,7 +17,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await time_.register_time(var, config) await cg.register_component(var, config) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index b6a3b8b615..c5846f5406 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.platformio.toolchain import copy_ccache_script +from esphome.types import ConfigType from .const import KEY_HOST @@ -22,7 +23,7 @@ AUTO_LOAD = ["network", "preferences"] IS_TARGET_PLATFORM = True -def set_core_data(config): +def set_core_data(config: ConfigType) -> ConfigType: CORE.data[KEY_HOST] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_HOST CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host" @@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_NATIVE_64BIT_TIME") # The prefs file finds stored preferences by key, so key migration is possible diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index fcfb0b6c54..e39d35d077 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -1,4 +1,5 @@ import logging +from typing import Any from esphome import pins import esphome.codegen as cg @@ -14,6 +15,8 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from .const import host_ns @@ -22,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin) -def _translate_pin(value): +def _translate_pin(value: Any) -> int | str: if isinstance(value, dict) or value is None: raise cv.Invalid( "This variable only supports pin numbers, not full pin schemas " @@ -41,7 +44,7 @@ def _translate_pin(value): return value -def validate_gpio_pin(value): +def validate_gpio_pin(value: Any) -> int | str: return _translate_pin(value) @@ -53,7 +56,7 @@ HOST_PIN_SCHEMA = pins.gpio_base_schema( @pins.PIN_SCHEMA_REGISTRY.register("host", HOST_PIN_SCHEMA) -async def host_pin_to_code(config): +async def host_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) diff --git a/esphome/components/host/time/__init__.py b/esphome/components/host/time/__init__.py index d9a2f1207c..6eb0cf954d 100644 --- a/esphome/components/host/time/__init__.py +++ b/esphome/components/host/time/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import time as time_ import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] @@ -14,7 +15,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await time_.register_time(var, config) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 54d7f5c77b..8a5aae022a 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Any from esphome import automation import esphome.codegen as cg @@ -20,8 +21,10 @@ from esphome.const import ( PlatformFramework, __version__, ) -from esphome.core import CORE, Lambda +from esphome.core import CORE, ID, Lambda +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.helpers import IS_MACOS +from esphome.types import ConfigType DEPENDENCIES = ["network"] AUTO_LOAD = ["json", "watchdog"] @@ -63,14 +66,14 @@ CONF_BODY = "body" CONF_JSON = "json" -def validate_url(value): +def validate_url(value: Any) -> str: value = cv.url(value) if value.startswith(("http://", "https://")): return value raise cv.Invalid("URL must start with 'http://' or 'https://'") -def validate_ssl_verification(config): +def validate_ssl_verification(config: ConfigType) -> ConfigType: error_message = "" if CORE.is_rp2 and config[CONF_VERIFY_SSL]: @@ -91,7 +94,7 @@ def validate_ssl_verification(config): return config -def _declare_request_class(value): +def _declare_request_class(value: Any) -> ID: if CORE.is_host: return cv.declare_id(HttpRequestHost)(value) if CORE.is_esp32: @@ -151,7 +154,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_timeout(config[CONF_TIMEOUT])) cg.add(var.set_useragent(config[CONF_USERAGENT])) @@ -167,8 +170,11 @@ async def to_code(config): cg.add(var.set_watchdog_timeout(timeout_ms)) if CORE.is_esp32: - # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time). + # esp-tls is re-enabled too because http_request includes + # directly and esp_http_client only pulls it in as a private dependency. esp32.include_builtin_idf_component("esp_http_client") + esp32.include_builtin_idf_component("esp-tls") cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) @@ -298,7 +304,12 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend( HTTP_REQUEST_SEND_ACTION_SCHEMA, synchronous=True, ) -async def http_request_action_to_code(config, action_id, template_arg, args): +async def http_request_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index ddff954950..470ed332f1 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -196,6 +196,9 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c } container->feed_wdt(); + // IDF is the only backend reusing the container across redirect hops; + // drop the previous hop's headers (Arduino/host collect only the final response) + container->response_headers_.clear(); container->content_length = esp_http_client_fetch_headers(client); container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py index b7026e0f55..784e4ee47a 100644 --- a/esphome/components/http_request/ota/__init__.py +++ b/esphome/components/http_request/ota/__init__.py @@ -3,8 +3,10 @@ import esphome.codegen as cg from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME -from esphome.core import coroutine_with_priority +from esphome.core import ID, coroutine_with_priority from esphome.coroutine import CoroPriority +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns @@ -42,7 +44,7 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(CoroPriority.OTA_UPDATES) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await ota_to_code(var, config) await cg.register_component(var, config) @@ -72,7 +74,12 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All( OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA, synchronous=True, ) -async def ota_http_request_action_to_code(config, action_id, template_arg, args): +async def ota_http_request_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 8893b96c65..7e7594c3c3 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -64,8 +64,9 @@ void OtaHttpRequestComponent::flash() { } } -void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container) { - if (this->update_started_) { +void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container, + bool abort_backend) { + if (abort_backend) { ESP_LOGV(TAG, "Aborting OTA backend"); backend->abort(); } @@ -106,7 +107,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() { auto error_code = backend->begin(container->content_length); if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGW(TAG, "backend->begin error: %d", error_code); - this->cleanup_(std::move(backend), container); + // Nothing to abort: begin() failed, so no OTA handle was opened + this->cleanup_(std::move(backend), container, /*abort_backend=*/false); return error_code; } @@ -140,7 +142,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { } else { ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error); } - this->cleanup_(std::move(backend), container); + this->cleanup_(std::move(backend), container, /*abort_backend=*/true); return OTA_CONNECTION_ERROR; } @@ -150,14 +152,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() { md5_receive.add(buf, bufsize_or_error); // write bytes to OTA backend - this->update_started_ = true; error_code = backend->write(buf, bufsize_or_error); if (error_code != ota::OTA_RESPONSE_OK) { // error code explanation available at // https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code, container->get_bytes_read() - bufsize_or_error, container->content_length); - this->cleanup_(std::move(backend), container); + this->cleanup_(std::move(backend), container, /*abort_backend=*/true); return error_code; } } @@ -181,7 +182,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { this->md5_computed_ = md5_receive_str; if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) { ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str()); - this->cleanup_(std::move(backend), container); + this->cleanup_(std::move(backend), container, /*abort_backend=*/true); return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH; } else { backend->set_update_md5(md5_receive_str); @@ -197,7 +198,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { error_code = backend->end(); if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code); - this->cleanup_(std::move(backend), container); + this->cleanup_(std::move(backend), container, /*abort_backend=*/true); return error_code; } diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index a706331d9a..9bb748f175 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -38,7 +38,7 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented< void flash(); protected: - void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container); + void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container, bool abort_backend); uint8_t do_ota_(); std::string get_url_with_auth_(const std::string &url); bool http_get_md5_(); @@ -51,7 +51,6 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented< std::string username_{}; std::string url_{}; int status_ = -1; - bool update_started_ = false; static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size }; diff --git a/esphome/components/http_request/update/__init__.py b/esphome/components/http_request/update/__init__.py index d84d80109a..4bdc30e4cf 100644 --- a/esphome/components/http_request/update/__init__.py +++ b/esphome/components/http_request/update/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import ota, update import esphome.config_validation as cv from esphome.const import CONF_SOURCE +from esphome.types import ConfigType from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns from ..ota import OtaHttpRequestComponent @@ -29,7 +30,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await update.new_update(config) ota_parent = await cg.get_variable(config[CONF_OTA_ID]) cg.add(var.set_ota_parent(ota_parent)) diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 8808dc70f5..86dca77725 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -17,6 +17,9 @@ from esphome.const import ( UNIT_EMPTY, UNIT_PERCENT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -63,7 +66,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) @@ -95,7 +98,12 @@ async def to_code(config): ), synchronous=True, ) -async def set_heater_level_to_code(config, action_id, template_arg, args): +async def set_heater_level_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) level_ = await cg.templatable(config[CONF_LEVEL], args, cg.uint8) @@ -115,7 +123,12 @@ async def set_heater_level_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def set_heater_to_code(config, action_id, template_arg, args): +async def set_heater_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) status_ = await cg.templatable(config[CONF_STATUS], args, cg.bool_) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 7b163d065e..b053125446 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -1,6 +1,7 @@ import logging import re import sys +from typing import Any from esphome import pins import esphome.codegen as cg @@ -52,9 +53,10 @@ from esphome.const import ( PLATFORM_RP2, PlatformFramework, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.cpp_generator import MockObj import esphome.final_validate as fv +from esphome.types import ConfigType LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] @@ -96,13 +98,13 @@ CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled" MULTI_CONF = True -def validate_device(value): +def validate_device(value: str) -> str: if not re.match(r"^/(?:[^/]+/)*[^/]+$", value): raise cv.Invalid("Device must be an absolute device path (e.g., /dev/i2c-0)") return value -def _bus_declare_type(value): +def _bus_declare_type(value: Any) -> ID: if CORE.is_esp32: return cv.declare_id(IDFI2CBus)(value) if CORE.using_arduino: @@ -114,7 +116,7 @@ def _bus_declare_type(value): raise NotImplementedError -def _rp2040_i2c_controller(pin): +def _rp2040_i2c_controller(pin: int) -> int: """Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin. See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"): @@ -125,7 +127,7 @@ def _rp2040_i2c_controller(pin): return (pin // 2) % 2 -def validate_config(config): +def validate_config(config: ConfigType) -> ConfigType: if CORE.is_esp32: return cv.require_framework_version( esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) @@ -142,7 +144,7 @@ def validate_config(config): return config -def validate_host_config(config): +def validate_host_config(config: ConfigType) -> ConfigType: if CORE.is_host: # Host I2C is currently only supported on Linux if not sys.platform.lower().startswith("linux"): @@ -229,7 +231,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") @@ -281,9 +283,14 @@ FINAL_VALIDATE_SCHEMA = _final_validate @coroutine_with_priority(CoroPriority.BUS) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(i2c_ns.using) cg.add_define("USE_I2C") + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + # Re-enable the I2C driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_i2c") if CORE.is_host: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -353,7 +360,7 @@ async def to_code(config): cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE]))) -def i2c_device_schema(default_address): +def i2c_device_schema(default_address: int | None) -> cv.Schema: """Create a schema for a i2c device. :param default_address: The default address of the i2c device, can be None to represent @@ -370,7 +377,7 @@ def i2c_device_schema(default_address): return cv.Schema(schema) -async def register_i2c_device(var, config): +async def register_i2c_device(var: MockObj, config: ConfigType) -> None: """Register an i2c device with the given config. Sets the i2c bus to use and the i2c address. @@ -385,11 +392,11 @@ async def register_i2c_device(var, config): def final_validate_device_schema( name: str, *, - min_frequency: cv.frequency = None, - max_frequency: cv.frequency = None, - min_timeout: cv.time_period = None, - max_timeout: cv.time_period = None, -): + min_frequency: Any = None, + max_frequency: Any = None, + min_timeout: Any = None, + max_timeout: Any = None, +) -> cv.Schema: hub_schema = {} if (min_frequency is not None) and (max_frequency is not None): hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range( diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 4809bf5a92..c5e82beb46 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -21,8 +21,9 @@ from esphome.components.esp32.const import ( import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE from esphome.core import CORE -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass import esphome.final_validate as fv +from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] @@ -145,7 +146,7 @@ I2S_MCLK_MULTIPLE = { _validate_bits = cv.float_with_unit("bits", "bit") -def validate_mclk_divisible_by_3(config): +def validate_mclk_divisible_by_3(config: ConfigType) -> ConfigType: if config[CONF_BITS_PER_SAMPLE] == 24 and config[CONF_MCLK_MULTIPLE] % 3 != 0: raise cv.Invalid( f"{CONF_MCLK_MULTIPLE} must be divisible by 3 when bits per sample is 24" @@ -159,7 +160,7 @@ def i2s_audio_component_schema( default_sample_rate: int, default_channel: str, default_bits_per_sample: str, -): +) -> cv.Schema: return cv.Schema( { cv.GenerateID(): cv.declare_id(class_), @@ -182,7 +183,7 @@ def i2s_audio_component_schema( ) -async def register_i2s_audio_component(var, config): +async def register_i2s_audio_component(var: MockObj, config: ConfigType) -> None: await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]])) slot_mode = config[CONF_CHANNEL] @@ -260,7 +261,7 @@ def _assign_ports() -> None: next_port += 1 -def _final_validate(_): +def _final_validate(_: ConfigType) -> None: i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] variant = get_esp32_variant() if variant not in I2S_PORTS: @@ -275,7 +276,7 @@ def _final_validate(_): FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 9c6228087c..c217317237 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_NUM_CHANNELS, CONF_SAMPLE_RATE, ) +from esphome.types import ConfigType from .. import ( CONF_ADC_TYPE, @@ -46,7 +47,7 @@ I2S_PDM_DSR = { } -def _validate_esp32_variant(config): +def _validate_esp32_variant(config: ConfigType) -> ConfigType: variant = esp32.get_esp32_variant() if config[CONF_ADC_TYPE] == "external": if config[CONF_PDM] and variant not in PDM_VARIANTS: @@ -65,13 +66,13 @@ def _validate_esp32_variant(config): raise NotImplementedError -def _validate_channel(config): +def _validate_channel(config: ConfigType) -> ConfigType: if config[CONF_CHANNEL] == CONF_MONO: raise cv.Invalid(f"I2S microphone does not support {CONF_MONO}.") return config -def _set_num_channels_from_config(config): +def _set_num_channels_from_config(config: ConfigType) -> ConfigType: if config[CONF_CHANNEL] in (CONF_LEFT, CONF_RIGHT): config[CONF_NUM_CHANNELS] = 1 else: @@ -80,7 +81,7 @@ def _set_num_channels_from_config(config): return config -def _set_stream_limits(config): +def _set_stream_limits(config: ConfigType) -> ConfigType: audio.set_stream_limits( min_bits_per_sample=config.get(CONF_BITS_PER_SAMPLE), max_bits_per_sample=config.get(CONF_BITS_PER_SAMPLE), @@ -134,7 +135,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: if config[CONF_ADC_TYPE] == "internal": raise cv.Invalid( "Internal ADC is no longer supported. Use an external I2S microphone instead." @@ -144,7 +145,7 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await register_i2s_audio_component(var, config) diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 6d3c39c68e..4dc15681bf 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import audio, esp32, speaker +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -13,6 +14,7 @@ from esphome.const import ( CONF_SAMPLE_RATE, CONF_TIMEOUT, ) +from esphome.types import ConfigType from .. import ( CONF_I2S_DOUT_PIN, @@ -78,7 +80,7 @@ I2C_COMM_FMT_OPTIONS = { INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32] -def _set_num_channels_from_config(config): +def _set_num_channels_from_config(config: ConfigType) -> ConfigType: if config[CONF_CHANNEL] in (CONF_MONO, CONF_LEFT, CONF_RIGHT): config[CONF_NUM_CHANNELS] = 1 else: @@ -87,7 +89,7 @@ def _set_num_channels_from_config(config): return config -def _set_stream_limits(config): +def _set_stream_limits(config: ConfigType) -> ConfigType: if config.get(CONF_SPDIF_MODE, False): # SPDIF mode: 16/24/32-bit audio and stereo at configured sample rate audio.set_stream_limits( @@ -133,14 +135,14 @@ def _set_stream_limits(config): return config -def _select_speaker_class(config): +def _select_speaker_class(config: ConfigType) -> ConfigType: """Override ID type when SPDIF mode is enabled.""" if config.get(CONF_SPDIF_MODE, False): config[CONF_ID].type = I2SAudioSpeakerSPDIF return config -def _validate_esp32_variant(config): +def _validate_esp32_variant(config: ConfigType) -> ConfigType: variant = esp32.get_esp32_variant() if config[CONF_DAC_TYPE] == "internal": if variant not in INTERNAL_DAC_VARIANTS: @@ -207,7 +209,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: if config[CONF_DAC_TYPE] == "internal": raise cv.Invalid( "Internal DAC is no longer supported. Use an external I2S DAC instead." @@ -238,7 +240,7 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await register_i2s_audio_component(var, config) @@ -260,3 +262,13 @@ async def to_code(config): if config[CONF_TIMEOUT] != CONF_NEVER: cg.add(var.set_timeout(config[CONF_TIMEOUT])) cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION])) + + +# The SPDIF encoder and speaker are fully #ifdef'd on USE_I2S_AUDIO_SPDIF_MODE, +# set only when spdif_mode is enabled. +FILTER_SOURCE_FILES = filter_source_files_from_defines( + { + "spdif_encoder.cpp": "USE_I2S_AUDIO_SPDIF_MODE", + "i2s_audio_spdif.cpp": "USE_I2S_AUDIO_SPDIF_MODE", + } +) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index b1d332c1e5..64f87c167c 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -31,6 +31,7 @@ from esphome.const import ( ) from esphome.core import CORE, HexInt from esphome.final_validate import full_config +from esphome.types import ConfigType DEPENDENCIES = ["spi"] @@ -91,7 +92,7 @@ CONF_INVERT_DISPLAY = "invert_display" CONF_PIXEL_MODE = "pixel_mode" -def cmd(c, *args): +def cmd(c: int, *args: int) -> list[int]: """ Create a command sequence :param c: The command (8 bit) @@ -101,7 +102,7 @@ def cmd(c, *args): return [c, len(args)] + list(args) -def map_sequence(value): +def map_sequence(value: list[int]) -> list[int]: """ An initialisation sequence is a literal array of data bytes. The format is a repeated sequence of [CMD, ] @@ -111,7 +112,7 @@ def map_sequence(value): return cmd(*value) -def _validate(config): +def _validate(config: ConfigType) -> ConfigType: if ( config.get(CONF_COLOR_PALETTE) == "IMAGE_ADAPTIVE" and CONF_COLOR_PALETTE_IMAGES not in config @@ -196,7 +197,7 @@ CONFIG_SCHEMA = cv.All( ) -def final_validate(config): +def final_validate(config: ConfigType) -> None: global_config = full_config.get() # Ideally would calculate buffer size here, but that info is not available on the Python side needs_buffer = ( @@ -218,7 +219,7 @@ def final_validate(config): FINAL_VALIDATE_SCHEMA = final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: LOGGER.warning( "The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead." ) @@ -278,7 +279,7 @@ async def to_code(config): cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8_INDEXED)) from PIL import Image - def load_image(filename): + def load_image(filename: str) -> Image.Image: path = CORE.relative_config_path(filename) try: return Image.open(path) diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py index 15e2faba07..7bb589f0b1 100644 --- a/esphome/components/ina2xx_base/__init__.py +++ b/esphome/components/ina2xx_base/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + import esphome.codegen as cg from esphome.components import sensor from esphome.components.const import UNIT_AMPERE_HOUR @@ -26,6 +28,9 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.core import EnumValue +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@latonita"] @@ -76,7 +81,7 @@ SENSOR_MODEL_OPTIONS = { } -def validate_model_config(config): +def validate_model_config(config: ConfigType) -> ConfigType: model = config[CONF_MODEL] for key in config: @@ -92,7 +97,7 @@ def validate_model_config(config): return config -def validate_adc_time(value): +def validate_adc_time(value: Any) -> EnumValue: value = cv.positive_time_period_microseconds(value).total_microseconds return cv.enum(ADC_TIMES, int=True)(value) @@ -198,7 +203,7 @@ INA2XX_SCHEMA = cv.Schema( ).extend(cv.polling_component_schema("60s")) -async def setup_ina2xx(var, config): +async def setup_ina2xx(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 9b97995a96..5a909738c6 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -75,8 +75,6 @@ void Infrared::dump_config() { YESNO(this->traits_.get_supports_receiver())); } -InfraredCall Infrared::make_call() { return InfraredCall(this); } - void Infrared::control(const InfraredCall &call) { if (this->transmitter_ == nullptr) { ESP_LOGW(TAG, "No transmitter configured"); diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index 6d91c97cce..b6863e37ce 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -134,7 +134,7 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote const InfraredTraits &get_traits() const { return this->traits_; } /// Create a call object for transmitting - InfraredCall make_call(); + InfraredCall make_call() { return InfraredCall(this); } /// Get capability flags for this infrared instance uint32_t get_capability_flags() const; diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 8d784df672..82e8ba8df8 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -11,7 +11,10 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_VALUE, ) +from esphome.core import ID from esphome.core.entity_helpers import inherit_property_from +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType integration_ns = cg.esphome_ns.namespace("integration") IntegrationSensor = integration_ns.class_( @@ -39,14 +42,14 @@ CONF_TIME_UNIT = "time_unit" CONF_INTEGRATION_METHOD = "integration_method" -def inherit_unit_of_measurement(uom, config): +def inherit_unit_of_measurement(uom: str, config: ConfigType) -> str: suffix = config[CONF_TIME_UNIT] if uom.endswith("/" + suffix): return uom[0 : -len("/" + suffix)] return uom + suffix -def inherit_accuracy_decimals(decimals, config): +def inherit_accuracy_decimals(decimals: int, config: ConfigType) -> int: return decimals + 2 @@ -90,7 +93,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -113,7 +116,12 @@ async def to_code(config): ), synchronous=True, ) -async def sensor_integration_reset_to_code(config, action_id, template_arg, args): +async def sensor_integration_reset_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -130,7 +138,12 @@ async def sensor_integration_reset_to_code(config, action_id, template_arg, args ), synchronous=True, ) -async def sensor_integration_set_value_to_code(config, action_id, template_arg, args): +async def sensor_integration_set_value_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_VALUE], args, cg.float_) diff --git a/esphome/components/it8951/display.py b/esphome/components/it8951/display.py index bdc68b5257..57bf86c4c6 100644 --- a/esphome/components/it8951/display.py +++ b/esphome/components/it8951/display.py @@ -2,6 +2,9 @@ ESPHome configuration for the IT8951 e-paper controller. """ +from collections.abc import Callable +from typing import Any + from esphome import automation, core, pins import esphome.codegen as cg from esphome.components import display, spi @@ -33,8 +36,10 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, CONF_WIDTH, ) -from esphome.cpp_generator import RawExpression +from esphome.core import ID +from esphome.cpp_generator import MockObj, RawExpression, TemplateArgsType from esphome.final_validate import full_config +from esphome.types import ConfigType AUTO_LOAD = ["split_buffer"] DEPENDENCIES = ["spi"] @@ -97,16 +102,16 @@ class IT8951Model: models: dict[str, "IT8951Model"] = {} - def __init__(self, name: str, **defaults): + def __init__(self, name: str, **defaults: Any) -> None: name = name.upper() self.name = name self.defaults = defaults IT8951Model.models[name] = self - def get_default(self, key, fallback=None): + def get_default(self, key: str, fallback: Any = None) -> Any: return self.defaults.get(key, fallback) - def get_dimensions(self, config) -> tuple[int, int]: + def get_dimensions(self, config: ConfigType) -> tuple[int, int]: # If dimensions are in config, use them; otherwise fall back to model defaults. if CONF_DIMENSIONS in config: dimensions = config[CONF_DIMENSIONS] @@ -181,14 +186,16 @@ DIMENSION_SCHEMA = cv.Schema( ) -def _model_pin_option(model, key, schema): +def _model_pin_option( + model: IT8951Model, key: str, schema: Callable[[Any], Any] +) -> tuple[cv.Optional | cv.Required, Callable[[Any], Any]]: default = model.get_default(key) if default is None: return cv.Required(key), schema return cv.Optional(key, default=default), schema -def _model_schema(config): +def _model_schema(config: ConfigType) -> cv.Schema: model = IT8951Model.models[config[CONF_MODEL]] has_default_dimensions = ( model.get_default(CONF_WIDTH) is not None @@ -293,7 +300,7 @@ def _model_schema(config): return schema.extend(pin_extra) -def _customise_schema(config): +def _customise_schema(config: ConfigType) -> ConfigType: config = cv.Schema( { cv.Required(CONF_MODEL): cv.one_of( @@ -336,7 +343,7 @@ def _customise_schema(config): CONFIG_SCHEMA = _customise_schema -def _final_validate(config) -> None: +def _final_validate(config: ConfigType) -> None: # IT8951 reads from SPI (DevInfo, VCOM, register reads) so MISO is required. spi.final_validate_device_schema("it8951", require_miso=True, require_mosi=True)( config @@ -356,7 +363,7 @@ def _final_validate(config) -> None: FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: model = IT8951Model.models[config[CONF_MODEL]] width, height = model.get_dimensions(config) @@ -423,7 +430,12 @@ async def to_code(config): ), synchronous=True, ) -async def it8951_update_action_to_code(config, action_id, template_arg, args): +async def it8951_update_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: display_var = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, display_var) if mode := config.get(CONF_MODE): diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index 1f4519df2d..bf47b6df88 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -15,8 +15,9 @@ from esphome.const import ( CONF_TIMEOUT, CONF_TRIGGER_ID, ) +from esphome.core import ID from esphome.cpp_generator import MockObj, literal -from esphome.types import TemplateArgsType +from esphome.types import ConfigType, TemplateArgsType CODEOWNERS = ["@ssieb"] @@ -90,7 +91,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for source_conf in config.get(CONF_SOURCE_ID, ()): @@ -144,7 +145,12 @@ async def to_code(config): ), synchronous=True, ) -async def enable_to_code(config, action_id, template_arg, args): +async def enable_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -160,7 +166,12 @@ async def enable_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def disable_to_code(config, action_id, template_arg, args): +async def disable_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/key_collector/text_sensor/__init__.py b/esphome/components/key_collector/text_sensor/__init__.py index 1676cf7bdf..e32d15df2e 100644 --- a/esphome/components/key_collector/text_sensor/__init__.py +++ b/esphome/components/key_collector/text_sensor/__init__.py @@ -4,7 +4,7 @@ from esphome.components.text_sensor import TextSensor import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.cpp_generator import literal -from esphome.types import TemplateArgsType +from esphome.types import ConfigType, TemplateArgsType from .. import CONF_ON_RESULT, CONF_SOURCE_ID, TRIGGER_TYPES, KeyCollector @@ -15,7 +15,7 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema(TextSensor).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_SOURCE_ID]) var = cg.new_Pvariable(config[CONF_ID]) await text_sensor.register_text_sensor(var, config) diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index bf1072ce66..08ec395720 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -1,7 +1,11 @@ +from typing import Any + import esphome.codegen as cg from esphome.components import display import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_DIMENSIONS, CONF_POSITION +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CONF_USER_CHARACTERS = "user_characters" @@ -9,7 +13,7 @@ lcd_base_ns = cg.esphome_ns.namespace("lcd_base") LCDDisplay = lcd_base_ns.class_("LCDDisplay", cg.PollingComponent) -def validate_lcd_dimensions(value): +def validate_lcd_dimensions(value: Any) -> list[int]: value = cv.dimensions(value) if value[0] > 0x40: raise cv.Invalid("LCD displays can't have more than 64 columns") @@ -18,7 +22,7 @@ def validate_lcd_dimensions(value): return value -def validate_user_characters(value): +def validate_user_characters(value: list[ConfigType]) -> list[ConfigType]: positions = set() for conf in value: if conf[CONF_POSITION] in positions: @@ -51,7 +55,7 @@ LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( ).extend(cv.polling_component_schema("1s")) -async def setup_lcd_display(var, config): +async def setup_lcd_display(var: MockObj, config: ConfigType) -> None: await display.register_display(var, config) cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])) if CONF_USER_CHARACTERS in config: diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index 360e56330a..19786f38d3 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -4,6 +4,9 @@ import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_THROTTLE, CONF_TIMEOUT +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] @@ -69,7 +72,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) @@ -102,7 +105,12 @@ BLUETOOTH_PASSWORD_SET_SCHEMA = cv.Schema( BLUETOOTH_PASSWORD_SET_SCHEMA, synchronous=True, ) -async def bluetooth_password_set_to_code(config, action_id, template_arg, args): +async def bluetooth_password_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_PASSWORD], args, cg.std_string) diff --git a/esphome/components/ld2410/binary_sensor.py b/esphome/components/ld2410/binary_sensor.py index fb5b5cabff..2b68733532 100644 --- a/esphome/components/ld2410/binary_sensor.py +++ b/esphome/components/ld2410/binary_sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( ICON_ACCOUNT, ICON_MOTION_SENSOR, ) +from esphome.types import ConfigType from . import CONF_LD2410_ID, LD2410Component @@ -46,7 +47,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if has_target_config := config.get(CONF_HAS_TARGET): sens = await binary_sensor.new_binary_sensor(has_target_config) diff --git a/esphome/components/ld2410/button/__init__.py b/esphome/components/ld2410/button/__init__.py index fa6f31ee25..59a9558331 100644 --- a/esphome/components/ld2410/button/__init__.py +++ b/esphome/components/ld2410/button/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( ICON_RESTART, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns @@ -44,7 +45,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if factory_reset_config := config.get(CONF_FACTORY_RESET): b = await button.new_button(factory_reset_config) diff --git a/esphome/components/ld2410/number/__init__.py b/esphome/components/ld2410/number/__init__.py index 01dbcc785d..3500d704a1 100644 --- a/esphome/components/ld2410/number/__init__.py +++ b/esphome/components/ld2410/number/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_SECOND, ) +from esphome.types import ConfigType from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns @@ -85,7 +86,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if timeout_config := config.get(CONF_TIMEOUT): n = await number.new_number( diff --git a/esphome/components/ld2410/select/__init__.py b/esphome/components/ld2410/select/__init__.py index 9c4f654aa1..e89e3d5997 100644 --- a/esphome/components/ld2410/select/__init__.py +++ b/esphome/components/ld2410/select/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ICON_SCALE, ICON_THERMOMETER, ) +from esphome.types import ConfigType from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns @@ -48,7 +49,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION): s = await select.new_select( diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index 459018e263..ca42b3a1d3 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -15,6 +15,7 @@ from esphome.const import ( UNIT_CENTIMETER, UNIT_PERCENT, ) +from esphome.types import ConfigType from . import CONF_LD2410_ID, LD2410Component @@ -155,7 +156,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if moving_distance_config := config.get(CONF_MOVING_DISTANCE): sens = await sensor.new_sensor(moving_distance_config) diff --git a/esphome/components/ld2410/switch/__init__.py b/esphome/components/ld2410/switch/__init__.py index 4276b28a71..6d8053ddd6 100644 --- a/esphome/components/ld2410/switch/__init__.py +++ b/esphome/components/ld2410/switch/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_PULSE, ) +from esphome.types import ConfigType from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns @@ -35,7 +36,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if engineering_mode_config := config.get(CONF_ENGINEERING_MODE): s = await switch.new_switch(engineering_mode_config) diff --git a/esphome/components/ld2410/text_sensor.py b/esphome/components/ld2410/text_sensor.py index a34c8ec0d2..25c61a4825 100644 --- a/esphome/components/ld2410/text_sensor.py +++ b/esphome/components/ld2410/text_sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_CHIP, ) +from esphome.types import ConfigType from . import CONF_LD2410_ID, LD2410Component @@ -26,7 +27,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if version_config := config.get(CONF_VERSION): sens = await text_sensor.new_text_sensor(version_config) diff --git a/esphome/components/ld2412/__init__.py b/esphome/components/ld2412/__init__.py index e701d0bda9..82db319861 100644 --- a/esphome/components/ld2412/__init__.py +++ b/esphome/components/ld2412/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_THROTTLE +from esphome.types import ConfigType AUTO_LOAD = ["ld24xx"] CODEOWNERS = ["@Rihan9"] @@ -40,7 +41,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/ld2412/binary_sensor.py b/esphome/components/ld2412/binary_sensor.py index 98fa5965cd..80cff014c0 100644 --- a/esphome/components/ld2412/binary_sensor.py +++ b/esphome/components/ld2412/binary_sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( ICON_ACCOUNT, ICON_MOTION_SENSOR, ) +from esphome.types import ConfigType from . import CONF_LD2412_ID, LD2412Component @@ -48,7 +49,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if dynamic_background_correction_status_config := config.get( CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS diff --git a/esphome/components/ld2412/button/__init__.py b/esphome/components/ld2412/button/__init__.py index e0ca285265..5a1ea2e6a5 100644 --- a/esphome/components/ld2412/button/__init__.py +++ b/esphome/components/ld2412/button/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( ICON_RESTART, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component @@ -54,7 +55,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if factory_reset_config := config.get(CONF_FACTORY_RESET): b = await button.new_button(factory_reset_config) diff --git a/esphome/components/ld2412/number/__init__.py b/esphome/components/ld2412/number/__init__.py index b6e1c8d039..1a81c330ad 100644 --- a/esphome/components/ld2412/number/__init__.py +++ b/esphome/components/ld2412/number/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_SECOND, ) +from esphome.types import ConfigType from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component @@ -85,7 +86,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD): n = await number.new_number( diff --git a/esphome/components/ld2412/select/__init__.py b/esphome/components/ld2412/select/__init__.py index a54cd700ed..02ecf2c30f 100644 --- a/esphome/components/ld2412/select/__init__.py +++ b/esphome/components/ld2412/select/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ICON_SCALE, ICON_THERMOMETER, ) +from esphome.types import ConfigType from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component @@ -48,7 +49,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if baud_rate_config := config.get(CONF_BAUD_RATE): s = await select.new_select( diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py index f562afe0ee..0b6e676931 100644 --- a/esphome/components/ld2412/sensor.py +++ b/esphome/components/ld2412/sensor.py @@ -16,6 +16,7 @@ from esphome.const import ( UNIT_EMPTY, UNIT_PERCENT, ) +from esphome.types import ConfigType from . import CONF_LD2412_ID, LD2412Component @@ -156,7 +157,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if detection_distance_config := config.get(CONF_DETECTION_DISTANCE): sens = await sensor.new_sensor(detection_distance_config) diff --git a/esphome/components/ld2412/switch/__init__.py b/esphome/components/ld2412/switch/__init__.py index 7a87e9e483..e7f71222fd 100644 --- a/esphome/components/ld2412/switch/__init__.py +++ b/esphome/components/ld2412/switch/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_PULSE, ) +from esphome.types import ConfigType from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component @@ -35,7 +36,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if bluetooth_config := config.get(CONF_BLUETOOTH): s = await switch.new_switch(bluetooth_config) diff --git a/esphome/components/ld2412/text_sensor.py b/esphome/components/ld2412/text_sensor.py index 22fba5193e..c8e9f42ef3 100644 --- a/esphome/components/ld2412/text_sensor.py +++ b/esphome/components/ld2412/text_sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_CHIP, ) +from esphome.types import ConfigType from . import CONF_LD2412_ID, LD2412Component @@ -26,7 +27,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if version_config := config.get(CONF_VERSION): sens = await text_sensor.new_text_sensor(version_config) diff --git a/esphome/components/ld2420/__init__.py b/esphome/components/ld2420/__init__.py index 71a5fa13e4..5a5aabeba0 100644 --- a/esphome/components/ld2420/__init__.py +++ b/esphome/components/ld2420/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType CODEOWNERS = ["@descipher"] @@ -33,7 +34,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/ld2420/binary_sensor/__init__.py b/esphome/components/ld2420/binary_sensor/__init__.py index 5ebc4a9f63..76b42c0362 100644 --- a/esphome/components/ld2420/binary_sensor/__init__.py +++ b/esphome/components/ld2420/binary_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import CONF_HAS_TARGET, CONF_ID, DEVICE_CLASS_OCCUPANCY +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -23,7 +24,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) if CONF_HAS_TARGET in config: diff --git a/esphome/components/ld2420/button/__init__.py b/esphome/components/ld2420/button/__init__.py index dfeb121c91..cfcffd0922 100644 --- a/esphome/components/ld2420/button/__init__.py +++ b/esphome/components/ld2420/button/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ICON_RESTART, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -50,7 +51,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2420_component = await cg.get_variable(config[CONF_LD2420_ID]) if apply_config := config.get(CONF_APPLY_CONFIG): b = await button.new_button(apply_config) diff --git a/esphome/components/ld2420/number/__init__.py b/esphome/components/ld2420/number/__init__.py index a2637b7b06..448639c911 100644 --- a/esphome/components/ld2420/number/__init__.py +++ b/esphome/components/ld2420/number/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( ICON_TIMELAPSE, UNIT_SECOND, ) +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -113,7 +114,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2420_component = await cg.get_variable(config[CONF_LD2420_ID]) if gate_timeout_config := config.get(CONF_PRESENCE_TIMEOUT): n = await number.new_number( diff --git a/esphome/components/ld2420/select/__init__.py b/esphome/components/ld2420/select/__init__.py index b9059c120f..cd66064e47 100644 --- a/esphome/components/ld2420/select/__init__.py +++ b/esphome/components/ld2420/select/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import select import esphome.config_validation as cv from esphome.const import ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -23,7 +24,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2420_component = await cg.get_variable(config[CONF_LD2420_ID]) if operating_mode_config := config.get(CONF_OPERATING_MODE): sel = await select.new_select( diff --git a/esphome/components/ld2420/sensor/__init__.py b/esphome/components/ld2420/sensor/__init__.py index 97acdabd7b..f98d63585b 100644 --- a/esphome/components/ld2420/sensor/__init__.py +++ b/esphome/components/ld2420/sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_CENTIMETER, ) +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -30,7 +31,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) if CONF_MOVING_DISTANCE in config: diff --git a/esphome/components/ld2420/text_sensor/__init__.py b/esphome/components/ld2420/text_sensor/__init__.py index 14d982e5fb..cee8f25c1f 100644 --- a/esphome/components/ld2420/text_sensor/__init__.py +++ b/esphome/components/ld2420/text_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_ID, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -24,7 +25,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) if CONF_FW_VERSION in config: diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index 585c9f7bf5..4c37f4fcd1 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE +from esphome.types import ConfigType AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] @@ -49,7 +50,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/ld2450/binary_sensor.py b/esphome/components/ld2450/binary_sensor.py index 89e629253a..779d151fd9 100644 --- a/esphome/components/ld2450/binary_sensor.py +++ b/esphome/components/ld2450/binary_sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, ) +from esphome.types import ConfigType from . import CONF_LD2450_ID, LD2450Component @@ -39,7 +40,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if has_target_config := config.get(CONF_HAS_TARGET): sens = await binary_sensor.new_binary_sensor(has_target_config) diff --git a/esphome/components/ld2450/button/__init__.py b/esphome/components/ld2450/button/__init__.py index 682487d750..42cadd2052 100644 --- a/esphome/components/ld2450/button/__init__.py +++ b/esphome/components/ld2450/button/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( ICON_RESTART, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns @@ -35,7 +36,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if factory_reset_config := config.get(CONF_FACTORY_RESET): b = await button.new_button(factory_reset_config) diff --git a/esphome/components/ld2450/number/__init__.py b/esphome/components/ld2450/number/__init__.py index 799c0703f2..4f242076d6 100644 --- a/esphome/components/ld2450/number/__init__.py +++ b/esphome/components/ld2450/number/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( UNIT_MILLIMETER, UNIT_SECOND, ) +from esphome.types import ConfigType from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns @@ -78,7 +79,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if presence_timeout_config := config.get(CONF_PRESENCE_TIMEOUT): n = await number.new_number( diff --git a/esphome/components/ld2450/select/__init__.py b/esphome/components/ld2450/select/__init__.py index 4f237dc94f..d91b42426a 100644 --- a/esphome/components/ld2450/select/__init__.py +++ b/esphome/components/ld2450/select/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ICON_THERMOMETER, ) +from esphome.types import ConfigType from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns @@ -31,7 +32,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if baud_rate_config := config.get(CONF_BAUD_RATE): s = await select.new_select( diff --git a/esphome/components/ld2450/sensor.py b/esphome/components/ld2450/sensor.py index ae13900e7a..40462e202d 100644 --- a/esphome/components/ld2450/sensor.py +++ b/esphome/components/ld2450/sensor.py @@ -15,6 +15,7 @@ from esphome.const import ( UNIT_DEGREES, UNIT_MILLIMETER, ) +from esphome.types import ConfigType from . import CONF_LD2450_ID, LD2450Component @@ -226,7 +227,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if target_count_config := config.get(CONF_TARGET_COUNT): diff --git a/esphome/components/ld2450/switch/__init__.py b/esphome/components/ld2450/switch/__init__.py index 0c0c92377b..084f79ee1b 100644 --- a/esphome/components/ld2450/switch/__init__.py +++ b/esphome/components/ld2450/switch/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_PULSE, ) +from esphome.types import ConfigType from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns @@ -35,7 +36,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if bluetooth_config := config.get(CONF_BLUETOOTH): s = await switch.new_switch(bluetooth_config) diff --git a/esphome/components/ld2450/text_sensor.py b/esphome/components/ld2450/text_sensor.py index 4e5d7d419b..a8b978ef48 100644 --- a/esphome/components/ld2450/text_sensor.py +++ b/esphome/components/ld2450/text_sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( ICON_CHIP, ICON_SIGN_DIRECTION, ) +from esphome.types import ConfigType from . import CONF_LD2450_ID, LD2450Component @@ -49,7 +50,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if version_config := config.get(CONF_VERSION): sens = await text_sensor.new_text_sensor(version_config) diff --git a/esphome/components/ld6002b/__init__.py b/esphome/components/ld6002b/__init__.py index 99f2ead3bb..af1e501a6a 100644 --- a/esphome/components/ld6002b/__init__.py +++ b/esphome/components/ld6002b/__init__.py @@ -60,7 +60,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/ld6002b/binary_sensor.py b/esphome/components/ld6002b/binary_sensor.py index 63f7b40c23..74095d5ded 100644 --- a/esphome/components/ld6002b/binary_sensor.py +++ b/esphome/components/ld6002b/binary_sensor.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import CONF_TARGET, DEVICE_CLASS_OCCUPANCY +from esphome.types import ConfigType from . import LD6002BComponent from .const import AREA_COUNT, CONF_LD6002B_ID, MAX_TARGETS @@ -36,7 +37,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_LD6002B_ID]) if target_config := config.get(CONF_TARGET): diff --git a/esphome/components/ld6002b/button/__init__.py b/esphome/components/ld6002b/button/__init__.py index 508d5c2bc6..a664890a86 100644 --- a/esphome/components/ld6002b/button/__init__.py +++ b/esphome/components/ld6002b/button/__init__.py @@ -129,7 +129,7 @@ BUTTON_MAP = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: for key, button_type in BUTTON_MAP.items(): if button_config := config.get(key): b = cg.new_Pvariable(button_config[CONF_ID], button_type) diff --git a/esphome/components/ld6002b/number/__init__.py b/esphome/components/ld6002b/number/__init__.py index 452e38d6e3..236b049f53 100644 --- a/esphome/components/ld6002b/number/__init__.py +++ b/esphome/components/ld6002b/number/__init__.py @@ -136,7 +136,7 @@ def final_validate(config: ConfigType) -> None: FINAL_VALIDATE_SCHEMA = final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_LD6002B_ID]) for key, number_type, setter, min_value, max_value, step in ( diff --git a/esphome/components/ld6002b/select/__init__.py b/esphome/components/ld6002b/select/__init__.py index 3da647ee2c..7f5e528b84 100644 --- a/esphome/components/ld6002b/select/__init__.py +++ b/esphome/components/ld6002b/select/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import select import esphome.config_validation as cv from esphome.const import CONF_AREA_ID, CONF_SENSITIVITY, ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType from .. import LD6002BComponent, ld6002b_ns from ..const import CONF_INSTALLATION_MODE, CONF_LD6002B_ID, CONF_TRIGGER_SPEED @@ -64,7 +65,7 @@ SELECT_MAP = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_LD6002B_ID]) for key, select_type, setter, options in SELECT_MAP: diff --git a/esphome/components/ld6002b/sensor.py b/esphome/components/ld6002b/sensor.py index 3aedaf9fdd..cceefb3837 100644 --- a/esphome/components/ld6002b/sensor.py +++ b/esphome/components/ld6002b/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_METER, ) +from esphome.types import ConfigType from . import LD6002BComponent from .const import ( @@ -150,7 +151,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_LD6002B_ID]) if target_count_config := config.get(CONF_TARGET_COUNT): diff --git a/esphome/components/ld6002b/switch/__init__.py b/esphome/components/ld6002b/switch/__init__.py index d27baa87fe..a414308b65 100644 --- a/esphome/components/ld6002b/switch/__init__.py +++ b/esphome/components/ld6002b/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType from .. import LD6002BComponent, ld6002b_ns from ..const import ( @@ -46,7 +47,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_LD6002B_ID]) for key, switch_type, setter in ( diff --git a/esphome/components/ld6002b/text_sensor.py b/esphome/components/ld6002b/text_sensor.py index a18d387437..0e8e2e80e7 100644 --- a/esphome/components/ld6002b/text_sensor.py +++ b/esphome/components/ld6002b/text_sensor.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC +from esphome.types import ConfigType from . import LD6002BComponent from .const import CONF_LD6002B_ID, CONF_OTA_VERSION, CONF_WORK_MODE @@ -21,7 +22,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_LD6002B_ID]) if work_mode_config := config.get(CONF_WORK_MODE): sens = await text_sensor.new_text_sensor(work_mode_config) diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 95df1fba23..e5e7c3dcbe 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -1,6 +1,9 @@ +from typing import Any + from esphome import automation, pins import esphome.codegen as cg from esphome.components import output +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHANNEL, @@ -9,20 +12,23 @@ from esphome.const import ( CONF_PHASE_ANGLE, CONF_PIN, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["esp32"] -def calc_max_frequency(bit_depth): +def calc_max_frequency(bit_depth: int) -> float: return 80e6 / (2**bit_depth) -def calc_min_frequency(bit_depth): +def calc_min_frequency(bit_depth: int) -> float: max_div_num = ((2**20) - 1) / 256.0 return 80e6 / (max_div_num * (2**bit_depth)) -def validate_frequency(value): +def validate_frequency(value: Any) -> float: value = cv.frequency(value) min_freq = calc_min_frequency(20) max_freq = calc_max_frequency(1) @@ -56,7 +62,10 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: + # Re-enable the LEDC driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_ledc") + gpio = await cg.gpio_pin_expression(config[CONF_PIN]) var = cg.new_Pvariable(config[CONF_ID], gpio) await cg.register_component(var, config) @@ -79,7 +88,12 @@ async def to_code(config): ), synchronous=True, ) -async def ledc_set_frequency_to_code(config, action_id, template_arg, args): +async def ledc_set_frequency_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_) diff --git a/esphome/components/libretiny_pwm/output.py b/esphome/components/libretiny_pwm/output.py index 6f71530aaf..716ccfad2b 100644 --- a/esphome/components/libretiny_pwm/output.py +++ b/esphome/components/libretiny_pwm/output.py @@ -3,6 +3,9 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_PIN +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["libretiny"] @@ -21,7 +24,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: gpio = await cg.gpio_pin_expression(config[CONF_PIN]) var = cg.new_Pvariable(config[CONF_ID], gpio) await cg.register_component(var, config) @@ -40,7 +43,12 @@ async def to_code(config): ), synchronous=True, ) -async def libretiny_pwm_set_frequency_to_code(config, action_id, template_arg, args): +async def libretiny_pwm_set_frequency_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 175f5b43cf..dbcc28d64a 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -7,6 +7,7 @@ import esphome.automation as auto import esphome.codegen as cg from esphome.components import mqtt, power_supply, web_server from esphome.components.const import CONF_CHANNEL_COLORS, CONF_IS_WRGB +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_BLUE, @@ -557,3 +558,10 @@ async def new_light(config, *args): @coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(light_ns.using) + + +# light_json_schema.cpp is only used by mqtt and web_server, which both +# auto load json; USE_JSON alone is too broad since other components load it. +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"light_json_schema.cpp": ("USE_MQTT", "USE_WEBSERVER")} +) diff --git a/esphome/components/light/esp_range_view.cpp b/esphome/components/light/esp_range_view.cpp index 58d552031a..5d372983d9 100644 --- a/esphome/components/light/esp_range_view.cpp +++ b/esphome/components/light/esp_range_view.cpp @@ -13,8 +13,6 @@ ESPColorView ESPRangeView::operator[](int32_t index) const { index = interpret_index(index, this->size()) + this->begin_; return (*this->parent_)[index]; } -ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; } -ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; } void ESPRangeView::set(const Color &color) { for (int32_t i = this->begin_; i < this->end_; i++) { diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h index f5e4ebb83f..ec129bdf70 100644 --- a/esphome/components/light/esp_range_view.h +++ b/esphome/components/light/esp_range_view.h @@ -75,4 +75,7 @@ class ESPRangeIterator { int32_t i_; }; +inline ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; } +inline ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; } + } // namespace esphome::light diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 9d0181a05c..82c00e2382 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -157,8 +157,6 @@ void LightState::loop() { } } -float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } - void LightState::publish_state() { if (this->remote_values_listeners_) { for (auto *listener : *this->remote_values_listeners_) { @@ -194,25 +192,11 @@ void LightState::add_target_state_reached_listener(LightTargetStateReachedListen this->target_state_reached_listeners_->push_back(listener); } -void LightState::set_default_transition_length(uint32_t default_transition_length) { - this->default_transition_length_ = default_transition_length; -} -uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; } -void LightState::set_flash_transition_length(uint32_t flash_transition_length) { - this->flash_transition_length_ = flash_transition_length; -} -uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } -void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } -void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } -void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; } -bool LightState::supports_effects() { return !this->effects_.empty(); } -const FixedVector &LightState::get_effects() const { return this->effects_; } void LightState::add_effects(const std::initializer_list &effects) { // Called once from Python codegen during setup with all effects from YAML config this->effects_ = effects; } -void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void LightState::current_values_as_brightness(float *brightness) { this->current_values.as_brightness(brightness); *brightness = this->gamma_correct_lut(*brightness); @@ -333,8 +317,6 @@ float LightState::gamma_uncorrect_lut(float value) const { } #endif // USE_LIGHT_GAMMA_LUT -bool LightState::is_transformer_active() { return this->is_transformer_active_; } - void LightState::start_effect_(uint32_t effect_index) { this->stop_effect_(); if (effect_index == 0) diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 5efc05358b..3a3f8fc368 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -109,7 +109,7 @@ class LightState : public EntityBase, public Component { void dump_config() override; void loop() override; /// Shortly after HARDWARE. - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; } /** The current values of the light as outputted to the light. * @@ -157,15 +157,19 @@ class LightState : public EntityBase, public Component { void add_target_state_reached_listener(LightTargetStateReachedListener *listener); /// Set the default transition length, i.e. the transition length when no transition is provided. - void set_default_transition_length(uint32_t default_transition_length); - uint32_t get_default_transition_length() const; + void set_default_transition_length(uint32_t default_transition_length) { + this->default_transition_length_ = default_transition_length; + } + uint32_t get_default_transition_length() const { return this->default_transition_length_; } /// Set the flash transition length - void set_flash_transition_length(uint32_t flash_transition_length); - uint32_t get_flash_transition_length() const; + void set_flash_transition_length(uint32_t flash_transition_length) { + this->flash_transition_length_ = flash_transition_length; + } + uint32_t get_flash_transition_length() const { return this->flash_transition_length_; } /// Set the gamma correction factor - void set_gamma_correct(float gamma_correct); + void set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } float get_gamma_correct() const { return this->gamma_correct_; } #ifdef USE_LIGHT_GAMMA_LUT @@ -186,17 +190,17 @@ class LightState : public EntityBase, public Component { #endif // USE_LIGHT_GAMMA_LUT /// Set the restore mode of this light - void set_restore_mode(LightRestoreMode restore_mode); + void set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } /// Set a callback to populate the initial state defaults during setup. /// The callback is called once, then cleared. Values live in flash as code. - void set_initial_state(void (*callback)(LightStateRTCState &)); + void set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; } /// Return whether the light has any effects that meet the trait requirements. - bool supports_effects(); + bool supports_effects() const { return !this->effects_.empty(); } /// Get all effects for this light state. - const FixedVector &get_effects() const; + const FixedVector &get_effects() const { return this->effects_; } /// Add effects for this light state. void add_effects(const std::initializer_list &effects); @@ -254,7 +258,7 @@ class LightState : public EntityBase, public Component { } /// The result of all the current_values_as_* methods have gamma correction applied. - void current_values_as_binary(bool *binary); + void current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void current_values_as_brightness(float *brightness); @@ -281,7 +285,7 @@ class LightState : public EntityBase, public Component { * return; * } */ - bool is_transformer_active(); + bool is_transformer_active() const { return this->is_transformer_active_; } protected: friend LightOutput; diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py index 76eabc2b71..0f42083cb5 100644 --- a/esphome/components/lightwaverf/__init__.py +++ b/esphome/components/lightwaverf/__init__.py @@ -11,7 +11,10 @@ from esphome.const import ( CONF_REPEAT, CONF_WRITE_PIN, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.cpp_helpers import gpio_pin_expression +from esphome.types import ConfigType CODEOWNERS = ["@max246"] @@ -57,7 +60,12 @@ LIGHTWAVE_SEND_SCHEMA = cv.Any( LIGHTWAVE_SEND_SCHEMA, synchronous=True, ) -async def send_raw_to_code(config, action_id, template_arg, args): +async def send_raw_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -71,7 +79,7 @@ async def send_raw_to_code(config, action_id, template_arg, args): return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 0a8ad58bc2..a4a7b5237d 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -12,13 +12,14 @@ from esphome.const import ( CONF_ON_UNLOCK, CONF_WEB_SERVER, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -102,7 +103,7 @@ _CALLBACK_AUTOMATIONS = ( @setup_entity("lock") -async def _setup_lock_core(var, config): +async def _setup_lock_core(var: MockObj, config: ConfigType) -> None: await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) if mqtt_id := config.get(CONF_MQTT_ID): @@ -113,7 +114,7 @@ async def _setup_lock_core(var, config): await web_server.add_entity_config(var, web_server_config) -async def register_lock(var, config): +async def register_lock(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) queue_entity_register("lock", config) @@ -121,7 +122,7 @@ async def register_lock(var, config): await _setup_lock_core(var, config) -async def new_lock(config, *args): +async def new_lock(config: ConfigType, *args: SafeExpType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID], *args) await register_lock(var, config) return var @@ -143,23 +144,38 @@ LOCK_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( "lock.open", OpenAction, LOCK_ACTION_SCHEMA, synchronous=True ) -async def lock_action_to_code(config, action_id, template_arg, args): +async def lock_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_condition("lock.is_locked", LockCondition, LOCK_ACTION_SCHEMA) -async def lock_is_on_to_code(config, condition_id, template_arg, args): +async def lock_is_on_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren, True) @automation.register_condition("lock.is_unlocked", LockCondition, LOCK_ACTION_SCHEMA) -async def lock_is_off_to_code(config, condition_id, template_arg, args): +async def lock_is_off_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren, False) @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(lock_ns.using) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index f307f5d5d1..07b8b03084 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,4 +1,5 @@ import re +from typing import Any from esphome import automation from esphome.automation import LambdaAction, StatelessLambdaAction @@ -58,7 +59,8 @@ from esphome.const import ( PLATFORM_RTL87XX, PlatformFramework, ) -from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -164,7 +166,7 @@ HARDWARE_UART_TO_SERIAL = { is_log_level = cv.one_of(*LOG_LEVELS, upper=True) -def uart_selection(value): +def uart_selection(value: Any) -> str: if CORE.is_esp32: variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: @@ -187,7 +189,7 @@ def uart_selection(value): raise NotImplementedError -def validate_local_no_higher_than_global(config): +def validate_local_no_higher_than_global(config: ConfigType) -> ConfigType: global_level = config[CONF_LEVEL] global_level_index = LOG_LEVEL_SEVERITY.index(global_level) errs = [] @@ -204,7 +206,7 @@ def validate_local_no_higher_than_global(config): return config -def validate_initial_no_higher_than_global(config): +def validate_initial_no_higher_than_global(config: ConfigType) -> ConfigType: if initial_level := config.get(CONF_INITIAL_LEVEL): global_level = config[CONF_LEVEL] if LOG_LEVEL_SEVERITY.index(initial_level) > LOG_LEVEL_SEVERITY.index( @@ -217,7 +219,7 @@ def validate_initial_no_higher_than_global(config): return config -def validate_wait_for_cdc(config): +def validate_wait_for_cdc(config: ConfigType) -> ConfigType: if config.get(CONF_WAIT_FOR_CDC) and config.get(CONF_HARDWARE_UART) != USB_CDC: raise cv.Invalid("wait_for_cdc requires hardware_uart: USB_CDC") return config @@ -518,7 +520,7 @@ async def _late_logger_init(config: ConfigType) -> None: CORE.add_job(final_step) -def validate_printf(value): +def validate_printf(value: ConfigType) -> ConfigType: # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python cfmt = r""" ( # start of capture group 1 @@ -559,7 +561,12 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All( @automation.register_action( CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA, synchronous=True ) -async def logger_log_action_to_code(config, action_id, template_arg, args): +async def logger_log_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]] @@ -584,7 +591,12 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def logger_set_level_to_code(config, action_id, template_arg, args): +async def logger_set_level_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: level = LOG_LEVELS[config[CONF_LEVEL]] logger = await cg.get_variable(config[CONF_LOGGER_ID]) if tag := config.get(CONF_TAG): @@ -656,7 +668,7 @@ def request_log_listener() -> None: @coroutine_with_priority(CoroPriority.FINAL) -async def final_step(): +async def final_step() -> None: """Final code generation step to configure optional logger features.""" domain_data = CORE.data.get(DOMAIN, {}) if domain_data.get(KEY_LEVEL_LISTENERS, False): diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 6527b6aa8c..bfc005070e 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -201,17 +201,10 @@ void Logger::process_messages_() { #endif // USE_ESPHOME_TASK_LOG_BUFFER } -void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } #endif -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) -UARTSelection Logger::get_uart() const { return this->uart_; } -#endif - -float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } - // Log level strings - packed into flash on ESP8266, indexed by log level (0-7) PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 69d8e6d32a..9c26814f7e 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -148,7 +148,7 @@ class Logger final : public Component { void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. - void set_baud_rate(uint32_t baud_rate); + void set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } uint32_t get_baud_rate() const { return baud_rate_; } #if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *get_hw_serial() const { return hw_serial_; } @@ -163,7 +163,7 @@ class Logger final : public Component { #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. - UARTSelection get_uart() const; + UARTSelection get_uart() const { return this->uart_; } #endif /// Set the default log level for this logger. @@ -197,7 +197,7 @@ class Logger final : public Component { void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); } #endif - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::BUS + 500.0f; } void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py index 6ce663978e..00f67422f3 100644 --- a/esphome/components/logger/select/__init__.py +++ b/esphome/components/logger/select/__init__.py @@ -4,6 +4,7 @@ import esphome.config_validation as cv from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_BUG from esphome.core import CORE from esphome.cpp_helpers import register_component, register_parented +from esphome.types import ConfigType from .. import ( CONF_LOGGER_ID, @@ -26,7 +27,7 @@ CONFIG_SCHEMA = select.select_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: request_logger_level_listeners() parent = await cg.get_variable(config[CONF_LOGGER_ID]) levels = list(LOG_LEVELS) diff --git a/esphome/components/ltr501/sensor.py b/esphome/components/ltr501/sensor.py index c1fa9009b3..c2091a6336 100644 --- a/esphome/components/ltr501/sensor.py +++ b/esphome/components/ltr501/sensor.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.components import i2c, sensor @@ -24,6 +26,7 @@ from esphome.const import ( UNIT_LUX, UNIT_MILLISECOND, ) +from esphome.types import ConfigType CODEOWNERS = ["@latonita"] DEPENDENCIES = ["i2c"] @@ -87,17 +90,17 @@ PS_GAINS = { } -def validate_integration_time(value): +def validate_integration_time(value: Any) -> Any: value = cv.positive_time_period_milliseconds(value).total_milliseconds return cv.enum(INTEGRATION_TIMES, int=True)(value) -def validate_repeat_rate(value): +def validate_repeat_rate(value: Any) -> Any: value = cv.positive_time_period_milliseconds(value).total_milliseconds return cv.enum(MEASUREMENT_REPEAT_RATES, int=True)(value) -def validate_time_and_repeat_rate(config): +def validate_time_and_repeat_rate(config: ConfigType) -> ConfigType: integraton_time = config[CONF_INTEGRATION_TIME] repeat_rate = config[CONF_REPEAT] if integraton_time > repeat_rate: @@ -107,7 +110,7 @@ def validate_time_and_repeat_rate(config): return config -def validate_als_gain_and_integration_time(config): +def validate_als_gain_and_integration_time(config: ConfigType) -> ConfigType: integraton_time = config[CONF_INTEGRATION_TIME] if config[CONF_GAIN] == "1X" and integraton_time > 100: raise cv.Invalid( @@ -221,7 +224,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ltr_als_ps/sensor.py b/esphome/components/ltr_als_ps/sensor.py index 893415f028..af09282e2d 100644 --- a/esphome/components/ltr_als_ps/sensor.py +++ b/esphome/components/ltr_als_ps/sensor.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.components import i2c, sensor @@ -23,6 +25,7 @@ from esphome.const import ( UNIT_LUX, UNIT_MILLISECOND, ) +from esphome.types import ConfigType CODEOWNERS = ["@latonita"] DEPENDENCIES = ["i2c"] @@ -93,17 +96,17 @@ PS_GAINS = { } -def validate_integration_time(value): +def validate_integration_time(value: Any) -> Any: value = cv.positive_time_period_milliseconds(value).total_milliseconds return cv.enum(INTEGRATION_TIMES, int=True)(value) -def validate_repeat_rate(value): +def validate_repeat_rate(value: Any) -> Any: value = cv.positive_time_period_milliseconds(value).total_milliseconds return cv.enum(MEASUREMENT_REPEAT_RATES, int=True)(value) -def validate_time_and_repeat_rate(config): +def validate_time_and_repeat_rate(config: ConfigType) -> ConfigType: integraton_time = config[CONF_INTEGRATION_TIME] repeat_rate = config[CONF_REPEAT] if integraton_time > repeat_rate: @@ -211,7 +214,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/m5stack_8angle/__init__.py b/esphome/components/m5stack_8angle/__init__.py index a1c197b381..6404bcf64c 100644 --- a/esphome/components/m5stack_8angle/__init__.py +++ b/esphome/components/m5stack_8angle/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] CODEOWNERS = ["@rnauber"] @@ -26,7 +27,7 @@ CONFIG_SCHEMA = cv.Schema( ).extend(i2c.i2c_device_schema(0x43)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/m5stack_8angle/binary_sensor/__init__.py b/esphome/components/m5stack_8angle/binary_sensor/__init__.py index 22ab73e901..09398876d4 100644 --- a/esphome/components/m5stack_8angle/binary_sensor/__init__.py +++ b/esphome/components/m5stack_8angle/binary_sensor/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv +from esphome.types import ConfigType from .. import CONF_M5STACK_8ANGLE_ID, M5Stack8AngleComponent, m5stack_8angle_ns @@ -22,7 +23,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID]) sens = await binary_sensor.new_binary_sensor(config) cg.add(sens.set_parent(hub)) diff --git a/esphome/components/m5stack_8angle/light/__init__.py b/esphome/components/m5stack_8angle/light/__init__.py index 806ecaabf4..5c4863acf7 100644 --- a/esphome/components/m5stack_8angle/light/__init__.py +++ b/esphome/components/m5stack_8angle/light/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import light import esphome.config_validation as cv from esphome.const import CONF_OUTPUT_ID +from esphome.types import ConfigType from .. import CONF_M5STACK_8ANGLE_ID, M5Stack8AngleComponent, m5stack_8angle_ns @@ -21,7 +22,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID]) lights = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(lights, config) diff --git a/esphome/components/m5stack_8angle/sensor/__init__.py b/esphome/components/m5stack_8angle/sensor/__init__.py index 2132eaa4c2..87d1425241 100644 --- a/esphome/components/m5stack_8angle/sensor/__init__.py +++ b/esphome/components/m5stack_8angle/sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( ICON_ROTATE_RIGHT, STATE_CLASS_MEASUREMENT, ) +from esphome.types import ConfigType from .. import ( CONF_M5STACK_8ANGLE_ID, @@ -55,7 +56,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_M5STACK_8ANGLE_ID]) diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py index 3c7d78a27b..cd846877ae 100644 --- a/esphome/components/mapping/__init__.py +++ b/esphome/components/mapping/__init__.py @@ -1,5 +1,6 @@ from collections.abc import Callable import difflib +from typing import Any import esphome.codegen as cg from esphome.components.const import KEY_METADATA @@ -13,6 +14,7 @@ from esphome.cpp_generator import ( add_global, ) from esphome.loader import get_component +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] MULTI_CONF = True @@ -32,13 +34,16 @@ class IndexType: """ def __init__( - self, validator: Callable, data_type: MockObj, conversion: Callable = None + self, + validator: Callable, + data_type: MockObj, + conversion: Callable | None = None, ) -> None: self.validator = validator self.data_type = data_type self.conversion = conversion - async def convert_value(self, value): + async def convert_value(self, value: Any) -> Any: if self.conversion: return self.conversion(value) return await cg.get_variable(value) @@ -60,7 +65,7 @@ class MappingMetaData: self.to_ = to_ -def to_schema(value): +def to_schema(value: Any) -> str: """ Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name. :param value: @@ -82,7 +87,7 @@ BASE_SCHEMA = cv.Schema( ) -def get_object_type(to_) -> MockObjClass | None: +def get_object_type(to_: str) -> MockObjClass | None: """ Get the object type from a string. Possible formats: xxx The name of a component which defines INSTANCE_TYPE @@ -121,7 +126,7 @@ def add_metadata( get_all_mapping_metadata()[mapping_id.id] = MappingMetaData(from_, to_) -def map_schema(config): +def map_schema(config: ConfigType) -> ConfigType: config = BASE_SCHEMA(config) if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict): raise cv.Invalid("an entries dictionary is required for a mapping") @@ -163,7 +168,7 @@ def map_schema(config): CONFIG_SCHEMA = map_schema -async def to_code(config): +async def to_code(config: ConfigType) -> MockObj: varid = config[CONF_ID] metadata = get_mapping_metadata(varid.id) entries = { diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 868b149211..47cf4793b1 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -4,6 +4,7 @@ from esphome.components import key_provider from esphome.components.const import CONF_ROWS import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID +from esphome.types import ConfigType CODEOWNERS = ["@ssieb"] @@ -27,7 +28,7 @@ CONF_HAS_DIODES = "has_diodes" CONF_HAS_PULLDOWNS = "has_pulldowns" -def check_keys(obj): +def check_keys(obj: ConfigType) -> ConfigType: if CONF_KEYS in obj and len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len( obj[CONF_COLUMNS] ): @@ -62,7 +63,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) row_pins = [] diff --git a/esphome/components/matrix_keypad/binary_sensor/__init__.py b/esphome/components/matrix_keypad/binary_sensor/__init__.py index 8e63ed43ce..6c6e0aad73 100644 --- a/esphome/components/matrix_keypad/binary_sensor/__init__.py +++ b/esphome/components/matrix_keypad/binary_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import CONF_COL, CONF_ID, CONF_KEY, CONF_ROW +from esphome.types import ConfigType from .. import CONF_KEYPAD_ID, MatrixKeypad, matrix_keypad_ns @@ -12,7 +13,7 @@ MatrixKeypadBinarySensor = matrix_keypad_ns.class_( ) -def check_button(obj): +def check_button(obj: ConfigType) -> ConfigType: if CONF_ROW in obj or CONF_COL in obj: if CONF_KEY in obj: raise cv.Invalid("You can't provide both a key and a position") @@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: if CONF_KEY in config: var = cg.new_Pvariable(config[CONF_ID], config[CONF_KEY][0]) else: diff --git a/esphome/components/max17043/sensor.py b/esphome/components/max17043/sensor.py index ebb045dfce..67fb8aa5b7 100644 --- a/esphome/components/max17043/sensor.py +++ b/esphome/components/max17043/sensor.py @@ -14,6 +14,9 @@ from esphome.const import ( UNIT_PERCENT, UNIT_VOLT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -50,7 +53,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) @@ -74,6 +77,11 @@ MAX17043_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( "max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA, synchronous=True ) -async def max17043_sleep_mode_to_code(config, action_id, template_arg, args): +async def max17043_sleep_mode_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index e9fae4cceb..5e45d71899 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -11,6 +11,9 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLUP, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@looping40"] @@ -54,7 +57,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) @@ -62,7 +65,7 @@ async def to_code(config): cg.add(var.set_brightness_global(config[CONF_BRIGHTNESS_GLOBAL])) -def validate_mode(value): +def validate_mode(value: ConfigType) -> ConfigType: if not (value[CONF_INPUT] or value[CONF_OUTPUT]): raise cv.Invalid("Mode must be either input or output") if value[CONF_INPUT] and value[CONF_OUTPUT]: @@ -87,7 +90,7 @@ MAX6956_PIN_SCHEMA = pins.gpio_base_schema( @pins.PIN_SCHEMA_REGISTRY.register(CONF_MAX6956, MAX6956_PIN_SCHEMA) -async def max6956_pin_to_code(config): +async def max6956_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_MAX6956]) @@ -114,7 +117,12 @@ async def max6956_pin_to_code(config): ), synchronous=True, ) -async def max6956_set_brightness_global_to_code(config, action_id, template_arg, args): +async def max6956_set_brightness_global_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_BRIGHTNESS_GLOBAL], args, cg.uint8) @@ -136,7 +144,12 @@ async def max6956_set_brightness_global_to_code(config, action_id, template_arg, ), synchronous=True, ) -async def max6956_set_brightness_mode_to_code(config, action_id, template_arg, args): +async def max6956_set_brightness_mode_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable( diff --git a/esphome/components/max6956/output/__init__.py b/esphome/components/max6956/output/__init__.py index 352ba04a95..f92bbb762a 100644 --- a/esphome/components/max6956/output/__init__.py +++ b/esphome/components/max6956/output/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PIN +from esphome.types import ConfigType from .. import CONF_MAX6956, MAX6956, max6956_ns @@ -20,7 +21,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_MAX6956]) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index df2423b0d0..54711263dd 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -10,6 +10,9 @@ from esphome.const import ( CONF_NUM_CHIPS, CONF_STATE, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@rspaargaren"] DEPENDENCIES = ["spi"] @@ -84,7 +87,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await spi.register_spi_device(var, config, write_only=True) await display.register_display(var, config) @@ -144,7 +147,12 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( MAX7219_ON_ACTION_SCHEMA, synchronous=True, ) -async def max7219digit_invert_to_code(config, action_id, template_arg, args): +async def max7219digit_invert_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) @@ -164,7 +172,12 @@ async def max7219digit_invert_to_code(config, action_id, template_arg, args): MAX7219_ON_ACTION_SCHEMA, synchronous=True, ) -async def max7219digit_visible_to_code(config, action_id, template_arg, args): +async def max7219digit_visible_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) @@ -184,7 +197,12 @@ async def max7219digit_visible_to_code(config, action_id, template_arg, args): MAX7219_ON_ACTION_SCHEMA, synchronous=True, ) -async def max7219digit_reverse_to_code(config, action_id, template_arg, args): +async def max7219digit_reverse_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) @@ -209,7 +227,12 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( MAX7219_INTENSITY_SCHEMA, synchronous=True, ) -async def max7219digit_intensity_to_code(config, action_id, template_arg, args): +async def max7219digit_intensity_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8) diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index d53499a78f..755d86e4ea 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -15,6 +15,8 @@ from esphome.const import ( CONF_PULLUP, ) from esphome.core import CORE, ID, coroutine +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType AUTO_LOAD = ["gpio_expander"] CODEOWNERS = ["@jesserockz"] @@ -41,7 +43,7 @@ MCP23XXX_CONFIG_SCHEMA = cv.Schema( @coroutine -async def register_mcp23xxx(config, num_pins): +async def register_mcp23xxx(config: ConfigType, num_pins: int) -> MockObj: id: ID = config[CONF_ID] var = cg.new_Pvariable(id) await cg.register_component(var, config) @@ -52,7 +54,7 @@ async def register_mcp23xxx(config, num_pins): return var -def validate_mode(value): +def validate_mode(value: ConfigType) -> ConfigType: if not (value[CONF_INPUT] or value[CONF_OUTPUT]): raise cv.Invalid("Mode must be either input or output") if value[CONF_INPUT] and value[CONF_OUTPUT]: @@ -81,7 +83,7 @@ MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema( @pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23XXX, MCP23XXX_PIN_SCHEMA) -async def mcp23xxx_pin_to_code(config): +async def mcp23xxx_pin_to_code(config: ConfigType) -> MockObj: parent_id: ID = config[CONF_MCP23XXX] parent = await cg.get_variable(parent_id) diff --git a/esphome/components/mcp4461/__init__.py b/esphome/components/mcp4461/__init__.py index f3ef6f4917..60cece67d7 100644 --- a/esphome/components/mcp4461/__init__.py +++ b/esphome/components/mcp4461/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType CODEOWNERS = ["@p1ngb4ck"] DEPENDENCIES = ["i2c"] @@ -30,7 +31,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable( config[CONF_ID], config[CONF_DISABLE_WIPER_0], diff --git a/esphome/components/mcp4461/output/__init__.py b/esphome/components/mcp4461/output/__init__.py index 99d4988c90..db1a1e6a29 100644 --- a/esphome/components/mcp4461/output/__init__.py +++ b/esphome/components/mcp4461/output/__init__.py @@ -3,6 +3,9 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID, CONF_INITIAL_VALUE +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType from .. import CONF_MCP4461_ID, Mcp4461Component, mcp4461_ns @@ -34,7 +37,7 @@ CONF_NONVOLATILE_WRITE_DELAY = "nonvolatile_write_delay" VOLATILE_CHANNELS = ("A", "B", "C", "D") -def _validate_nonvolatile(config) -> None: +def _validate_nonvolatile(config: ConfigType) -> None: channel = str(config[CONF_CHANNEL]) # Channels E-H address the nonvolatile registers directly — the mirroring options only @@ -89,7 +92,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( FINAL_VALIDATE_SCHEMA = _validate_nonvolatile -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_MCP4461_ID]) var = cg.new_Pvariable( config[CONF_ID], @@ -147,7 +150,12 @@ TERMINAL_ACTION_SCHEMA = cv.Schema( @automation.register_action( "mcp4461.wiper.decrease", WiperDecreaseAction, WIPER_ACTION_SCHEMA, synchronous=True ) -async def mcp4461_wiper_step_to_code(config, action_id, template_arg, args): +async def mcp4461_wiper_step_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: wiper = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, wiper) @@ -158,7 +166,12 @@ async def mcp4461_wiper_step_to_code(config, action_id, template_arg, args): WIPER_ACTION_SCHEMA, synchronous=True, ) -async def mcp4461_wiper_store_to_code(config, action_id, template_arg, args): +async def mcp4461_wiper_store_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: wiper = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, wiper) @@ -169,7 +182,12 @@ async def mcp4461_wiper_store_to_code(config, action_id, template_arg, args): TERMINAL_ACTION_SCHEMA, synchronous=True, ) -async def mcp4461_wiper_terminal_to_code(config, action_id, template_arg, args): +async def mcp4461_wiper_terminal_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: wiper = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable( action_id, template_arg, wiper, ord(config[CONF_TERMINAL]), config[CONF_ENABLE] diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index b7d0ad1998..33cb27080c 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -15,6 +15,9 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PARTS_PER_MILLION, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["uart"] @@ -78,7 +81,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) @@ -129,7 +132,12 @@ NO_ARGS_ACTION_SCHEMA = maybe_simple_id( NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def mhz19_no_args_action_to_code(config, action_id, template_arg, args): +async def mhz19_no_args_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -151,7 +159,12 @@ RANGE_ACTION_SCHEMA = maybe_simple_id( RANGE_ACTION_SCHEMA, synchronous=True, ) -async def mhz19_detection_range_set_to_code(config, action_id, template_arg, args): +async def mhz19_detection_range_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) detection_range = config.get(CONF_DETECTION_RANGE) diff --git a/esphome/components/micronova/__init__.py b/esphome/components/micronova/__init__.py index b462352229..ff06d0b913 100644 --- a/esphome/components/micronova/__init__.py +++ b/esphome/components/micronova/__init__.py @@ -7,6 +7,8 @@ import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@jorre05", "@edenhaus"] @@ -63,7 +65,7 @@ def MICRONOVA_ADDRESS_SCHEMA( default_memory_location: int | None = None, default_memory_address: int | None = None, is_polling_component: bool, -): +) -> cv.Schema: location_key = ( cv.Optional(CONF_MEMORY_LOCATION, default=default_memory_location) if default_memory_location is not None @@ -91,7 +93,9 @@ def register_micronova_writer() -> None: _get_data().has_writer = True -async def to_code_micronova_listener(mv, var, config): +async def to_code_micronova_listener( + mv: MockObj, var: MockObj, config: ConfigType +) -> None: _get_data().listener_count += 1 await cg.register_component(var, config) cg.add(var.set_memory_location(config[CONF_MEMORY_LOCATION])) @@ -100,7 +104,7 @@ async def to_code_micronova_listener(mv, var, config): cg.add(mv.register_micronova_listener(var)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN]) var = cg.new_Pvariable(config[CONF_ID], enable_rx_pin) await cg.register_component(var, config) diff --git a/esphome/components/micronova/button/__init__.py b/esphome/components/micronova/button/__init__.py index 63b127e63d..68b5b9aca6 100644 --- a/esphome/components/micronova/button/__init__.py +++ b/esphome/components/micronova/button/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import button import esphome.config_validation as cv +from esphome.types import ConfigType from .. import ( CONF_MEMORY_ADDRESS, @@ -33,7 +34,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if custom_button_config := config.get(CONF_CUSTOM_BUTTON): diff --git a/esphome/components/micronova/number/__init__.py b/esphome/components/micronova/number/__init__.py index bcc972c5a9..d33bb150ce 100644 --- a/esphome/components/micronova/number/__init__.py +++ b/esphome/components/micronova/number/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv from esphome.const import CONF_STEP, DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS +from esphome.types import ConfigType from .. import ( CONF_MICRONOVA_ID, @@ -56,7 +57,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if thermostat_temperature_config := config.get(CONF_THERMOSTAT_TEMPERATURE): diff --git a/esphome/components/micronova/sensor/__init__.py b/esphome/components/micronova/sensor/__init__.py index e53c49aca5..6091718d65 100644 --- a/esphome/components/micronova/sensor/__init__.py +++ b/esphome/components/micronova/sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_REVOLUTIONS_PER_MINUTE, ) +from esphome.types import ConfigType from .. import ( CONF_MICRONOVA_ID, @@ -125,7 +126,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) for key, divisor in { diff --git a/esphome/components/micronova/switch/__init__.py b/esphome/components/micronova/switch/__init__.py index e149ee3ce3..1f57497ad7 100644 --- a/esphome/components/micronova/switch/__init__.py +++ b/esphome/components/micronova/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import ICON_POWER +from esphome.types import ConfigType from .. import ( CONF_MICRONOVA_ID, @@ -49,7 +50,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if stove_config := config.get(CONF_STOVE): diff --git a/esphome/components/micronova/text_sensor/__init__.py b/esphome/components/micronova/text_sensor/__init__.py index 33d0779eae..d6b94c437f 100644 --- a/esphome/components/micronova/text_sensor/__init__.py +++ b/esphome/components/micronova/text_sensor/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv +from esphome.types import ConfigType from .. import ( CONF_MICRONOVA_ID, @@ -33,7 +34,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if stove_state_config := config.get(CONF_STOVE_STATE): diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index 6b5ee8c3e1..9a3f5b43e7 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from esphome import automation from esphome.automation import maybe_simple_id import esphome.codegen as cg @@ -12,8 +14,10 @@ from esphome.const import ( CONF_ON_DATA, CONF_TRIGGER_ID, ) -from esphome.core import CORE +from esphome.core import CORE, ID from esphome.coroutine import CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType AUTO_LOAD = ["audio"] CODEOWNERS = ["@jesserockz", "@kahrendt"] @@ -50,7 +54,7 @@ IsCapturingCondition = microphone_ns.class_( IsMutedCondition = microphone_ns.class_("IsMutedCondition", automation.Condition) -async def setup_microphone_core_(var, config): +async def setup_microphone_core_(var: MockObj, config: ConfigType) -> None: for conf in config.get(CONF_ON_DATA, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation( @@ -60,7 +64,7 @@ async def setup_microphone_core_(var, config): ) -async def register_microphone(var, config): +async def register_microphone(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) await setup_microphone_core_(var, config) @@ -85,7 +89,7 @@ def microphone_source_schema( max_bits_per_sample: int = 16, min_channels: int = 1, max_channels: int = 1, -): +) -> cv.All: """Schema for a microphone source Components requesting microphone data should use this schema instead of accessing a microphone directly. @@ -97,7 +101,7 @@ def microphone_source_schema( max_channels (int, optional): Maximum number of channels the requesting component supports. Defaults to 1. """ - def _validate_unique_channels(config): + def _validate_unique_channels(config: list[int]) -> list[int]: if len(config) != len(set(config)): raise cv.Invalid("Channels must be unique") return config @@ -124,7 +128,7 @@ def microphone_source_schema( def final_validate_microphone_source_schema( component_name: str, sample_rate: int = cv.UNDEFINED -): +) -> Callable[[ConfigType], ConfigType]: """Validates that the microphone source can provide audio in the correct format. In particular it validates the sample rate and the enabled channels. Note that: @@ -136,7 +140,7 @@ def final_validate_microphone_source_schema( sample_rate (int, optional): The sample rate the component requesting mic audio requires """ - def _validate_audio_compatability(config): + def _validate_audio_compatability(config: ConfigType) -> ConfigType: if sample_rate is not cv.UNDEFINED: # Issues require changing the microphone configuration # - Verifies sample rates match @@ -161,7 +165,9 @@ def final_validate_microphone_source_schema( return _validate_audio_compatability -async def microphone_source_to_code(config, passive=False): +async def microphone_source_to_code( + config: ConfigType, passive: bool = False +) -> MockObj: """Creates a MicrophoneSource variable for codegen. Setting passive to true makes the MicrophoneSource never start/stop the microphone, but only receives audio when another component has actively started the Microphone. If false, then the microphone needs to be explicitly started/stopped. @@ -183,7 +189,12 @@ async def microphone_source_to_code(config, passive=False): return mic_source -async def microphone_action(config, action_id, template_arg, args): +async def microphone_action( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -219,6 +230,6 @@ automation.register_condition( @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(microphone_ns.using) cg.add_define("USE_MICROPHONE") diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index 8c125a9606..b23982655a 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -53,6 +53,7 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.final_validate import full_config +from esphome.types import ConfigType from . import mipi_dsi_ns, models from .models import DsiDriverChip @@ -85,7 +86,7 @@ COLOR_DEPTHS = { } -def model_schema(config): +def model_schema(config: ConfigType) -> cv.All: model = MODELS[config[CONF_MODEL].upper()] transform = model.transform_schema() # CUSTOM model will need to provide a custom init sequence @@ -148,7 +149,7 @@ def model_schema(config): @model_schema_extractor(MODELS, model_schema) -def _config_schema(config): +def _config_schema(config: ConfigType) -> ConfigType: config = cv.Schema( { cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), @@ -175,7 +176,7 @@ def _config_schema(config): return config -def _final_validate(config) -> None: +def _final_validate(config: ConfigType) -> None: global_config = full_config.get() from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN @@ -189,7 +190,7 @@ CONFIG_SCHEMA = _config_schema FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: model = MODELS[config[CONF_MODEL].upper()] color_depth = COLOR_DEPTHS[get_color_depth(config)] pixel_mode = int(config[CONF_PIXEL_MODE].removesuffix("bit")) diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 897088a257..e23e19a000 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -1,5 +1,6 @@ import importlib import pkgutil +from typing import Any from esphome import pins import esphome.codegen as cg @@ -72,6 +73,7 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.final_validate import full_config +from esphome.types import ConfigType from . import models from .models import RgbDriverChip @@ -97,7 +99,7 @@ for module_info in pkgutil.iter_modules(models.__path__): MODELS = DriverChip.get_models() -def data_pin_validate(value): +def data_pin_validate(value: Any) -> ConfigType: """ It is safe to use strapping pins as RGB output data bits, as they are outputs only, and not initialised until after boot. @@ -112,14 +114,14 @@ def data_pin_validate(value): return DATA_PIN_SCHEMA(value) -def data_pin_set(length): +def data_pin_set(length: int) -> cv.All: return cv.All( [data_pin_validate], cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), ) -def model_schema(config): +def model_schema(config: ConfigType) -> cv.Schema: model = MODELS[config[CONF_MODEL].upper()] transform = model.transform_schema() # RPI model does not use an init sequence, indicates with empty list @@ -213,7 +215,7 @@ def model_schema(config): @model_schema_extractor(MODELS, model_schema) -def _config_schema(config): +def _config_schema(config: ConfigType) -> ConfigType: config = cv.Schema( { cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), @@ -248,7 +250,7 @@ def _config_schema(config): CONFIG_SCHEMA = _config_schema -def _final_validate(config) -> None: +def _final_validate(config: ConfigType) -> None: global_config = full_config.get() from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN @@ -265,7 +267,7 @@ def _final_validate(config) -> None: FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: model = MODELS[config[CONF_MODEL].upper()] width, height, _offset_width, _offset_height, _pad_width, _pad_height = ( model.get_dimensions(config) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py index a20e9d1c01..cad5dc8e20 100644 --- a/esphome/components/mipi_rgb/models/st7701s.py +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -8,7 +8,7 @@ SDIR_CMD = 0xC7 class ST7701S(RgbDriverChip): # The ST7701s does not use the standard MADCTL bits for x/y mirroring - def add_madctl(self, sequence: list, config: dict): + def add_madctl(self, sequence: list, config: dict) -> int: transform = self.get_transform(config) madctl = 0x00 if config[CONF_COLOR_ORDER] == MODE_BGR: diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 246db237b1..e8b54da5c7 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -53,8 +53,9 @@ from esphome.const import ( CONF_TRANSFORM, CONF_WIDTH, ) -from esphome.cpp_generator import TemplateArguments +from esphome.cpp_generator import MockObjClass, TemplateArguments from esphome.final_validate import full_config +from esphome.types import ConfigType from . import CONF_BUS_MODE, CONF_SPI_16, DOMAIN, models @@ -110,7 +111,7 @@ DISPLAY_PIXEL_MODES = { } -def denominator(config): +def denominator(config: ConfigType) -> int: """ Calculate the best denominator for a buffer size fraction. The denominator should be a number between 2 and 16 that divides the display height evenly, @@ -132,7 +133,7 @@ def denominator(config): return next(x for x in range(2, 17) if frac >= 1 / x) -def model_schema(config): +def model_schema(config: ConfigType) -> cv.All | cv.Schema: model = MODELS[config[CONF_MODEL]] bus_mode = config[CONF_BUS_MODE] transform = model.transform_schema() @@ -238,7 +239,7 @@ def model_schema(config): @model_schema_extractor(MODELS, model_schema, extra={CONF_BUS_MODE: TYPE_SINGLE}) -def customise_schema(config): +def customise_schema(config: ConfigType) -> ConfigType: """ Create a customised config schema for a specific model and validate the configuration. :param config: The configuration dictionary to validate @@ -305,7 +306,7 @@ def customise_schema(config): CONFIG_SCHEMA = customise_schema -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: global_config = full_config.get() model = MODELS[config[CONF_MODEL]] @@ -341,7 +342,7 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate -def get_instance(config): +def get_instance(config: ConfigType) -> tuple[MockObjClass, list]: """ Get the type of MipiSpi instance to create based on the configuration, and the template arguments. @@ -394,7 +395,7 @@ def get_instance(config): return MipiSpi, templateargs -async def to_code(config): +async def to_code(config: ConfigType) -> None: model = MODELS[config[CONF_MODEL]] var_id = config[CONF_ID] init_sequence = model.get_sequence(config, add_madctl=False, add_reset=True) diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index b269f46dc9..2552451bd7 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -246,33 +246,36 @@ class MipiSpi : public display::Display, this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); this->disable(); } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { - this->dc_pin_->digital_write(false); + // Toggle D/C only while holding the bus; on boards where D/C doubles as + // another bus signal, driving it while another device owns the bus + // corrupts that device's transfer. this->enable(); + this->dc_pin_->digital_write(false); this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8); - this->disable(); this->dc_pin_->digital_write(true); + this->disable(); if (len != 0) { this->enable(); this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8); this->disable(); } } else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) { - this->dc_pin_->digital_write(false); this->enable(); + this->dc_pin_->digital_write(false); this->write_byte(cmd); - this->disable(); this->dc_pin_->digital_write(true); + this->disable(); if (len != 0) { this->enable(); this->write_array(bytes, len); this->disable(); } } else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) { - this->dc_pin_->digital_write(false); this->enable(); + this->dc_pin_->digital_write(false); this->write_byte(cmd); - this->disable(); this->dc_pin_->digital_write(true); + this->disable(); for (size_t i = 0; i != len; i++) { this->enable(); this->write_byte(0); diff --git a/esphome/components/mitsubishi_cn105/__init__.py b/esphome/components/mitsubishi_cn105/__init__.py index 450d1cd222..470b7be5fc 100644 --- a/esphome/components/mitsubishi_cn105/__init__.py +++ b/esphome/components/mitsubishi_cn105/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_ON_STATE, CONF_TEMPERATURE, CONF_UPDATE_INTERVAL, + CONF_USE_FAHRENHEIT, ) from esphome.core import ID, Lambda from esphome.cpp_generator import LambdaExpression, MockObj @@ -71,6 +72,7 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_TELEMETRY_REQUEST_MIN_INTERVAL, default="60s" ): cv.update_interval, + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, cv.Optional(CONF_VANE): cv.Schema( { cv.Optional(CONF_ON_STATE): automation.validate_automation({}), @@ -114,6 +116,7 @@ async def to_code(config: ConfigType) -> None: config[CONF_TELEMETRY_REQUEST_MIN_INTERVAL] ) ) + cg.add(var.set_use_fahrenheit(config[CONF_USE_FAHRENHEIT])) if on_state := config.get(CONF_VANE, {}).get(CONF_ON_STATE): cg.add_global(mitsubishi_ns.using) for conf in on_state: diff --git a/esphome/components/mitsubishi_cn105/automation.h b/esphome/components/mitsubishi_cn105/automation.h index 2fc6ba3c32..f9ca3a47e6 100644 --- a/esphome/components/mitsubishi_cn105/automation.h +++ b/esphome/components/mitsubishi_cn105/automation.h @@ -9,7 +9,7 @@ namespace esphome::mitsubishi_cn105 { template -class SetRemoteTemperatureAction : public Action, public Parented { +class SetRemoteTemperatureAction final : public Action, public Parented { public: TEMPLATABLE_VALUE(float, temperature) @@ -17,12 +17,12 @@ class SetRemoteTemperatureAction : public Action, public Parented -class ClearRemoteTemperatureAction : public Action, public Parented { +class ClearRemoteTemperatureAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->clear_remote_temperature(); } }; -template class VaneControlAction : public Action { +template class VaneControlAction final : public Action { public: using ApplyFn = void (*)(VaneCall &, const std::remove_cvref_t &...); diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index b6b11b4820..4d3f899dee 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -83,6 +83,7 @@ class MitsubishiCN105 { return this->is_telemetry_polling_enabled() ? !std::isnan(this->status_.room_temperature) : !std::isnan(this->status_.target_temperature); } + bool is_temperature_encoding_b() const { return this->property_context_.use_temperature_encoding_b; } void set_power(bool power_on); void set_target_temperature(float target_temperature); diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index 197e1e1bb5..53b8c21de6 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -50,7 +50,11 @@ static constexpr std::optional reverse_map_lookup(const std::arrayparent_->get_temperature_mapping().get_use_fahrenheit() ? 'F' : 'C'); +} void MitsubishiCN105Climate::setup() { this->parent_->add_on_status_callback([this]() { this->apply_values_(); }); @@ -70,15 +74,17 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() { traits.add_supported_fan_mode(p.second); } - traits.set_supported_swing_modes(this->supported_swing_modes_); + traits.set_supported_swing_modes(this->swing_mode_manager_.supported_swing_modes()); - traits.set_visual_min_temperature(16.0f); - traits.set_visual_max_temperature(31.0f); + const bool use_fahrenheit = this->parent_->get_temperature_mapping().get_use_fahrenheit(); + traits.set_temperature_unit(use_fahrenheit ? TemperatureUnit::FAHRENHEIT : TemperatureUnit::CELSIUS); + traits.set_visual_min_temperature(use_fahrenheit ? 61.0f : 16.0f); + traits.set_visual_max_temperature(use_fahrenheit ? 88.0f : 31.0f); traits.set_visual_temperature_step(1.0f); if (this->parent_->is_telemetry_polling_enabled()) { traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); - traits.set_visual_current_temperature_step(0.5f); + traits.set_visual_current_temperature_step(use_fahrenheit ? 1.0f : 0.5f); } return traits; @@ -86,7 +92,7 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() { void MitsubishiCN105Climate::control(const climate::ClimateCall &call) { if (const auto target_temperature = call.get_target_temperature()) { - this->parent_->set_target_temperature(*target_temperature); + this->parent_->set_target_temperature(this->parent_->get_temperature_mapping().to_mitsubishi(*target_temperature)); } if (const auto mode = call.get_mode()) { @@ -103,33 +109,11 @@ void MitsubishiCN105Climate::control(const climate::ClimateCall &call) { } if (const auto swing_mode = call.get_swing_mode()) { - auto vane = this->last_non_swing_vane_mode_; - auto wide = this->last_non_swing_wide_vane_mode_; - - switch (*swing_mode) { - case climate::CLIMATE_SWING_BOTH: - vane = MitsubishiCN105::VaneMode::SWING; - wide = MitsubishiCN105::WideVaneMode::SWING; - break; - - case climate::CLIMATE_SWING_VERTICAL: - vane = MitsubishiCN105::VaneMode::SWING; - break; - - case climate::CLIMATE_SWING_HORIZONTAL: - wide = MitsubishiCN105::WideVaneMode::SWING; - break; - - case climate::CLIMATE_SWING_OFF: - default: - break; + if (const auto vane = this->swing_mode_manager_.vane_from(*swing_mode)) { + this->parent_->set_vane_mode(*vane); } - - if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) { - this->parent_->set_vane_mode(vane); - } - if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) { - this->parent_->set_wide_vane_mode(wide); + if (const auto wide = this->swing_mode_manager_.wide_vane_from(*swing_mode)) { + this->parent_->set_wide_vane_mode(*wide); } } @@ -139,10 +123,10 @@ void MitsubishiCN105Climate::control(const climate::ClimateCall &call) { void MitsubishiCN105Climate::apply_values_() { const auto &status = this->parent_->status(); - this->target_temperature = status.target_temperature; + this->target_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.target_temperature); if (this->parent_->is_telemetry_polling_enabled()) { - this->current_temperature = status.room_temperature; + this->current_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.room_temperature); } if (status.power_on) { @@ -160,64 +144,39 @@ void MitsubishiCN105Climate::apply_values_() { ESP_LOGD(TAG, "Unable to map fan mode"); } - if (!this->supported_swing_modes_.empty()) { - bool vertical_swinging = false; - bool horizontal_swinging = false; - - if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) { - if (status.vane_mode == MitsubishiCN105::VaneMode::SWING) { - vertical_swinging = true; - } else if (status.vane_mode != MitsubishiCN105::VaneMode::UNKNOWN) { - this->last_non_swing_vane_mode_ = status.vane_mode; - } - } - - if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) { - if (status.wide_vane_mode == MitsubishiCN105::WideVaneMode::SWING) { - horizontal_swinging = true; - } else if (status.wide_vane_mode != MitsubishiCN105::WideVaneMode::UNKNOWN) { - this->last_non_swing_wide_vane_mode_ = status.wide_vane_mode; - } - } - - if (vertical_swinging && horizontal_swinging) { - this->swing_mode = climate::CLIMATE_SWING_BOTH; - } else if (vertical_swinging) { - this->swing_mode = climate::CLIMATE_SWING_VERTICAL; - } else if (horizontal_swinging) { - this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - } else { - this->swing_mode = climate::CLIMATE_SWING_OFF; - } + if (const auto swing_mode = + this->swing_mode_manager_.update_and_get_swing_mode(status.vane_mode, status.wide_vane_mode)) { + this->swing_mode = *swing_mode; } this->publish_state(); } void MitsubishiCN105Climate::set_supported_swing_mode(climate::ClimateSwingMode mode) { - this->supported_swing_modes_.clear(); + climate::ClimateSwingModeMask supported_swing_modes; switch (mode) { case climate::CLIMATE_SWING_VERTICAL: - this->supported_swing_modes_.insert(climate::CLIMATE_SWING_OFF); - this->supported_swing_modes_.insert(climate::CLIMATE_SWING_VERTICAL); + supported_swing_modes.insert(climate::CLIMATE_SWING_OFF); + supported_swing_modes.insert(climate::CLIMATE_SWING_VERTICAL); break; case climate::CLIMATE_SWING_HORIZONTAL: - this->supported_swing_modes_.insert(climate::CLIMATE_SWING_OFF); - this->supported_swing_modes_.insert(climate::CLIMATE_SWING_HORIZONTAL); + supported_swing_modes.insert(climate::CLIMATE_SWING_OFF); + supported_swing_modes.insert(climate::CLIMATE_SWING_HORIZONTAL); break; case climate::CLIMATE_SWING_BOTH: - this->supported_swing_modes_.insert(climate::CLIMATE_SWING_OFF); - this->supported_swing_modes_.insert(climate::CLIMATE_SWING_VERTICAL); - this->supported_swing_modes_.insert(climate::CLIMATE_SWING_HORIZONTAL); - this->supported_swing_modes_.insert(climate::CLIMATE_SWING_BOTH); + supported_swing_modes.insert(climate::CLIMATE_SWING_OFF); + supported_swing_modes.insert(climate::CLIMATE_SWING_VERTICAL); + supported_swing_modes.insert(climate::CLIMATE_SWING_HORIZONTAL); + supported_swing_modes.insert(climate::CLIMATE_SWING_BOTH); break; case climate::CLIMATE_SWING_OFF: default: break; } + this->swing_mode_manager_.set_supported_swing_modes(supported_swing_modes); } } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h index 5341c2d2d9..cea76278ab 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h @@ -6,10 +6,13 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/climate/climate.h" +#include "mitsubishi_cn105_swing_mode_manager.h" namespace esphome::mitsubishi_cn105 { -class MitsubishiCN105Climate : public climate::Climate, public Component, public Parented { +class MitsubishiCN105Climate final : public climate::Climate, + public Component, + public Parented { public: void setup() override; void dump_config() override; @@ -25,14 +28,12 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public protected: void apply_values_(); - climate::ClimateSwingModeMask supported_swing_modes_{}; - MitsubishiCN105::VaneMode last_non_swing_vane_mode_{MitsubishiCN105::VaneMode::AUTO}; - MitsubishiCN105::WideVaneMode last_non_swing_wide_vane_mode_{MitsubishiCN105::WideVaneMode::CENTER}; + SwingModeManager swing_mode_manager_; }; // Legacy climate action compatibility. Remove in 2027.2.0. template -class LegacySetRemoteTemperatureAction : public Action, public Parented { +class LegacySetRemoteTemperatureAction final : public Action, public Parented { public: TEMPLATABLE_VALUE(float, temperature) @@ -41,7 +42,7 @@ class LegacySetRemoteTemperatureAction : public Action, public Parented -class LegacyClearRemoteTemperatureAction : public Action, public Parented { +class LegacyClearRemoteTemperatureAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->clear_remote_temperature(); } }; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp index 8e9e954645..e2a6ee05af 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp @@ -27,6 +27,13 @@ void MitsubishiCN105Component::setup() { this->hp_.initialize(); } void MitsubishiCN105Component::loop() { if (this->hp_.update()) { + // Encoding A only supports whole °C values and cannot represent native °F setpoints accurately. + // See https://github.com/esphome/esphome/pull/15488#issuecomment-5268304343 + if (this->temperature_mapping_.get_use_fahrenheit() && !this->hp_.is_temperature_encoding_b()) { + ESP_LOGE(TAG, "Unit reports encoding A, which cannot accurately convert °F setpoints; disable 'use_fahrenheit'"); + this->mark_failed(); + return; + } this->notify_status_listeners_(); } } diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h index 6461fb464b..aa9bfe0d8c 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h @@ -3,13 +3,43 @@ #include "mitsubishi_cn105.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/uart/uart.h" -#include +#include +#include #include +#include namespace esphome::mitsubishi_cn105 { +struct TemperatureMapping { + float to_mitsubishi(float value) const { + if (!this->use_fahrenheit_) { + return value; + } + const int fahrenheit = std::clamp(static_cast(std::round(value)), 61, 88); + return 0.5f * (fahrenheit - 28 + (fahrenheit > 68) - (fahrenheit < 68)); + } + + float from_mitsubishi(float value) const { + if (!this->use_fahrenheit_) { + return value; + } + if (value < 16.0f || value > 30.5f) { + return celsius_to_fahrenheit(value); + } + const int mitsubishi_half_degrees = static_cast(std::round(value * 2.0f)); + return mitsubishi_half_degrees + 29 - (mitsubishi_half_degrees >= 40) - (mitsubishi_half_degrees > 40); + } + + bool get_use_fahrenheit() const { return this->use_fahrenheit_; } + void set_use_fahrenheit(bool value) { this->use_fahrenheit_ = value; } + + protected: + bool use_fahrenheit_{false}; +}; + enum VerticalVaneMode : uint8_t { VERTICAL_VANE_MODE_AUTO = static_cast(MitsubishiCN105::VaneMode::AUTO), VERTICAL_VANE_MODE_POSITION_1 = static_cast(MitsubishiCN105::VaneMode::POSITION_1), @@ -50,7 +80,7 @@ struct VaneCall { MitsubishiCN105Component *parent_; }; -class MitsubishiCN105Component : public Component, public uart::UARTDevice { +class MitsubishiCN105Component final : public Component, public uart::UARTDevice { public: explicit MitsubishiCN105Component() : hp_(*this) {} @@ -60,6 +90,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice { void set_update_interval(uint32_t ms) { this->hp_.set_update_interval(ms); } void set_telemetry_request_min_interval(uint32_t ms) { this->hp_.set_telemetry_request_min_interval(ms); } + void set_use_fahrenheit(bool value) { this->temperature_mapping_.set_use_fahrenheit(value); } void set_remote_temperature(float temperature) { this->hp_.set_remote_temperature(temperature); } void clear_remote_temperature() { this->hp_.clear_remote_temperature(); } @@ -75,6 +106,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice { const MitsubishiCN105::Status &status() const { return this->hp_.status(); } bool is_status_initialized() const { return this->hp_.is_status_initialized(); } bool is_telemetry_polling_enabled() const { return this->hp_.is_telemetry_polling_enabled(); } + const TemperatureMapping &get_temperature_mapping() const { return this->temperature_mapping_; } template void add_on_status_callback(F &&callback) { this->status_callback_.add(std::forward(callback)); @@ -99,6 +131,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice { } MitsubishiCN105 hp_; + TemperatureMapping temperature_mapping_; CallbackManager status_callback_; LazyCallbackManager vane_state_callback_; }; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_swing_mode_manager.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_swing_mode_manager.h new file mode 100644 index 0000000000..20f54f0bbb --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_swing_mode_manager.h @@ -0,0 +1,86 @@ +#pragma once + +#include + +#include "esphome/components/climate/climate.h" +#include "mitsubishi_cn105.h" + +namespace esphome::mitsubishi_cn105 { + +class SwingModeManager final { + public: + const climate::ClimateSwingModeMask &supported_swing_modes() const { return this->supported_swing_modes_; } + void set_supported_swing_modes(const climate::ClimateSwingModeMask &supported_swing_modes) { + this->supported_swing_modes_ = supported_swing_modes; + } + + std::optional vane_from(climate::ClimateSwingMode swing_mode) const { + if (!this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) { + return std::nullopt; + } + + switch (swing_mode) { + case climate::CLIMATE_SWING_BOTH: + case climate::CLIMATE_SWING_VERTICAL: + return MitsubishiCN105::VaneMode::SWING; + default: + return this->last_non_swing_vane_mode_; + } + } + + std::optional wide_vane_from(climate::ClimateSwingMode swing_mode) const { + if (!this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) { + return std::nullopt; + } + + switch (swing_mode) { + case climate::CLIMATE_SWING_BOTH: + case climate::CLIMATE_SWING_HORIZONTAL: + return MitsubishiCN105::WideVaneMode::SWING; + default: + return this->last_non_swing_wide_vane_mode_; + } + } + + std::optional update_and_get_swing_mode(MitsubishiCN105::VaneMode vane_mode, + MitsubishiCN105::WideVaneMode wide_vane_mode) { + if (this->supported_swing_modes_.empty()) { + return std::nullopt; + } + + bool vertical_swinging = false; + bool horizontal_swinging = false; + if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) { + if (vane_mode == MitsubishiCN105::VaneMode::SWING) { + vertical_swinging = true; + } else if (vane_mode != MitsubishiCN105::VaneMode::UNKNOWN) { + this->last_non_swing_vane_mode_ = vane_mode; + } + } + if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) { + if (wide_vane_mode == MitsubishiCN105::WideVaneMode::SWING) { + horizontal_swinging = true; + } else if (wide_vane_mode != MitsubishiCN105::WideVaneMode::UNKNOWN) { + this->last_non_swing_wide_vane_mode_ = wide_vane_mode; + } + } + + if (vertical_swinging && horizontal_swinging) { + return climate::CLIMATE_SWING_BOTH; + } + if (vertical_swinging) { + return climate::CLIMATE_SWING_VERTICAL; + } + if (horizontal_swinging) { + return climate::CLIMATE_SWING_HORIZONTAL; + } + return climate::CLIMATE_SWING_OFF; + } + + private: + climate::ClimateSwingModeMask supported_swing_modes_{}; + MitsubishiCN105::VaneMode last_non_swing_vane_mode_{MitsubishiCN105::VaneMode::AUTO}; + MitsubishiCN105::WideVaneMode last_non_swing_wide_vane_mode_{MitsubishiCN105::WideVaneMode::CENTER}; +}; + +} // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical.h b/esphome/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical.h index 76977d59d7..656b78b487 100644 --- a/esphome/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical.h +++ b/esphome/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical.h @@ -7,9 +7,9 @@ namespace esphome::mitsubishi_cn105 { -class MitsubishiCN105VerticalVaneDirectionSelect : public select::Select, - public Component, - public Parented { +class MitsubishiCN105VerticalVaneDirectionSelect final : public select::Select, + public Component, + public Parented { public: void setup() override; void publish_vane_state(MitsubishiCN105::VaneMode mode); diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index 47164a9997..a3746c019a 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -15,8 +15,11 @@ from esphome.const import ( CONF_TIMEOUT, PLATFORM_ESP32, ) +from esphome.core import ID from esphome.core.entity_helpers import inherit_property_from +from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv +from esphome.types import ConfigType AUTO_LOAD = ["audio"] CODEOWNERS = ["@kahrendt"] @@ -48,7 +51,7 @@ SOURCE_SPEAKER_SCHEMA = speaker.SPEAKER_SCHEMA.extend( ) -def _validate_source_speaker(config): +def _validate_source_speaker(config: ConfigType) -> ConfigType: fconf = fv.full_config.get() # Get ID for the output speaker and add it to the source speakers config to easily inherit properties @@ -70,7 +73,7 @@ def _validate_source_speaker(config): return config -def _validate_output_speaker(config): +def _validate_output_speaker(config: ConfigType) -> ConfigType: audio.final_validate_audio_schema( "mixer", audio_device=CONF_OUTPUT_SPEAKER, @@ -112,7 +115,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -161,7 +164,12 @@ async def to_code(config): ), synchronous=True, ) -async def ducking_set_to_code(config, action_id, template_arg, args): +async def ducking_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) decibel_reduction = await cg.templatable( diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 58bd0f65dc..89ffc7facf 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -1,15 +1,23 @@ from __future__ import annotations import logging -from typing import Any, Literal +from typing import Any, Literal, NamedTuple from esphome import pins import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ADDRESS, CONF_DISABLE_CRC, CONF_FLOW_CONTROL_PIN, CONF_ID +from esphome.const import ( + CONF_ADDRESS, + CONF_CONTINUOUS, + CONF_DISABLE_CRC, + CONF_FLOW_CONTROL_PIN, + CONF_ID, +) +from esphome.cpp_generator import MockObj from esphome.cpp_helpers import gpio_pin_expression import esphome.final_validate as fv +from esphome.types import ConfigType, TemplateArgsType _LOGGER = logging.getLogger(__name__) @@ -46,6 +54,73 @@ CONF_TURNAROUND_TIME = "turnaround_time" MODBUS_ROLES = ["client", "server"] + +class _CommandOption(NamedTuple): + """One per-command option forwarded to the hub (modbus::CommandOptions).""" + + conf_key: str + field: str # the C++ field, and so the set_() setter name + validator: Any # the static (non-templatable) validator for the key + cpp_type: Any # the C++ type the value is generated as + default: Any + + +# Per-direction command options. Single-sourcing the schema and the setter generation here keeps +# them from drifting; the C++ side must add the matching field per the rules documented on +# CommandOptions (modbus.h). +_COMMAND_OPTIONS: dict[str, list[_CommandOption]] = { + "read": [_CommandOption(CONF_CONTINUOUS, "continuous", cv.boolean, bool, False)], + "write": [], +} + + +def _command_options(direction: str) -> list[_CommandOption]: + try: + return _COMMAND_OPTIONS[direction] + except KeyError: + raise ValueError(f"unknown command-options direction {direction!r}") from None + + +def command_options_schema( + *, direction: Literal["read", "write"], templatable: bool = False +) -> dict[cv.Optional, Any]: + """Schema fragment for the per-command options a component forwards to the hub + (modbus::CommandOptions). Extend this into any schema that queues commands. Keys are + direction-specific so a schema never offers an option the hub would strip (e.g. + continuous on a write); the write side has no options yet. For actions (templatable=True the + keys also accept lambdas), register the values with register_templatable_command_options(). + """ + return { + cv.Optional(option.conf_key, default=option.default): ( + cv.templatable(option.validator) if templatable else option.validator + ) + for option in _command_options(direction) + } + + +async def register_templatable_command_options( + var: MockObj, config: ConfigType, args: TemplateArgsType, direction: str +) -> None: + """Generate the set_