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/docker/Dockerfile b/docker/Dockerfile index 55aa0ac982..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.1 +RUN uv pip install --no-cache-dir esphome-device-builder==1.12.4 RUN \ platformio settings set enable_telemetry No \ 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/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..0dc4b905bf 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,5 +1,6 @@ import base64 import logging +from typing import Any from esphome import automation from esphome.automation import Condition @@ -129,7 +130,7 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType: return config -def validate_encryption_key(value): +def validate_encryption_key(value: Any) -> str: value = cv.string_strict(value) try: decoded = base64.b64decode(value, validate=True) @@ -217,7 +218,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) @@ -256,7 +257,7 @@ ENCRYPTION_SCHEMA = cv.Schema( ) -def _encryption_schema(config): +def _encryption_schema(config: ConfigType | None) -> ConfigType: if config is None: config = {} return ENCRYPTION_SCHEMA(config) @@ -393,7 +394,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 @@ -581,7 +582,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 +648,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 +677,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 +730,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 +751,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 +835,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/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/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/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/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/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/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 704a61d4de..ffd121499b 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/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/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/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/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/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/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_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 28c8c7fcf1..4f6355df70 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -38,7 +38,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 @@ -262,7 +263,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) @@ -360,7 +361,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 +390,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 +418,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_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_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 b808234240..5c8bef7fca 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 @@ -166,7 +168,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"), @@ -209,7 +211,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) @@ -287,7 +289,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") @@ -516,7 +518,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) @@ -537,7 +539,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)) @@ -561,7 +563,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/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/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/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 7686b64cb4..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") @@ -845,7 +847,12 @@ def _filter_source_files() -> list[str]: 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/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/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..7cc16eb5b2 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) 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 1717870238..401bba5118 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" @@ -41,7 +42,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/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 94aad4d019..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,7 +283,7 @@ 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: @@ -358,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 @@ -375,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. @@ -390,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/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/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/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/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/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/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/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/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/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..17ff6d34ca 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_(); }); @@ -72,13 +76,15 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() { traits.set_supported_swing_modes(this->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()) { @@ -139,10 +145,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) { 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..508a15e6d5 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), @@ -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/modbus/__init__.py b/esphome/components/modbus/__init__.py index 58bd0f65dc..a98591c6bc 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -8,8 +8,10 @@ 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.cpp_generator import MockObj from esphome.cpp_helpers import gpio_pin_expression import esphome.final_validate as fv +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -84,7 +86,7 @@ CONFIG_SCHEMA = cv.typed_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(modbus_ns.using) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -112,7 +114,9 @@ def _validate_server_address(value: Any) -> int: return address -def modbus_device_schema(default_address, role: Literal["client", "server"] = "client"): +def modbus_device_schema( + default_address: int | None, role: Literal["client", "server"] = "client" +) -> cv.Schema: hub_type = ModbusClient if role == "client" else ModbusServer address_validator = _validate_server_address if role == "server" else cv.hex_uint8_t schema = { @@ -127,14 +131,14 @@ def modbus_device_schema(default_address, role: Literal["client", "server"] = "c def final_validate_modbus_device( name: str, *, role: Literal["server", "client"] | None = None -): - def validate_role(value): +) -> cv.Schema: + def validate_role(value: str) -> str: assert role in MODBUS_ROLES if value != role: raise cv.Invalid(f"Component {name} requires role to be {role}") return value - def validate_hub(hub_config): + def validate_hub(hub_config: ConfigType) -> ConfigType: hub_schema = {} if role is not None: hub_schema[cv.Required(CONF_ROLE)] = validate_role @@ -147,19 +151,19 @@ def final_validate_modbus_device( ) -async def register_modbus_client_device(var, config): +async def register_modbus_client_device(var: MockObj, config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_MODBUS_ID]) cg.add(var.set_parent(parent)) cg.add(var.set_address(config[CONF_ADDRESS])) -async def register_modbus_server_device(var, config): +async def register_modbus_server_device(var: MockObj, config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_MODBUS_ID]) cg.add(var.set_address(config[CONF_ADDRESS])) cg.add(parent.register_device(var)) -async def register_modbus_device(var, config): +async def register_modbus_device(var: MockObj, config: ConfigType) -> None: # Remove before 2026.12.0 _LOGGER.warning( "'register_modbus_device' is deprecated, use 'register_modbus_client_device' " diff --git a/esphome/components/mpr121/__init__.py b/esphome/components/mpr121/__init__.py index 0bf9377275..da56b4ff4b 100644 --- a/esphome/components/mpr121/__init__.py +++ b/esphome/components/mpr121/__init__.py @@ -12,7 +12,9 @@ from esphome.const import ( CONF_NUMBER, CONF_OUTPUT, ) +from esphome.cpp_generator import MockObj import esphome.final_validate as fv +from esphome.types import ConfigType CONF_TOUCH_THRESHOLD = "touch_threshold" CONF_RELEASE_THRESHOLD = "release_threshold" @@ -49,7 +51,7 @@ CONFIG_SCHEMA = ( ) -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: fconf = fv.full_config.get() max_touch_channel = 3 if (binary_sensors := fconf.get(CONF_BINARY_SENSOR)) is not None: @@ -71,7 +73,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]) cg.add(var.set_touch_debounce(config[CONF_TOUCH_DEBOUNCE])) cg.add(var.set_release_debounce(config[CONF_RELEASE_DEBOUNCE])) @@ -82,7 +84,7 @@ async def to_code(config): await i2c.register_i2c_device(var, config) -def validate_mode(value): +def validate_mode(value: ConfigType) -> ConfigType: if bool(value[CONF_INPUT]) == bool(value[CONF_OUTPUT]): raise cv.Invalid("Mode must be either input or output") return value @@ -105,7 +107,9 @@ MPR121_GPIO_PIN_SCHEMA = pins.gpio_base_schema( ) -def mpr121_pin_final_validate(pin_config, parent_config): +def mpr121_pin_final_validate( + pin_config: ConfigType, parent_config: ConfigType +) -> None: if pin_config[CONF_NUMBER] <= parent_config[CONF_MAX_TOUCH_CHANNEL]: raise cv.Invalid( "Pin number must be higher than the max touch channel of the MPR121 component", @@ -115,7 +119,7 @@ def mpr121_pin_final_validate(pin_config, parent_config): @pins.PIN_SCHEMA_REGISTRY.register( CONF_MPR121, MPR121_GPIO_PIN_SCHEMA, mpr121_pin_final_validate ) -async def mpr121_gpio_pin_to_code(config): +async def mpr121_gpio_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_MPR121]) diff --git a/esphome/components/mpr121/binary_sensor/__init__.py b/esphome/components/mpr121/binary_sensor/__init__.py index 1252a65a84..565789cdc3 100644 --- a/esphome/components/mpr121/binary_sensor/__init__.py +++ b/esphome/components/mpr121/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_CHANNEL +from esphome.types import ConfigType from .. import ( CONF_MPR121_ID, @@ -24,7 +25,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(MPR121BinarySensor).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) hub = await cg.get_variable(config[CONF_MPR121_ID]) cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index d5ee4c6a9b..0e6a374f9b 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -118,8 +118,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1f; // current_temp_step root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1f; - // temperature units are always coerced to Celsius internally - root[MQTT_TEMPERATURE_UNIT] = "C"; + root[MQTT_TEMPERATURE_UNIT] = traits.get_temperature_unit() == TemperatureUnit::FAHRENHEIT ? "F" : "C"; // min_humidity root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity(); diff --git a/esphome/components/nau7802/sensor.py b/esphome/components/nau7802/sensor.py index 9798c1c297..415ae09daf 100644 --- a/esphome/components/nau7802/sensor.py +++ b/esphome/components/nau7802/sensor.py @@ -4,6 +4,9 @@ import esphome.codegen as cg from esphome.components import i2c, sensor import esphome.config_validation as cv from esphome.const import CONF_GAIN, CONF_ID, ICON_SCALE, STATE_CLASS_MEASUREMENT +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@cujomalainey"] DEPENDENCIES = ["i2c"] @@ -93,7 +96,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) @@ -131,7 +134,12 @@ NAU7802_CALIBRATE_SCHEMA = maybe_simple_id( NAU7802_CALIBRATE_SCHEMA, synchronous=True, ) -async def nau7802_calibrate_to_code(config, action_id, template_arg, args): +async def nau7802_calibrate_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/ntc/sensor.py b/esphome/components/ntc/sensor.py index dd7d1bd35d..6c2cb69990 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -1,4 +1,5 @@ from math import log +from typing import Any import esphome.codegen as cg from esphome.components import sensor @@ -15,6 +16,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) +from esphome.types import ConfigType ntc_ns = cg.esphome_ns.namespace("ntc") NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor) @@ -25,7 +27,7 @@ CONF_C = "c" ZERO_POINT = 273.15 -def validate_calibration_parameter(value): +def validate_calibration_parameter(value: Any) -> ConfigType: if isinstance(value, dict): return cv.Schema( { @@ -48,7 +50,7 @@ def validate_calibration_parameter(value): ) -def calc_steinhart_hart(value): +def calc_steinhart_hart(value: list[ConfigType]) -> tuple[float, float, float]: r1 = value[0][CONF_VALUE] r2 = value[1][CONF_VALUE] r3 = value[2][CONF_VALUE] @@ -73,7 +75,7 @@ def calc_steinhart_hart(value): return a, b, c -def calc_b(value): +def calc_b(value: ConfigType) -> tuple[float, float, float]: beta = value[CONF_B_CONSTANT] t0 = value[CONF_REFERENCE_TEMPERATURE] + ZERO_POINT r0 = value[CONF_REFERENCE_RESISTANCE] @@ -85,7 +87,7 @@ def calc_b(value): return a, b, c -def process_calibration(value): +def process_calibration(value: Any) -> ConfigType: if isinstance(value, dict): value = cv.Schema( { @@ -132,7 +134,7 @@ CONFIG_SCHEMA = ( ) -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/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 22bce4cc41..fe4f727cd6 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -216,7 +216,7 @@ void OnlineImage::loop() { } void OnlineImage::end_connection_() { - // Abort any in-progress decode to free decoder resources. + // Abort any in-progress decode; the decoder object is kept warm for the next decode. // Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release(). if (this->is_decoding()) { RuntimeImage::release(); diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 4018ad81e7..ab69f5d9ae 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.components.esp32 import ( @@ -31,10 +33,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 @@ -76,7 +80,7 @@ CONF_DEVICE_TYPES = [ ] -def _validate_txpower(value): +def _validate_txpower(value: Any) -> int | float: if CORE.is_esp32: variant = get_esp32_variant() @@ -90,7 +94,7 @@ def _validate_txpower(value): return value # Unsupported, fail later with clear error -def set_sdkconfig_options(config): +def set_sdkconfig_options(config: ConfigType) -> None: # and expose options for using SPI/UART RCPs add_idf_sdkconfig_option("CONFIG_IEEE802154_ENABLED", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_RADIO_NATIVE", True) @@ -180,7 +184,7 @@ def _validate(config: ConfigType) -> ConfigType: return config -def _require_vfs_select(config): +def _require_vfs_select(config: ConfigType) -> ConfigType: """Register VFS select requirement during config validation.""" # OpenThread uses esp_vfs_eventfd which requires VFS select support (ESP32 only) if CORE.is_esp32: @@ -188,7 +192,7 @@ def _require_vfs_select(config): return config -def _validate_platform(config): +def _validate_platform(config: ConfigType) -> ConfigType: if CORE.using_zephyr: return config return only_on_variant( @@ -203,7 +207,7 @@ def _validate_platform(config): )(config) -def _validate_tlv_hex(value): +def _validate_tlv_hex(value: Any) -> str: s = cv.string_strict(value) if len(s) % 2 != 0: raise cv.Invalid("TLV must have an even number of hex characters") @@ -242,7 +246,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(_): +def _final_validate(_: ConfigType) -> None: full_config = fv.full_config.get() network_config = full_config.get("network", {}) if not network_config.get(CONF_ENABLE_IPV6, False): @@ -274,7 +278,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( @coroutine_with_priority(CoroPriority.COMMUNICATION) -async def to_code(config): +async def to_code(config: ConfigType) -> None: # Re-enable openthread IDF component (excluded by default) if CORE.is_esp32: include_builtin_idf_component("openthread") @@ -339,7 +343,12 @@ POLL_PERIOD_ACTION_SCHEMA = automation.maybe_conf( POLL_PERIOD_ACTION_SCHEMA, synchronous=True, ) -async def openthread_poll_period_action_to_code(config, action_id, template_arg, args): +async def openthread_poll_period_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) template_ = await cg.templatable(config[CONF_POLL_PERIOD], args, cg.uint32) diff --git a/esphome/components/openthread_info/sensor.py b/esphome/components/openthread_info/sensor.py index 4d5b3d54f4..e77b84e17c 100644 --- a/esphome/components/openthread_info/sensor.py +++ b/esphome/components/openthread_info/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( UNIT_DECIBEL_MILLIWATT, UNIT_EMPTY, ) +from esphome.types import ConfigType CONF_PARENT_AVERAGE_RSSI = "parent_average_rssi" CONF_PARENT_LAST_RSSI = "parent_last_rssi" @@ -166,13 +167,13 @@ CONFIG_SCHEMA = cv.Schema( ) -async def setup_conf(config: dict, key: str): +async def setup_conf(config: dict, key: str) -> None: if conf := config.get(key): var = await sensor.new_sensor(conf) await cg.register_component(var, conf) -async def to_code(config): +async def to_code(config: ConfigType) -> None: await setup_conf(config, CONF_PARENT_AVERAGE_RSSI) await setup_conf(config, CONF_PARENT_LAST_RSSI) await setup_conf(config, CONF_PARENT_LINK_QUALITY_IN) diff --git a/esphome/components/openthread_info/text_sensor.py b/esphome/components/openthread_info/text_sensor.py index b672831bf0..da789ae706 100644 --- a/esphome/components/openthread_info/text_sensor.py +++ b/esphome/components/openthread_info/text_sensor.py @@ -8,6 +8,7 @@ from esphome.components.openthread.const import ( ) import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_IP_ADDRESS, ENTITY_CATEGORY_DIAGNOSTIC +from esphome.types import ConfigType CONF_ROLE = "role" CONF_RLOC16 = "rloc16" @@ -86,13 +87,13 @@ CONFIG_SCHEMA = cv.Schema( ) -async def setup_conf(config: dict, key: str): +async def setup_conf(config: dict, key: str) -> None: if conf := config.get(key): var = await text_sensor.new_text_sensor(conf) await cg.register_component(var, conf) -async def to_code(config): +async def to_code(config: ConfigType) -> None: await setup_conf(config, CONF_IP_ADDRESS) await setup_conf(config, CONF_ROLE) await setup_conf(config, CONF_RLOC16) diff --git a/esphome/components/pcf85063/time.py b/esphome/components/pcf85063/time.py index 8e19178cc9..771461905e 100644 --- a/esphome/components/pcf85063/time.py +++ b/esphome/components/pcf85063/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 = ["@brogon"] DEPENDENCIES = ["i2c"] @@ -31,7 +34,12 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ), synchronous=True, ) -async def pcf85063_write_time_to_code(config, action_id, template_arg, args): +async def pcf85063_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 @@ -47,13 +55,18 @@ async def pcf85063_write_time_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def pcf85063_read_time_to_code(config, action_id, template_arg, args): +async def pcf85063_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/pcf8563/time.py b/esphome/components/pcf8563/time.py index 1502158c29..8a0b871be9 100644 --- a/esphome/components/pcf8563/time.py +++ b/esphome/components/pcf8563/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 = ["@KoenBreeman"] @@ -34,7 +37,12 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ), synchronous=True, ) -async def pcf8563_write_time_to_code(config, action_id, template_arg, args): +async def pcf8563_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 @@ -50,13 +58,18 @@ async def pcf8563_write_time_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def pcf8563_read_time_to_code(config, action_id, template_arg, args): +async def pcf8563_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/pcm5122/audio_dac.py b/esphome/components/pcm5122/audio_dac.py index c18fb3993e..5091efabea 100644 --- a/esphome/components/pcm5122/audio_dac.py +++ b/esphome/components/pcm5122/audio_dac.py @@ -13,6 +13,8 @@ from esphome.const import ( CONF_NUMBER, CONF_OUTPUT, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@remcom"] DEPENDENCIES = ["i2c"] @@ -50,7 +52,7 @@ PCM5122_CHANNEL_MIX_ENUM = { _validate_bits = cv.float_with_unit("bits", "bit") -def _validate_volume_range(config): +def _validate_volume_range(config: ConfigType) -> ConfigType: if config[CONF_VOLUME_MIN_DB] >= config[CONF_VOLUME_MAX_DB]: raise cv.Invalid(f"{CONF_VOLUME_MIN_DB} must be less than {CONF_VOLUME_MAX_DB}") return config @@ -90,7 +92,7 @@ CONFIG_SCHEMA = cv.All( ) -def _validate_pin_mode(value): +def _validate_pin_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]: @@ -98,7 +100,7 @@ def _validate_pin_mode(value): return value -def _validate_pin(value): +def _validate_pin(value: ConfigType) -> ConfigType: if value[CONF_MODE][CONF_INPUT] and value[CONF_NUMBER] == 6: raise cv.Invalid("GPIO6 cannot be used as input on the PCM5122") return value @@ -120,7 +122,7 @@ PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register(CONF_PCM5122, PIN_SCHEMA) -async def pcm5122_pin_to_code(config): +async def pcm5122_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_PCM5122]) @@ -130,7 +132,7 @@ async def pcm5122_pin_to_code(config): 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/pcm5122/switch/__init__.py b/esphome/components/pcm5122/switch/__init__.py index 10519da895..829adeccb7 100644 --- a/esphome/components/pcm5122/switch/__init__.py +++ b/esphome/components/pcm5122/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_POWER_MODE, ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType from ..audio_dac import CONF_PCM5122, PCM5122, pcm5122_ns @@ -26,7 +27,7 @@ CONFIG_SCHEMA = switch.switch_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await switch.new_switch(config) await cg.register_parented(var, config[CONF_PCM5122]) cg.add(var.set_power_mode(config[CONF_POWER_MODE])) diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 0a11120bf0..fe784c5ffe 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -1,3 +1,5 @@ +from typing import Any + import esphome.codegen as cg from esphome.components import sensor, uart import esphome.config_validation as cv @@ -32,6 +34,8 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_PERCENT, ) +from esphome.core import TimePeriodMilliseconds +from esphome.types import ConfigType CODEOWNERS = ["@ximex"] DEPENDENCIES = ["uart"] @@ -167,14 +171,14 @@ SENSORS_TO_TYPE = { } -def validate_pmsx003_sensors(value): +def validate_pmsx003_sensors(value: ConfigType) -> ConfigType: for key, types in SENSORS_TO_TYPE.items(): if key in value and value[CONF_TYPE] not in types: raise cv.Invalid(f"{value[CONF_TYPE]} does not have {key} sensor!") return value -def validate_update_interval(value): +def validate_update_interval(value: Any) -> TimePeriodMilliseconds: value = cv.positive_time_period_milliseconds(value) if value == cv.time_period("0s"): return value @@ -295,7 +299,7 @@ CONFIG_SCHEMA = cv.All( ) -def final_validate(config): +def final_validate(config: ConfigType) -> None: require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s") schema = uart.final_validate_device_schema( "pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx @@ -306,7 +310,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 uart.register_uart_device(var, config) diff --git a/esphome/components/pmwcs3/sensor.py b/esphome/components/pmwcs3/sensor.py index c0bc54c5ba..ae22b3e0d6 100644 --- a/esphome/components/pmwcs3/sensor.py +++ b/esphome/components/pmwcs3/sensor.py @@ -10,6 +10,9 @@ from esphome.const import ( ICON_THERMOMETER, STATE_CLASS_MEASUREMENT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@SeByDocKy"] DEPENDENCIES = ["i2c"] @@ -72,7 +75,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) @@ -114,7 +117,12 @@ PMWCS3_CALIBRATION_SCHEMA = cv.Schema( PMWCS3_CALIBRATION_SCHEMA, synchronous=True, ) -async def pmwcs3_calibration_to_code(config, action_id, template_arg, args): +async def pmwcs3_calibration_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: parent = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, parent) @@ -134,7 +142,12 @@ PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( PMWCS3_NEW_I2C_ADDRESS_SCHEMA, synchronous=True, ) -async def pmwcs3newi2caddress_to_code(config, action_id, template_arg, args): +async def pmwcs3newi2caddress_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) address = await cg.templatable(config[CONF_ADDRESS], args, cg.int_) diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 3326745846..7c5a0590d7 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation, pins import esphome.codegen as cg from esphome.components import sensor @@ -19,7 +21,9 @@ from esphome.const import ( UNIT_PULSES, UNIT_PULSES_PER_MINUTE, ) -from esphome.core import CORE +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CONF_USE_PCNT = "use_pcnt" @@ -42,7 +46,7 @@ SetTotalPulsesAction = pulse_counter_ns.class_( ) -def validate_internal_filter(value): +def validate_internal_filter(value: ConfigType) -> ConfigType: use_pcnt = value.get(CONF_USE_PCNT) if CORE.is_esp8266 and use_pcnt: raise cv.Invalid( @@ -63,7 +67,7 @@ def validate_internal_filter(value): return value -def validate_pulse_counter_pin(value): +def validate_pulse_counter_pin(value: Any) -> ConfigType: value = pins.internal_gpio_input_pin_schema(value) if CORE.is_esp8266 and value[CONF_NUMBER] >= 16: raise cv.Invalid( @@ -72,7 +76,7 @@ def validate_pulse_counter_pin(value): return value -def validate_count_mode(value): +def validate_count_mode(value: ConfigType) -> ConfigType: rising_edge = value[CONF_RISING_EDGE] falling_edge = value[CONF_FALLING_EDGE] if rising_edge == "DISABLE" and falling_edge == "DISABLE": @@ -126,7 +130,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: use_pcnt = config.get(CONF_USE_PCNT) if CORE.is_esp32 and use_pcnt: include_builtin_idf_component("esp_driver_pcnt") @@ -157,7 +161,12 @@ async def to_code(config): ), synchronous=True, ) -async def set_total_action_to_code(config, action_id, template_arg, args): +async def set_total_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) template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint32) diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index ab3dd2a249..9bda891efc 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation, pins import esphome.codegen as cg from esphome.components import sensor @@ -17,7 +19,9 @@ from esphome.const import ( UNIT_PULSES, UNIT_PULSES_PER_MINUTE, ) -from esphome.core import CORE +from esphome.core import CORE, ID, TimePeriodMicroseconds +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@stevebaxter", "@cstaahl", "@TrentHouliston"] @@ -37,18 +41,18 @@ FILTER_MODES = { SetTotalPulsesAction = pulse_meter_ns.class_("SetTotalPulsesAction", automation.Action) -def validate_internal_filter(value): +def validate_internal_filter(value: Any) -> TimePeriodMicroseconds: return cv.positive_time_period_microseconds(value) -def validate_timeout(value): +def validate_timeout(value: Any) -> TimePeriodMicroseconds: value = cv.positive_time_period_microseconds(value) if value.total_minutes > 70: raise cv.Invalid("Maximum timeout is 70 minutes") return value -def validate_pulse_meter_pin(value): +def validate_pulse_meter_pin(value: Any) -> ConfigType: value = pins.internal_gpio_input_pin_schema(value) if CORE.is_esp8266 and value[CONF_NUMBER] >= 16: raise cv.Invalid( @@ -81,7 +85,7 @@ CONFIG_SCHEMA = sensor.sensor_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) @@ -107,7 +111,12 @@ async def to_code(config): ), synchronous=True, ) -async def set_total_action_to_code(config, action_id, template_arg, args): +async def set_total_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) template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint32) diff --git a/esphome/components/pylontech/__init__.py b/esphome/components/pylontech/__init__.py index 82b98654a2..4ab606d9f9 100644 --- a/esphome/components/pylontech/__init__.py +++ b/esphome/components/pylontech/__init__.py @@ -4,6 +4,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 _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,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) diff --git a/esphome/components/pylontech/sensor/__init__.py b/esphome/components/pylontech/sensor/__init__.py index 450f663274..40391206fb 100644 --- a/esphome/components/pylontech/sensor/__init__.py +++ b/esphome/components/pylontech/sensor/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_VOLT, ) +from esphome.types import ConfigType from .. import CONF_BATTERY, CONF_PYLONTECH_ID, PYLONTECH_COMPONENT_SCHEMA, pylontech_ns @@ -90,7 +91,7 @@ CONFIG_SCHEMA = PYLONTECH_COMPONENT_SCHEMA.extend( ).extend({cv.Optional(marker): schema for marker, schema in TYPES.items()}) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_PYLONTECH_ID]) bat = cg.new_Pvariable(config[CONF_ID], config[CONF_BATTERY]) diff --git a/esphome/components/pylontech/text_sensor/__init__.py b/esphome/components/pylontech/text_sensor/__init__.py index f68ca10374..511eb7d542 100644 --- a/esphome/components/pylontech/text_sensor/__init__.py +++ b/esphome/components/pylontech/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 +from esphome.types import ConfigType from .. import CONF_BATTERY, CONF_PYLONTECH_ID, PYLONTECH_COMPONENT_SCHEMA, pylontech_ns @@ -24,7 +25,7 @@ CONFIG_SCHEMA = PYLONTECH_COMPONENT_SCHEMA.extend( ).extend({cv.Optional(marker): text_sensor.text_sensor_schema() for marker in MARKERS}) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_PYLONTECH_ID]) bat = cg.new_Pvariable(config[CONF_ID], config[CONF_BATTERY]) diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 5bb734cb2d..f093262e18 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -26,6 +26,8 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType AUTO_LOAD = ["modbus"] @@ -93,7 +95,12 @@ CONFIG_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: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -105,7 +112,7 @@ def _final_validate(config: ConfigType) -> None: 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 modbus.register_modbus_client_device(var, config) diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index b2c7c3a29d..b9f7246b72 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -20,6 +20,8 @@ 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 AUTO_LOAD = ["modbus"] @@ -75,7 +77,12 @@ CONFIG_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: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -87,7 +94,7 @@ def _final_validate(config: ConfigType) -> None: 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 modbus.register_modbus_client_device(var, config) diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index fe34381ad8..e0186be163 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -1,4 +1,6 @@ +from collections.abc import Callable import logging +from typing import Any from esphome import pins import esphome.codegen as cg @@ -24,6 +26,7 @@ from esphome.const import ( UNIT_DEGREES, UNIT_MICROTESLA, ) +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -60,7 +63,7 @@ QMC5883LOversamplings = { } -def validate_config(config): +def validate_config(config: ConfigType) -> ConfigType: if ( config[CONF_UPDATE_INTERVAL].total_milliseconds < 15 and CONF_DRDY_PIN not in config @@ -72,14 +75,16 @@ def validate_config(config): return config -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): @@ -137,7 +142,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/rd03d/__init__.py b/esphome/components/rd03d/__init__.py index 52e9a2c09a..4fff41e4f6 100644 --- a/esphome/components/rd03d/__init__.py +++ b/esphome/components/rd03d/__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 CODEOWNERS = ["@jasstrong"] DEPENDENCIES = ["uart"] @@ -38,7 +39,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/rd03d/binary_sensor.py b/esphome/components/rd03d/binary_sensor.py index afb7527aa1..2c040d0560 100644 --- a/esphome/components/rd03d/binary_sensor.py +++ b/esphome/components/rd03d/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 CONF_RD03D_ID, RD03DComponent @@ -26,7 +27,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_RD03D_ID]) if target_config := config.get(CONF_TARGET): diff --git a/esphome/components/rd03d/sensor.py b/esphome/components/rd03d/sensor.py index 953d99c2da..d29656bab0 100644 --- a/esphome/components/rd03d/sensor.py +++ b/esphome/components/rd03d/sensor.py @@ -15,6 +15,7 @@ from esphome.const import ( UNIT_DEGREES, UNIT_MILLIMETER, ) +from esphome.types import ConfigType from . import CONF_RD03D_ID, RD03DComponent @@ -75,7 +76,7 @@ CONFIG_SCHEMA = cv.Schema( ).extend({cv.Optional(f"target_{i + 1}"): TARGET_SCHEMA for i in range(MAX_TARGETS)}) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_RD03D_ID]) if target_count_config := config.get(CONF_TARGET_COUNT): diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index ad9c4b5a18..6e8c73d331 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, remote_base @@ -21,6 +23,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE, TimePeriod +from esphome.types import ConfigType CONF_FILTER_SYMBOLS = "filter_symbols" CONF_RECEIVE_SYMBOLS = "receive_symbols" @@ -62,7 +65,7 @@ RemoteReceiverComponent = remote_receiver_ns.class_( ) -def validate_config(config): +def validate_config(config: ConfigType) -> ConfigType: if CORE.is_esp32: variant = esp32.get_esp32_variant() if variant in esp32_rmt.VARIANTS_NO_RMT: @@ -78,7 +81,7 @@ def validate_config(config): return config -def validate_tolerance(value): +def validate_tolerance(value: Any) -> ConfigType: if isinstance(value, dict): return TOLERANCE_SCHEMA(value) @@ -196,7 +199,7 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) diff --git a/esphome/components/remote_receiver/binary_sensor.py b/esphome/components/remote_receiver/binary_sensor.py index fe3e2af950..d4009f396b 100644 --- a/esphome/components/remote_receiver/binary_sensor.py +++ b/esphome/components/remote_receiver/binary_sensor.py @@ -1,4 +1,5 @@ from esphome.components import binary_sensor, remote_base +from esphome.types import ConfigType from . import FILTER_SOURCE_FILES # noqa: F401 pylint: disable=unused-import @@ -7,6 +8,6 @@ DEPENDENCIES = ["remote_receiver"] CONFIG_SCHEMA = remote_base.validate_binary_sensor -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await remote_base.build_binary_sensor(config) await binary_sensor.register_binary_sensor(var, config) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 521c3daf87..9d8761ea90 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -18,7 +18,9 @@ from esphome.const import ( CONF_VALUE, PlatformFramework, ) -from esphome.core import CORE +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -94,7 +96,7 @@ CONFIG_SCHEMA = ( ) -def _validate_non_blocking(config): +def _validate_non_blocking(config: ConfigType) -> None: if ( CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT @@ -125,7 +127,12 @@ DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value( DIGITAL_WRITE_ACTION_SCHEMA, synchronous=True, ) -async def digital_write_action_to_code(config, action_id, template_arg, args): +async def digital_write_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_TRANSMITTER_ID]) template_ = await cg.templatable(config[CONF_VALUE], args, cg.bool_) @@ -133,7 +140,7 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) @@ -178,12 +185,14 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, + "remote_transmitter_rtl87xx.cpp": { + PlatformFramework.RTL87XX_ARDUINO, + }, "remote_transmitter.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, - PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, PlatformFramework.RP2_ARDUINO, }, diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 49c711330b..67341e936f 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,7 +2,8 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) +#if (defined(USE_LIBRETINY) && !defined(USE_RTL87XX)) || defined(USE_ESP8266) || defined(USE_RP2) || \ + (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) namespace esphome::remote_transmitter { @@ -81,25 +82,37 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen ESP_LOGD(TAG, "Sending remote code"); uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); - this->target_time_ = 0; this->transmit_trigger_.trigger(); for (uint32_t i = 0; i < send_times; i++) { - InterruptLock lock; - for (int32_t item : this->temp_.get_data()) { - if (item > 0) { - const auto length = uint32_t(item); - this->mark_(on_time, off_time, length); - } else { - const auto length = uint32_t(-item); - this->space_(length); + { + InterruptLock lock; + // Re-anchor every iteration: timing must never span a lock boundary, as micros() can + // jump when interrupts are re-enabled between repeats (e.g. LibreTiny's Beken micros() + // discards its interrupt-lock correction, stretching the repeat gap by the lock duration) + this->target_time_ = 0; + for (int32_t item : this->temp_.get_data()) { + if (item > 0) { + const auto length = uint32_t(item); + this->mark_(on_time, off_time, length); + } else { + const auto length = uint32_t(-item); + this->space_(length); + } + App.feed_wdt(); } - App.feed_wdt(); + this->await_target_time_(); // wait for duration of last pulse + this->pin_->digital_write(false); } - this->await_target_time_(); // wait for duration of last pulse - this->pin_->digital_write(false); - if (i + 1 < send_times) - this->target_time_ += send_wait; + if (i + 1 < send_times) { + // Wait out the repeat gap with interrupts enabled: wait_time is unbounded user config + // (previously this spin ran inside the next iteration's lock, disabling interrupts for + // the whole gap). Anchoring after the lock release keeps it exact on all platforms. + const uint32_t gap_end = micros() + send_wait; + while ((int32_t) (gap_end - micros()) > 0) { + App.feed_wdt(); + } + } } this->complete_trigger_.trigger(); } diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index e2d33d13cc..94bcb74b09 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -65,14 +65,21 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; #if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) + void await_target_time_(); + uint32_t target_time_{0}; +#endif +#if defined(USE_ESP8266) || (defined(USE_LIBRETINY) && !defined(USE_RTL87XX)) || defined(USE_RP2) || \ + (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); void space_(uint32_t usec); - - void await_target_time_(); - uint32_t target_time_; +#endif +#ifdef USE_RTL87XX + // Carrier frequency the PWM is currently configured for; 0 = not yet configured + uint32_t current_carrier_frequency_{0}; + void *pwm_{nullptr}; // pwmout_t*, opaque here to keep the SDK header out of this shared header #endif #if defined(USE_ESP32) && SOC_RMT_SUPPORTED diff --git a/esphome/components/remote_transmitter/remote_transmitter_rtl87xx.cpp b/esphome/components/remote_transmitter/remote_transmitter_rtl87xx.cpp new file mode 100644 index 0000000000..b7078b9d69 --- /dev/null +++ b/esphome/components/remote_transmitter/remote_transmitter_rtl87xx.cpp @@ -0,0 +1,137 @@ +#include "remote_transmitter.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +// clang-tidy cannot parse the Realtek SDK headers pulled in via ArduinoPrivate.h +#if defined(USE_RTL87XX) && !defined(CLANG_TIDY) + +// ArduinoPrivate.h = Arduino.h + the SDK's mbed HAL (pwmout etc.) with the core's fixes for +// type-name collisions between the two (e.g. PinMode) +#include +#include +#include + +namespace esphome::remote_transmitter { + +static const char *const TAG = "remote_transmitter"; + +// The carrier is generated by the PWM peripheral instead of bit-banging the pin: software carrier +// generation requires disabling interrupts for the whole frame, but this core's micros() is derived +// from the FreeRTOS tick and freezes while interrupts are off, so the timing loop never advances and +// the watchdog resets the chip. With hardware PWM, software only times the mark/space envelope and +// interrupts can stay enabled. +// +// The PWM is driven through the SDK's pwmout HAL directly rather than the Arduino wiring layer: +// changing the carrier frequency via the wiring requires a GPIO/PWM pin mode round-trip, which +// use-after-frees the core's per-pin state (pinRemoveMode() frees without nulling) and corrupts the +// heap. pwmout_period_us() changes the frequency with no mode transitions. + +void RemoteTransmitterComponent::setup() { + // Deliberately no pin_->setup(): registering the pin as GPIO claims it in the SDK's pin + // management, and the pad is then never handed over to the PWM peripheral -- pwmout_init() + // must own the pin from the start. + PinInfo *info = pinInfo(this->pin_->get_pin()); + if (info == nullptr || !pinSupported(info, PIN_PWM)) { + // checked here because the AmebaZ (RTL8710B) SDK does not report PWM init failure + ESP_LOGE(TAG, "Pin %u is not PWM-capable", this->pin_->get_pin()); + this->mark_failed(); + return; + } + auto *pwm = new pwmout_t(); + this->pwm_ = pwm; + pwmout_init(pwm, static_cast(info->gpio)); +#if LT_RTL8720C + // only the AmebaZ2 SDK's pwmout_s reports init success + if (!pwm->is_init) { + ESP_LOGE(TAG, "PWM init failed on pin %u", this->pin_->get_pin()); + delete pwm; + this->pwm_ = nullptr; + this->mark_failed(); + return; + } +#endif + pwmout_period_us(pwm, 26); // placeholder; the real carrier period is set per transmission + pwmout_write(pwm, this->pin_->is_inverted() ? 1.0f : 0.0f); +} + +void RemoteTransmitterComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "Remote Transmitter:\n" + " Carrier Duty: %u%%", + this->carrier_duty_percent_); + LOG_PIN(" Pin: ", this->pin_); +} + +void RemoteTransmitterComponent::await_target_time_() { + const uint32_t current_time = micros(); + if (this->target_time_ == 0) { + this->target_time_ = current_time; + } else { + while ((int32_t) (this->target_time_ - micros()) > 0) { + } + } +} + +void RemoteTransmitterComponent::digital_write(bool value) { + if (this->pwm_ == nullptr) + return; + pwmout_write(static_cast(this->pwm_), (value != this->pin_->is_inverted()) ? 1.0f : 0.0f); +} + +void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { + auto *pwm = static_cast(this->pwm_); + if (pwm == nullptr) { + ESP_LOGW(TAG, "Cannot send: PWM not initialized"); + return; + } + ESP_LOGD(TAG, "Sending remote code"); + const uint32_t carrier_frequency = this->temp_.get_carrier_frequency(); + // unmodulated protocols (no carrier or 100% duty) drive the pin constantly during marks + float mark_duty = + (carrier_frequency > 0 && this->carrier_duty_percent_ < 100) ? this->carrier_duty_percent_ / 100.0f : 1.0f; + float space_duty = 0.0f; + if (this->pin_->is_inverted()) { + mark_duty = 1.0f - mark_duty; + space_duty = 1.0f; + } + if (carrier_frequency > 0 && carrier_frequency != this->current_carrier_frequency_) { + // round(1000000/freq), clamped like the bit-bang path so a bad lambda can't hand the SDK a zero period + const uint32_t period = std::max(uint32_t(1), (1000000UL + carrier_frequency / 2) / carrier_frequency); + pwmout_period_us(pwm, period); + this->current_carrier_frequency_ = carrier_frequency; + } + this->transmit_trigger_.trigger(); + const UBaseType_t saved_priority = uxTaskPriorityGet(nullptr); + for (uint32_t i = 0; i < send_times; i++) { + // Boost task priority for the frame only, so WiFi/lwIP tasks can't preempt mid-frame and + // merge adjacent marks. Interrupts stay enabled: micros() needs the FreeRTOS tick, and + // ISR latency is within receiver tolerance. + vTaskPrioritySet(nullptr, configMAX_PRIORITIES - 1); + // Re-anchor every iteration: a late exit from the normal-priority gap wait must not + // leave the schedule behind micros(), which would compress the next frame's leading items + this->target_time_ = 0; + for (int32_t item : this->temp_.get_data()) { + const bool is_mark = item > 0; + this->await_target_time_(); + pwmout_write(pwm, is_mark ? mark_duty : space_duty); + this->target_time_ += is_mark ? uint32_t(item) : uint32_t(-item); + App.feed_wdt(); + } + this->await_target_time_(); // wait for duration of last pulse + pwmout_write(pwm, space_duty); + vTaskPrioritySet(nullptr, saved_priority); + if (i + 1 < send_times) { + // The repeat gap is user-configurable and unbounded, so wait it out at normal + // priority, feeding the watchdog + const uint32_t gap_end = micros() + send_wait; + while ((int32_t) (gap_end - micros()) > 0) { + App.feed_wdt(); + } + } + } + this->complete_trigger_.trigger(); +} + +} // namespace esphome::remote_transmitter + +#endif // USE_RTL87XX && !CLANG_TIDY diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 0e5a03523d..72722ec4b1 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -15,6 +15,9 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_STEPS, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType rotary_encoder_ns = cg.esphome_ns.namespace("rotary_encoder") @@ -44,7 +47,7 @@ RotaryEncoderSetValueAction = rotary_encoder_ns.class_( ) -def validate_min_max_value(config): +def validate_min_max_value(config: ConfigType) -> ConfigType: if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: min_val = config[CONF_MIN_VALUE] max_val = config[CONF_MAX_VALUE] @@ -92,7 +95,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) @@ -126,7 +129,12 @@ async def to_code(config): ), synchronous=True, ) -async def sensor_template_publish_to_code(config, action_id, template_arg, args): +async def sensor_template_publish_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_VALUE], args, cg.int_) diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py index 9f7479edd0..5b7259f9e5 100644 --- a/esphome/components/rp2040_pio_led_strip/light.py +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -18,7 +18,7 @@ from esphome.types import ConfigType from esphome.util import _LOGGER -def get_nops(timing): +def get_nops(timing: float) -> list[float | str]: """ Calculate the number of NOP instructions required to wait for a given amount of time. """ @@ -39,7 +39,7 @@ def get_nops(timing): return nops -def generate_assembly_code(id, t0h, t0l, t1h, t1l): +def generate_assembly_code(id: str, t0h: int, t0l: int, t1h: int, t1l: int) -> str: """ Generate assembly code with the given timing values. """ @@ -125,7 +125,7 @@ writezero: return assembly_template + const_csdk_code -def time_to_cycles(time_us): +def time_to_cycles(time_us: float) -> int: cycles_per_us = 57.5 return round(float(time_us) * cycles_per_us) @@ -172,7 +172,7 @@ CONF_BIT1_HIGH = "bit1_high" CONF_BIT1_LOW = "bit1_low" -def _validate_timing(value): +def _validate_timing(value: str) -> float: # if doesn't end with us, raise error if not value.endswith("us"): raise cv.Invalid("Timing must be in microseconds (us)") diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 9fa32a5a65..3c130a7d75 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -77,6 +77,11 @@ class JPEGFormat(Format): def actions(self) -> None: cg.add_define("USE_RUNTIME_IMAGE_JPEG") cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4") + if CORE.is_host: + # JPEGDEC's host detection checks __MACH__/__LINUX__, but gcc only + # predefines the lowercase __linux__; without this a Linux host + # build tries to include Arduino.h. + cg.add_build_flag("-D__LINUX__") if CORE.is_esp32: from esphome.components.esp32 import add_idf_component diff --git a/esphome/components/runtime_image/bmp_decoder.cpp b/esphome/components/runtime_image/bmp_decoder.cpp index 6a1bd61d86..5d45621fb7 100644 --- a/esphome/components/runtime_image/bmp_decoder.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -12,6 +12,22 @@ namespace esphome::runtime_image { static const char *const TAG = "image_decoder.bmp"; +void BmpDecoder::reset() { + ImageDecoder::reset(); + this->bits_per_pixel_ = 0; + this->compression_method_ = 0; + this->image_data_size_ = 0; + this->width_ = 0; + this->height_ = 0; + this->current_index_ = 0; + this->paint_index_ = 0; + // color_table_ is deliberately kept allocated so the next decode can reuse it + this->color_table_entries_ = 0; + this->data_offset_ = 0; + this->padding_bytes_ = 0; + this->width_bytes_ = 0; +} + int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { size_t index = 0; if (this->current_index_ == 0) { @@ -85,7 +101,10 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { size_t header_size = encode_uint32(buffer[17], buffer[16], buffer[15], buffer[14]); size_t offset = 14 + header_size; - this->color_table_ = std::make_unique(this->color_table_entries_); + if (this->color_table_entries_ > this->color_table_capacity_) { + this->color_table_ = std::make_unique(this->color_table_entries_); + this->color_table_capacity_ = this->color_table_entries_; + } for (size_t i = 0; i < this->color_table_entries_; i++) { this->color_table_[i] = encode_uint32(buffer[offset + i * 4 + 3], buffer[offset + i * 4 + 2], diff --git a/esphome/components/runtime_image/bmp_decoder.h b/esphome/components/runtime_image/bmp_decoder.h index a52a561584..01acc41f91 100644 --- a/esphome/components/runtime_image/bmp_decoder.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -21,8 +21,9 @@ class BmpDecoder : public ImageDecoder { * * @param image The RuntimeImage to decode the stream into. */ - BmpDecoder(RuntimeImage *image) : ImageDecoder(image) {} + BmpDecoder(RuntimeImage *image) : ImageDecoder(image, BMP) {} + void reset() override; int HOT decode(uint8_t *buffer, size_t size) override; bool is_finished() const override { @@ -35,17 +36,18 @@ class BmpDecoder : public ImageDecoder { } protected: + std::unique_ptr color_table_; size_t current_index_{0}; size_t paint_index_{0}; ssize_t width_{0}; ssize_t height_{0}; - uint16_t bits_per_pixel_{0}; + size_t width_bytes_{0}; + size_t data_offset_{0}; uint32_t compression_method_{0}; uint32_t image_data_size_{0}; uint32_t color_table_entries_{0}; - std::unique_ptr color_table_; - size_t width_bytes_{0}; - size_t data_offset_{0}; + uint32_t color_table_capacity_{0}; // Allocated entries in color_table_, kept across decodes + uint16_t bits_per_pixel_{0}; uint8_t padding_bytes_{0}; }; diff --git a/esphome/components/runtime_image/image_decoder.h b/esphome/components/runtime_image/image_decoder.h index 6d351a10aa..2a8b393888 100644 --- a/esphome/components/runtime_image/image_decoder.h +++ b/esphome/components/runtime_image/image_decoder.h @@ -1,5 +1,6 @@ #pragma once #include "esphome/core/color.h" +#include "image_format.h" namespace esphome::runtime_image { @@ -36,18 +37,41 @@ class ImageDecoder { * @brief Construct a new Image Decoder object * * @param image The RuntimeImage to decode the stream into. + * @param format The image format this decoder handles. */ - ImageDecoder(RuntimeImage *image) : image_(image) {} + ImageDecoder(RuntimeImage *image, ImageFormat format) : image_(image), format_(format) {} virtual ~ImageDecoder() = default; + /// @brief Get the image format handled by this decoder. + ImageFormat get_format() const { return this->format_; } + + /// @brief Check if a decoding session is in progress (prepare() called, reset() not yet). + bool is_active() const { return this->active_; } + /** - * @brief Initialize the decoder. + * @brief Reset the decoder state, ending any decoding session. + * Subclasses should override this method to reset any format-specific state. + * Buffers the next decode can reuse should be kept allocated to avoid heap churn. + */ + virtual void reset() { + this->active_ = false; + this->expected_size_ = 0; + this->decoded_bytes_ = 0; + this->size_valid_ = true; + this->x_scale_ = 1.0; + this->y_scale_ = 1.0; + } + + /** + * @brief Initialize the decoder, starting a new decoding session. * * @param expected_size Hint about the expected data size (0 if unknown). * @return int Returns 0 on success, a {@see DecodeError} value in case of an error. */ virtual int prepare(size_t expected_size) { + this->reset(); this->expected_size_ = expected_size; + this->active_ = true; return 0; } @@ -103,11 +127,13 @@ class ImageDecoder { } protected: + double x_scale_ = 1.0; + double y_scale_ = 1.0; RuntimeImage *image_; size_t expected_size_ = 0; // Expected data size (0 if unknown) size_t decoded_bytes_ = 0; // Bytes processed so far - double x_scale_ = 1.0; - double y_scale_ = 1.0; + const ImageFormat format_; + bool active_ = false; // A decoding session is in progress bool size_valid_ = true; // Last set_size() result; draw() no-ops while false }; diff --git a/esphome/components/runtime_image/image_format.h b/esphome/components/runtime_image/image_format.h new file mode 100644 index 0000000000..524e52d7bc --- /dev/null +++ b/esphome/components/runtime_image/image_format.h @@ -0,0 +1,19 @@ +#pragma once + +namespace esphome::runtime_image { + +/** + * @brief Image format types that can be decoded dynamically. + */ +enum ImageFormat { + /** Automatically detect from data. Not implemented yet. */ + AUTO, + /** JPEG format. */ + JPEG, + /** PNG format. */ + PNG, + /** BMP format. */ + BMP, +}; + +} // namespace esphome::runtime_image diff --git a/esphome/components/runtime_image/jpeg_decoder.cpp b/esphome/components/runtime_image/jpeg_decoder.cpp index c46e86fd0d..85ec945259 100644 --- a/esphome/components/runtime_image/jpeg_decoder.cpp +++ b/esphome/components/runtime_image/jpeg_decoder.cpp @@ -52,12 +52,6 @@ static int draw_callback(JPEGDRAW *jpeg) { return 1; } -int JpegDecoder::prepare(size_t expected_size) { - ImageDecoder::prepare(expected_size); - // JPEG decoder needs complete data before decoding - return 0; -} - int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { // JPEG decoder requires complete data // If we know the expected size, wait for it diff --git a/esphome/components/runtime_image/jpeg_decoder.h b/esphome/components/runtime_image/jpeg_decoder.h index ed2401e263..67c9b77f4d 100644 --- a/esphome/components/runtime_image/jpeg_decoder.h +++ b/esphome/components/runtime_image/jpeg_decoder.h @@ -18,10 +18,9 @@ class JpegDecoder : public ImageDecoder { * * @param image The RuntimeImage to decode the stream into. */ - JpegDecoder(RuntimeImage *image) : ImageDecoder(image) {} + JpegDecoder(RuntimeImage *image) : ImageDecoder(image, JPEG) {} ~JpegDecoder() override {} - int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: diff --git a/esphome/components/runtime_image/png_decoder.cpp b/esphome/components/runtime_image/png_decoder.cpp index 9501702711..106f25bbe1 100644 --- a/esphome/components/runtime_image/png_decoder.cpp +++ b/esphome/components/runtime_image/png_decoder.cpp @@ -48,7 +48,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui } } -PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { +PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image, PNG) { { RAMAllocator allocator; pngle_t *pngle = allocator.allocate(1, PNGLE_T_SIZE); @@ -57,8 +57,8 @@ PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { return; } memset(pngle, 0, PNGLE_T_SIZE); - pngle_reset(pngle); this->pngle_ = pngle; + pngle_reset(this->pngle_); } } @@ -71,11 +71,12 @@ PngDecoder::~PngDecoder() { } int PngDecoder::prepare(size_t expected_size) { - ImageDecoder::prepare(expected_size); + // Check before the base prepare() so a failure never leaves an active session if (!this->pngle_) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); return DECODE_ERROR_OUT_OF_MEMORY; } + ImageDecoder::prepare(expected_size); pngle_set_user_data(this->pngle_, this); pngle_set_init_callback(this->pngle_, init_callback); pngle_set_draw_callback(this->pngle_, draw_callback); diff --git a/esphome/components/runtime_image/png_decoder.h b/esphome/components/runtime_image/png_decoder.h index 24521d33a8..a1cd60e0a6 100644 --- a/esphome/components/runtime_image/png_decoder.h +++ b/esphome/components/runtime_image/png_decoder.h @@ -22,6 +22,14 @@ class PngDecoder : public ImageDecoder { PngDecoder(RuntimeImage *image); ~PngDecoder() override; + void reset() override { + ImageDecoder::reset(); + if (this->pngle_) { + pngle_reset(this->pngle_); + } + this->pixels_decoded_ = 0; + } + int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 8fe9be4c8c..e269f7d8f3 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -172,33 +172,38 @@ void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, } bool RuntimeImage::begin_decode(size_t expected_size) { - if (this->decoder_) { + if (this->is_decoding()) { ESP_LOGW(TAG, "Decoding already in progress"); return false; } - this->decoder_ = this->create_decoder_(); + // An idle decoder for a different format cannot be reused + if (this->decoder_ != nullptr && this->decoder_->get_format() != this->format_) { + ESP_LOGD(TAG, "Decoder format mismatch: current: %d, new: %d", this->decoder_->get_format(), this->format_); + this->decoder_ = nullptr; + } + if (!this->decoder_) { - ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_); - return false; + this->decoder_ = this->create_decoder_(this->format_); + if (!this->decoder_) { + ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_); + return false; + } } - this->total_size_ = expected_size; this->decoded_bytes_ = 0; - // Initialize decoder int result = this->decoder_->prepare(expected_size); if (result < 0) { ESP_LOGE(TAG, "Failed to prepare decoder: %d", result); - this->decoder_ = nullptr; + this->decoder_ = nullptr; // If prepare fails, a full reset is needed return false; } - return true; } int RuntimeImage::feed_data(uint8_t *data, size_t len) { - if (!this->decoder_) { + if (!this->is_decoding()) { ESP_LOGE(TAG, "No decoder initialized"); return -1; } @@ -212,7 +217,7 @@ int RuntimeImage::feed_data(uint8_t *data, size_t len) { } bool RuntimeImage::end_decode() { - if (!this->decoder_) { + if (!this->is_decoding()) { return false; } @@ -224,26 +229,23 @@ bool RuntimeImage::end_decode() { this->data_start_ = this->buffer_; } - // Clean up decoder - this->decoder_ = nullptr; + // End the session; the decoder object stays warm so the next decode can + // reuse it (and its buffers) without churning the heap. + this->decoder_->reset(); ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_); return true; } -bool RuntimeImage::is_decode_finished() const { - if (!this->decoder_) { - return false; - } - return this->decoder_->is_finished(); -} +bool RuntimeImage::is_decode_finished() const { return this->is_decoding() && this->decoder_->is_finished(); } void RuntimeImage::release() { this->release_buffer_(); - // Reset decoder separately — release() can be called from within the decoder - // (via set_size -> resize -> resize_buffer_), so we must not destroy the decoder here. - // The decoder lifecycle is managed by begin_decode()/end_decode(). - this->decoder_ = nullptr; + // End any active decode session; decoders free the format-specific working buffers + // they can (PNG), while the decoder object itself is kept warm for the next decode. + if (this->decoder_) { + this->decoder_->reset(); + } } void RuntimeImage::release_buffer_() { @@ -347,8 +349,9 @@ size_t RuntimeImage::get_buffer_size(int width, int height) const { int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } -std::unique_ptr RuntimeImage::create_decoder_() { - switch (this->format_) { +std::unique_ptr RuntimeImage::create_decoder_(ImageFormat format) { + ESP_LOGV(TAG, "Creating decoder for format %d", format); + switch (format) { #ifdef USE_RUNTIME_IMAGE_BMP case BMP: return make_unique(this); @@ -362,7 +365,7 @@ std::unique_ptr RuntimeImage::create_decoder_() { return make_unique(this); #endif default: - ESP_LOGE(TAG, "Unsupported image format: %d", this->format_); + ESP_LOGE(TAG, "Unsupported image format: %d", format); return nullptr; } } diff --git a/esphome/components/runtime_image/runtime_image.h b/esphome/components/runtime_image/runtime_image.h index 10ce980be2..cfac253fdb 100644 --- a/esphome/components/runtime_image/runtime_image.h +++ b/esphome/components/runtime_image/runtime_image.h @@ -3,25 +3,11 @@ #include "esphome/components/image/image.h" #include "esphome/core/helpers.h" +#include "image_decoder.h" +#include "image_format.h" + namespace esphome::runtime_image { -// Forward declaration -class ImageDecoder; - -/** - * @brief Image format types that can be decoded dynamically. - */ -enum ImageFormat { - /** Automatically detect from data. Not implemented yet. */ - AUTO, - /** JPEG format. */ - JPEG, - /** PNG format. */ - PNG, - /** BMP format. */ - BMP, -}; - /** * @brief A dynamic image that can be loaded and decoded at runtime. * @@ -99,7 +85,7 @@ class RuntimeImage : public image::Image { /** * @brief Check if decoding is currently in progress. */ - bool is_decoding() const { return this->decoder_ != nullptr; } + bool is_decoding() const { return this->decoder_ != nullptr && this->decoder_->is_active(); } /** * @brief Check if the decoder has finished processing all data. @@ -120,9 +106,10 @@ class RuntimeImage : public image::Image { ImageFormat get_format() const { return this->format_; } /** - * @brief Release the image buffer and free memory. + * @brief Release the image buffer and free its memory, ending any decode session. * - * An external buffer is let go of rather than freed. + * An external buffer is let go of rather than freed. The decoder object is kept + * warm so the next decode can reuse it without churning the heap. */ void release(); @@ -194,9 +181,11 @@ class RuntimeImage : public image::Image { int get_position_(int x, int y) const; /** - * @brief Create decoder instance for the image's format. + * @brief Create decoder instance for the requested format. + * @param format The image format to decode. + * @return Unique pointer to the created decoder, or nullptr on failure. */ - std::unique_ptr create_decoder_(); + std::unique_ptr create_decoder_(ImageFormat format); // Memory management uint8_t *buffer_{nullptr}; @@ -224,7 +213,6 @@ class RuntimeImage : public image::Image { int buffer_height_{0}; // Decoding state - size_t total_size_{0}; size_t decoded_bytes_{0}; /** Fixed width requested on configuration, or 0 if not specified. */ diff --git a/esphome/components/rx8130/time.py b/esphome/components/rx8130/time.py index 4f6310358c..40d10e9f6b 100644 --- a/esphome/components/rx8130/time.py +++ b/esphome/components/rx8130/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 = ["@beormund"] DEPENDENCIES = ["i2c"] @@ -29,7 +32,12 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ), synchronous=True, ) -async def rx8130_write_time_to_code(config, action_id, template_arg, args): +async def rx8130_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 rx8130_write_time_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def rx8130_read_time_to_code(config, action_id, template_arg, args): +async def rx8130_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/scd30/sensor.py b/esphome/components/scd30/sensor.py index f60e913a0c..37789100f7 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -22,6 +22,9 @@ from esphome.const import ( UNIT_PARTS_PER_MILLION, UNIT_PERCENT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensirion_common"] @@ -82,7 +85,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) @@ -131,8 +134,11 @@ async def to_code(config): synchronous=True, ) async def scd30_force_recalibration_with_reference_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]) template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16) diff --git a/esphome/components/sendspin/image/sendspin_image.cpp b/esphome/components/sendspin/image/sendspin_image.cpp index 626d7966b7..558a292d5b 100644 --- a/esphome/components/sendspin/image/sendspin_image.cpp +++ b/esphome/components/sendspin/image/sendspin_image.cpp @@ -86,8 +86,8 @@ void SendspinImageSlot::on_decode_(const uint8_t *data, size_t length) { } const bool decoded = this->decode_frame_(data, length, target); - // Drops any half-finished decoder. An external buffer is let go of rather than freed, so this is - // safe on every path. + // Ends any half-finished decode session (the decoder object is kept for reuse). An external + // buffer is let go of rather than freed, so this is safe on every path. this->decode_sink_.release(); if (!decoded) { diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 277648137a..82368a60d0 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -11,6 +11,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"] @@ -62,7 +65,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) @@ -109,6 +112,11 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -async def senseair_action_to_code(config, action_id, template_arg, args): +async def senseair_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/servo/__init__.py b/esphome/components/servo/__init__.py index c2eaefe455..666c7dbcdd 100644 --- a/esphome/components/servo/__init__.py +++ b/esphome/components/servo/__init__.py @@ -13,6 +13,9 @@ from esphome.const import ( CONF_RESTORE, CONF_TRANSITION_LENGTH, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType servo_ns = cg.esphome_ns.namespace("servo") Servo = servo_ns.class_("Servo", cg.Component) @@ -39,7 +42,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) @@ -64,7 +67,12 @@ async def to_code(config): ), synchronous=True, ) -async def servo_write_to_code(config, action_id, template_arg, args): +async def servo_write_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_LEVEL], args, cg.float_) @@ -82,6 +90,11 @@ async def servo_write_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def servo_detach_to_code(config, action_id, template_arg, args): +async def servo_detach_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/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index dd99fcbc90..c166076e0f 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -1,6 +1,7 @@ import hashlib from pathlib import Path import re +from typing import Any from esphome import external_files, pins import esphome.codegen as cg @@ -66,7 +67,7 @@ KNOWN_FIRMWARE = { } -def parse_firmware_version(value): +def parse_firmware_version(value: str) -> tuple[int, int]: match = re.fullmatch(r"(\d+)\.(\d+)", value) if match is None: raise ValueError(f"Not a valid version number {value}") @@ -154,7 +155,7 @@ def _extract_firmware_ref(entry: ConfigType) -> RemoteFile | None: PREFETCH_FILES = external_files.single_stage_prefetch(_extract_firmware_ref) -def validate_firmware(value): +def validate_firmware(value: ConfigType) -> ConfigType: config = value.copy() if CONF_URL not in config: try: @@ -167,14 +168,14 @@ def validate_firmware(value): return config -def validate_sha256(value): +def validate_sha256(value: Any) -> str: value = cv.string(value) if not re.fullmatch(r"[0-9a-fA-F]{64}", value): raise ValueError(f"Not a valid SHA256 hex string: {value}") return value -def validate_version(value): +def validate_version(value: str) -> str: parse_firmware_version(value) return value @@ -231,7 +232,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: fw_hex = get_firmware(config[CONF_FIRMWARE]) fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION]) diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py index d25e883fa1..07ca5bf444 100644 --- a/esphome/components/sml/__init__.py +++ b/esphome/components/sml/__init__.py @@ -1,10 +1,12 @@ import re +from typing import Any from esphome import automation 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 +from esphome.types import ConfigType CODEOWNERS = ["@alengwenus"] @@ -46,14 +48,14 @@ _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) await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) -def obis_code(value): +def obis_code(value: Any) -> str: value = cv.string(value) match = re.match(r"^\d{1,3}-\d{1,3}:\d{1,3}\.\d{1,3}\.\d{1,3}$", value) if match is None: diff --git a/esphome/components/sml/sensor/__init__.py b/esphome/components/sml/sensor/__init__.py index e6d7180f17..64ac9773c6 100644 --- a/esphome/components/sml/sensor/__init__.py +++ b/esphome/components/sml/sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns @@ -24,7 +25,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable( config[CONF_ID], config[CONF_SERVER_ID], config[CONF_OBIS_CODE] ) diff --git a/esphome/components/sml/text_sensor/__init__.py b/esphome/components/sml/text_sensor/__init__.py index 5a5ab658c4..feff4ef256 100644 --- a/esphome/components/sml/text_sensor/__init__.py +++ b/esphome/components/sml/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_FORMAT +from esphome.types import ConfigType from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns @@ -33,7 +34,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await text_sensor.new_text_sensor( config, config[CONF_SERVER_ID], diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index 26e5c03802..367b65176b 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -12,6 +12,8 @@ from esphome.const import ( CONF_OUTPUT, CONF_TYPE, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType MULTI_CONF = True @@ -65,7 +67,7 @@ CONFIG_SCHEMA = cv.typed_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) if config[CONF_TYPE] == TYPE_GPIO: @@ -84,7 +86,7 @@ async def to_code(config): cg.add(var.set_sr_count(config[CONF_SR_COUNT])) -def _validate_output_mode(value): +def _validate_output_mode(value: ConfigType) -> ConfigType: if value.get(CONF_OUTPUT) is not True: raise cv.Invalid("Only output mode is supported") return value @@ -103,7 +105,9 @@ SN74HC595_PIN_SCHEMA = pins.gpio_base_schema( ) -def sn74hc595_pin_final_validate(pin_config, parent_config): +def sn74hc595_pin_final_validate( + pin_config: ConfigType, parent_config: ConfigType +) -> None: max_pins = parent_config[CONF_SR_COUNT] * 8 if pin_config[CONF_NUMBER] >= max_pins: raise cv.Invalid(f"Pin number must be less than {max_pins}") @@ -112,7 +116,7 @@ def sn74hc595_pin_final_validate(pin_config, parent_config): @pins.PIN_SCHEMA_REGISTRY.register( CONF_SN74HC595, SN74HC595_PIN_SCHEMA, sn74hc595_pin_final_validate ) -async def sn74hc595_pin_to_code(config): +async def sn74hc595_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_SN74HC595]) diff --git a/esphome/components/spa06_base/__init__.py b/esphome/components/spa06_base/__init__.py index 97d09aad81..c995c2c087 100644 --- a/esphome/components/spa06_base/__init__.py +++ b/esphome/components/spa06_base/__init__.py @@ -15,6 +15,8 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PASCAL, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@danielkent-net"] @@ -55,7 +57,7 @@ OVERSAMPLING_OPTIONS = { SPA06Component = spa06_ns.class_("SPA06Component", cg.PollingComponent) -def spa_oversample_time(oversample): +def spa_oversample_time(oversample: str) -> float: # Pressure oversampling conversion times are listed on datasheet Pg. 26 # Datasheet does not have a table for temperature oversampling; # assumption is that it is the same as pressure @@ -72,7 +74,7 @@ def spa_oversample_time(oversample): return OVERSAMPLING_CONVERSION_TIMES[oversample] -def spa_sample_rate(rate): +def spa_sample_rate(rate: str) -> float: SAMPLE_RATE_OPTIONS_HZ = { "1": 1.0, "2": 2.0, @@ -94,7 +96,7 @@ def spa_sample_rate(rate): return SAMPLE_RATE_OPTIONS_HZ[rate] -def compute_measurement_conversion_time(config): +def compute_measurement_conversion_time(config: ConfigType) -> int: # - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet # - returns a rounded up time in ms @@ -115,7 +117,7 @@ def compute_measurement_conversion_time(config): return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time)) -def measurement_timing_check(config): +def measurement_timing_check(config: ConfigType) -> ConfigType: temp_time = 0.0 if temperature_config := config.get(CONF_TEMPERATURE): @@ -176,7 +178,7 @@ CONFIG_SCHEMA_BASE = cv.Schema( CONFIG_SCHEMA_BASE.add_extra(measurement_timing_check) -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) if temperature_config := config.get(CONF_TEMPERATURE): diff --git a/esphome/components/sun_gtil2/__init__.py b/esphome/components/sun_gtil2/__init__.py index c7082794db..0f5ae27753 100644 --- a/esphome/components/sun_gtil2/__init__.py +++ b/esphome/components/sun_gtil2/__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 = ["@Mat931"] MULTI_CONF = True @@ -24,7 +25,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/sun_gtil2/sensor.py b/esphome/components/sun_gtil2/sensor.py index 55c8195391..26435cfa67 100644 --- a/esphome/components/sun_gtil2/sensor.py +++ b/esphome/components/sun_gtil2/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, ) +from esphome.types import ConfigType from . import CONF_SUN_GTIL2_ID, SunGTIL2Component @@ -73,7 +74,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_SUN_GTIL2_ID]) if ac_voltage_config := config.get(CONF_AC_VOLTAGE): sens = await sensor.new_sensor(ac_voltage_config) diff --git a/esphome/components/sun_gtil2/text_sensor.py b/esphome/components/sun_gtil2/text_sensor.py index f74f89b3b4..eae69fb4df 100644 --- a/esphome/components/sun_gtil2/text_sensor.py +++ b/esphome/components/sun_gtil2/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_STATE +from esphome.types import ConfigType from . import CONF_SUN_GTIL2_ID, SunGTIL2Component @@ -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_SUN_GTIL2_ID]) if state_config := config.get(CONF_STATE): sens = await text_sensor.new_text_sensor(state_config) diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index b61b92fd1e..c1e4e11d54 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -15,6 +15,8 @@ from esphome.const import ( CONF_PULLUP, CONF_TRIGGER_ID, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CONF_KEYPAD = "keypad" CONF_KEYS = "keys" @@ -40,7 +42,7 @@ SX1509KeyTrigger = sx1509_ns.class_( ) -def check_keys(config): +def check_keys(config: ConfigType) -> ConfigType: if ( CONF_KEYS in config and len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS] @@ -82,7 +84,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) @@ -104,7 +106,7 @@ async def to_code(config): await automation.build_automation(trigger, [(cg.uint8, "x")], tconf) -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]: @@ -142,7 +144,7 @@ SX1509_PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register(CONF_SX1509, SX1509_PIN_SCHEMA) -async def sx1509_pin_to_code(config): +async def sx1509_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_SX1509]) cg.add(var.set_parent(parent)) diff --git a/esphome/components/sx1509/binary_sensor/__init__.py b/esphome/components/sx1509/binary_sensor/__init__.py index 0ceca77a5d..154a841348 100644 --- a/esphome/components/sx1509/binary_sensor/__init__.py +++ b/esphome/components/sx1509/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_ROW +from esphome.types import ConfigType from .. import CONF_SX1509_ID, SX1509Component, sx1509_ns @@ -18,7 +19,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(SX1509BinarySensor).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) hub = await cg.get_variable(config[CONF_SX1509_ID]) cg.add(var.set_row_col(config[CONF_ROW], config[CONF_COL])) diff --git a/esphome/components/sx1509/output/__init__.py b/esphome/components/sx1509/output/__init__.py index 9e2db7bb10..aed5ab7dd4 100644 --- a/esphome/components/sx1509/output/__init__.py +++ b/esphome/components/sx1509/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_SX1509_ID, SX1509Component, sx1509_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_SX1509_ID]) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/sy6970/__init__.py b/esphome/components/sy6970/__init__.py index 2390d046e4..cb9d64aee7 100644 --- a/esphome/components/sy6970/__init__.py +++ b/esphome/components/sy6970/__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 = ["@linkedupbits"] DEPENDENCIES = ["i2c"] @@ -48,7 +49,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable( config[CONF_ID], config[CONF_ENABLE_STATUS_LED], diff --git a/esphome/components/sy6970/binary_sensor/__init__.py b/esphome/components/sy6970/binary_sensor/__init__.py index 132b282051..c95850aadc 100644 --- a/esphome/components/sy6970/binary_sensor/__init__.py +++ b/esphome/components/sy6970/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 DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_POWER +from esphome.types import ConfigType from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns @@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_SY6970_ID]) if vbus_connected_config := config.get(CONF_VBUS_CONNECTED): diff --git a/esphome/components/sy6970/sensor/__init__.py b/esphome/components/sy6970/sensor/__init__.py index e6ee9d1337..8f8090b6ee 100644 --- a/esphome/components/sy6970/sensor/__init__.py +++ b/esphome/components/sy6970/sensor/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( UNIT_MILLIAMP, UNIT_VOLT, ) +from esphome.types import ConfigType from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns @@ -71,7 +72,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_SY6970_ID]) if vbus_voltage_config := config.get(CONF_VBUS_VOLTAGE): diff --git a/esphome/components/sy6970/text_sensor/__init__.py b/esphome/components/sy6970/text_sensor/__init__.py index 2a4eb90811..03a55393b9 100644 --- a/esphome/components/sy6970/text_sensor/__init__.py +++ b/esphome/components/sy6970/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_SY6970_ID, SY6970Component, sy6970_ns @@ -36,7 +37,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_SY6970_ID]) if bus_status_config := config.get(CONF_BUS_STATUS): diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py index 87c7b9e85c..f9233511e1 100644 --- a/esphome/components/teleinfo/__init__.py +++ b/esphome/components/teleinfo/__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 = ["@0hax"] MULTI_CONF = True @@ -34,7 +35,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py index 150484d97a..b51d4cb795 100644 --- a/esphome/components/teleinfo/sensor/__init__.py +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( STATE_CLASS_TOTAL_INCREASING, UNIT_WATT_HOURS, ) +from esphome.types import ConfigType from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, teleinfo_ns @@ -22,7 +23,7 @@ CONFIG_SCHEMA = sensor.sensor_schema( ).extend(TELEINFO_LISTENER_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) await cg.register_component(var, config) await sensor.register_sensor(var, config) diff --git a/esphome/components/teleinfo/text_sensor/__init__.py b/esphome/components/teleinfo/text_sensor/__init__.py index 79fabd10d0..0b6ff11d74 100644 --- a/esphome/components/teleinfo/text_sensor/__init__.py +++ b/esphome/components/teleinfo/text_sensor/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import text_sensor from esphome.const import CONF_ID +from esphome.types import ConfigType from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, teleinfo_ns @@ -13,7 +14,7 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema(TeleInfoTextSensor).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) await cg.register_component(var, config) await text_sensor.register_text_sensor(var, config) diff --git a/esphome/components/tm1638/binary_sensor/__init__.py b/esphome/components/tm1638/binary_sensor/__init__.py index de6ea35e54..4f89b7bf5e 100644 --- a/esphome/components/tm1638/binary_sensor/__init__.py +++ b/esphome/components/tm1638/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_KEY +from esphome.types import ConfigType from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns @@ -15,7 +16,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TM1638Key).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) cg.add(var.set_keycode(config[CONF_KEY])) hub = await cg.get_variable(config[CONF_TM1638_ID]) diff --git a/esphome/components/tm1638/display.py b/esphome/components/tm1638/display.py index 14b70be94d..d6491129c6 100644 --- a/esphome/components/tm1638/display.py +++ b/esphome/components/tm1638/display.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_LAMBDA, CONF_STB_PIN, ) +from esphome.types import ConfigType CODEOWNERS = ["@skykingjwc"] @@ -31,7 +32,7 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( ).extend(cv.polling_component_schema("1s")) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await display.register_display(var, config) diff --git a/esphome/components/tm1638/output/__init__.py b/esphome/components/tm1638/output/__init__.py index b16b08d504..961abfee47 100644 --- a/esphome/components/tm1638/output/__init__.py +++ b/esphome/components/tm1638/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_LED +from esphome.types import ConfigType from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns @@ -17,7 +18,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/tm1638/switch/__init__.py b/esphome/components/tm1638/switch/__init__.py index 90ff87938c..f42b835e03 100644 --- a/esphome/components/tm1638/switch/__init__.py +++ b/esphome/components/tm1638/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_LED +from esphome.types import ConfigType from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns @@ -20,7 +21,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) cg.add(var.set_lednum(config[CONF_LED])) diff --git a/esphome/components/ufm01/__init__.py b/esphome/components/ufm01/__init__.py index 51cf3cfd91..ca0ea57796 100644 --- a/esphome/components/ufm01/__init__.py +++ b/esphome/components/ufm01/__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 = ["@ljungqvist"] @@ -34,7 +35,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/ufm01/binary_sensor.py b/esphome/components/ufm01/binary_sensor.py index 92ae585d96..59583357e4 100644 --- a/esphome/components/ufm01/binary_sensor.py +++ b/esphome/components/ufm01/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_PROBLEM, ENTITY_CATEGORY_DIAGNOSTIC +from esphome.types import ConfigType from . import CONF_UFM01_ID, UFM01Component @@ -32,7 +33,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ufm01_component = await cg.get_variable(config[CONF_UFM01_ID]) if ufc_chip_error_config := config.get(CONF_UFC_CHIP_ERROR): diff --git a/esphome/components/ufm01/sensor.py b/esphome/components/ufm01/sensor.py index 4dcd7ceebe..e3281f0b2d 100644 --- a/esphome/components/ufm01/sensor.py +++ b/esphome/components/ufm01/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( UNIT_CUBIC_METER_PER_HOUR, UNIT_LITRE, ) +from esphome.types import ConfigType from . import CONF_UFM01_ID, UFM01Component @@ -47,7 +48,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ufm01_component = await cg.get_variable(config[CONF_UFM01_ID]) if CONF_ACCUMULATED_FLOW in config: diff --git a/esphome/components/uponor_smatrix/__init__.py b/esphome/components/uponor_smatrix/__init__.py index 9588b0df7f..093408e868 100644 --- a/esphome/components/uponor_smatrix/__init__.py +++ b/esphome/components/uponor_smatrix/__init__.py @@ -2,6 +2,8 @@ import esphome.codegen as cg from esphome.components import time, uart import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_TIME_ID +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@kroimon"] @@ -61,7 +63,7 @@ UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(uponor_smatrix_ns.using) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -74,7 +76,7 @@ async def to_code(config): cg.add(var.set_time_device_address(time_device_address)) -async def register_uponor_smatrix_device(var, config): +async def register_uponor_smatrix_device(var: MockObj, config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID]) cg.add(var.set_parent(parent)) cg.add(var.set_address(config[CONF_ADDRESS])) diff --git a/esphome/components/uponor_smatrix/climate/__init__.py b/esphome/components/uponor_smatrix/climate/__init__.py index 47495fde9a..e80f59df24 100644 --- a/esphome/components/uponor_smatrix/climate/__init__.py +++ b/esphome/components/uponor_smatrix/climate/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import climate +from esphome.types import ConfigType from .. import ( UPONOR_SMATRIX_DEVICE_SCHEMA, @@ -22,7 +23,7 @@ CONFIG_SCHEMA = climate.climate_schema(UponorSmatrixClimate).extend( ) -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_uponor_smatrix_device(var, config) diff --git a/esphome/components/uponor_smatrix/sensor/__init__.py b/esphome/components/uponor_smatrix/sensor/__init__.py index f2b34538ba..52e755f005 100644 --- a/esphome/components/uponor_smatrix/sensor/__init__.py +++ b/esphome/components/uponor_smatrix/sensor/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PERCENT, ) +from esphome.types import ConfigType from .. import ( UPONOR_SMATRIX_DEVICE_SCHEMA, @@ -61,7 +62,7 @@ CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( ).extend(UPONOR_SMATRIX_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 register_uponor_smatrix_device(var, config) diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index abfed74f94..00c5e0b069 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -95,7 +95,7 @@ void USBUartTypeCH34X::dump_config() { ESP_LOGCONFIG(TAG, " CH34x chip: %s", this->chip_name_); } -bool USBUartTypeCH34X::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, +bool USBUartTypeCH34X::config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) { uint8_t cmd = 0xA1 + channel->index_; if (channel->index_ >= 2) diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index 2722ec8555..5551abe1a1 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -97,7 +97,7 @@ std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev return cdc_devs; } -bool USBUartTypeCP210X::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, +bool USBUartTypeCP210X::config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) { // On reload, skip the one-time IFC_ENABLE step (the interface is already enabled). if (reload) diff --git a/esphome/components/usb_uart/ft23xx.cpp b/esphome/components/usb_uart/ft23xx.cpp index 79aa107d72..fcebf0fbd9 100644 --- a/esphome/components/usb_uart/ft23xx.cpp +++ b/esphome/components/usb_uart/ft23xx.cpp @@ -270,7 +270,7 @@ std::vector USBUartTypeFT23XX::parse_descriptors(usb_device_handle_t dev return cdc_devs; } -void USBUartTypeFT23XX::start_input(USBUartChannel *channel) { +void USBUartTypeFT23XX::start_input(USBUartChannelBase *channel) { if (!channel->initialised_.load()) return; @@ -336,12 +336,12 @@ void USBUartTypeFT23XX::start_input(USBUartChannel *channel) { } } -void USBUartTypeFT23XX::on_rx_overflow(USBUartChannel *channel) { +void USBUartTypeFT23XX::on_rx_overflow(USBUartChannelBase *channel) { ESP_LOGW(TAG, "RX buffer overflow on channel %d, clearing to resync", channel->index_); channel->input_buffer_.clear(); } -bool USBUartTypeFT23XX::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, +bool USBUartTypeFT23XX::config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) { // On reload (settings change on an open channel) skip the SIO reset; the FTDI set_termios // path only re-applies baud + line properties and does not re-assert DTR/RTS. diff --git a/esphome/components/usb_uart/pl2303.cpp b/esphome/components/usb_uart/pl2303.cpp index c56f43f75a..a9f7348331 100644 --- a/esphome/components/usb_uart/pl2303.cpp +++ b/esphome/components/usb_uart/pl2303.cpp @@ -226,7 +226,7 @@ static const Pl2303InitStep PL2303_INIT[] = { }; static constexpr uint8_t PL2303_INIT_COUNT = sizeof(PL2303_INIT) / sizeof(PL2303_INIT[0]); -bool USBUartTypePL2303::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, +bool USBUartTypePL2303::config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) { bool is_legacy = (this->chip_type_ == PL2303_TYPE_H); bool is_hxn = (this->chip_type_ == PL2303_TYPE_HXN); diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index c289625f1a..cf66e4c369 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -136,7 +136,7 @@ size_t RingBuffer::pop(uint8_t *data, size_t len) { } return len; } -void USBUartChannel::write_array(const uint8_t *data, size_t len) { +void USBUartChannelBase::write_array(const uint8_t *data, size_t len) { if (!this->initialised_.load()) { ESP_LOGD(TAG, "Channel not initialised - write ignored"); return; @@ -170,7 +170,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { this->parent_->start_output(this); } -uart::UARTFlushResult USBUartChannel::flush() { +uart::UARTFlushResult USBUartChannelBase::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. // The flush_timeout_ms_ timeout guards against a device that stops responding mid-flush; @@ -186,14 +186,14 @@ uart::UARTFlushResult USBUartChannel::flush() { return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } -bool USBUartChannel::peek_byte(uint8_t *data) { +bool USBUartChannelBase::peek_byte(uint8_t *data) { if (this->input_buffer_.is_empty()) { return false; } *data = this->input_buffer_.peek(); return true; } -bool USBUartChannel::read_array(uint8_t *data, size_t len) { +bool USBUartChannelBase::read_array(uint8_t *data, size_t len) { if (!this->initialised_.load()) { ESP_LOGV(TAG, "Channel not initialised - read ignored"); return false; @@ -277,7 +277,7 @@ void USBUartComponent::dump_config() { YESNO(channel->dummy_receiver_)); } } -void USBUartComponent::start_input(USBUartChannel *channel) { +void USBUartComponent::start_input(USBUartChannelBase *channel) { if (!channel->initialised_.load()) return; // THREAD CONTEXT: Called from both USB task and main loop threads @@ -346,7 +346,7 @@ void USBUartComponent::start_input(USBUartChannel *channel) { } } -void USBUartComponent::start_output(USBUartChannel *channel) { +void USBUartComponent::start_output(USBUartChannelBase *channel) { // THREAD CONTEXT: Called from both main loop and USB task threads. // The output_queue_ is a lock-free SPSC queue, so pop() is safe from either thread. // The output_started_ atomic flag is claimed via compare_exchange to guarantee that @@ -491,7 +491,7 @@ void USBUartTypeCdcAcm::on_disconnected() { USBClient::on_disconnected(); } -bool USBUartTypeCdcAcm::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, +bool USBUartTypeCdcAcm::config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) { static constexpr uint8_t CDC_REQUEST_TYPE = usb_host::USB_TYPE_CLASS | usb_host::USB_RECIP_INTERFACE; static constexpr uint8_t CDC_SET_LINE_CODING = 0x20; @@ -537,7 +537,7 @@ void USBUartComponent::enable_channels() { this->start_config_(false); } -void USBUartComponent::apply_channel_settings(USBUartChannel *channel) { +void USBUartComponent::apply_channel_settings(USBUartChannelBase *channel) { if (this->cfg_active_) { // A config sequence is already running. Defer this reload until it finishes to preserve // the one-control-transfer-at-a-time guarantee (restarting mid-flight would let an @@ -620,7 +620,7 @@ bool USBUartComponent::run_config_machine_() { this->cfg_ok_ = true; } - USBUartChannel *channel = + USBUartChannelBase *channel = this->cfg_single_ != nullptr ? this->cfg_single_ : (this->cfg_channel_idx_ < this->channels_.size() ? this->channels_[this->cfg_channel_idx_] : nullptr); @@ -664,7 +664,7 @@ bool USBUartComponent::run_config_machine_() { return true; } -void USBUartChannel::load_settings(bool /*dump_config*/) { +void USBUartChannelBase::load_settings(bool /*dump_config*/) { // The per-channel control transfers already log their values at debug level. this->parent_->apply_channel_settings(this); } diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 5bb4c97796..00b34fb942 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -16,7 +16,7 @@ namespace esphome::usb_uart { class USBUartTypeCdcAcm; class USBUartComponent; -class USBUartChannel; +class USBUartChannelBase; class USBUartTypePL2303; static const char *const TAG = "usb_uart"; @@ -110,7 +110,7 @@ class RingBuffer { struct UsbDataChunk { uint8_t data[usb_host::USB_MAX_PACKET_SIZE]; uint16_t length; - USBUartChannel *channel; + USBUartChannelBase *channel; // Required for EventPool - no cleanup needed for POD types void release() {} @@ -126,7 +126,11 @@ struct UsbOutputChunk { void release() {} }; -class USBUartChannel final : public uart::UARTComponent, public Parented { +// Common, non-final base for all USB UART channel implementations. +// Concrete channel types (USBUartChannel for CDC-style devices, vendor-specific +// multiplexed channels like CH934X) derive from this and are themselves final, +// per the "configurable classes are final" convention. +class USBUartChannelBase : public uart::UARTComponent, public Parented { friend class USBUartComponent; friend class USBUartTypeCdcAcm; friend class USBUartTypeCP210X; @@ -139,7 +143,6 @@ class USBUartChannel final : public uart::UARTComponent, public Parented cb) { this->rx_callback_ = std::move(cb); } protected: + // Not directly instantiable; construct a concrete channel type instead. + USBUartChannelBase(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {} void check_logger_conflict() override {} // Larger structures first (8+ bytes) RingBuffer input_buffer_; @@ -185,33 +190,40 @@ class USBUartChannel final : public uart::UARTComponent, public Parented get_channels() { return this->channels_; } + std::vector get_channels() { return this->channels_; } - void add_channel(USBUartChannel *channel) { this->channels_.push_back(channel); } + void add_channel(USBUartChannelBase *channel) { this->channels_.push_back(channel); } - virtual void start_input(USBUartChannel *channel); - void start_output(USBUartChannel *channel); + virtual void start_input(USBUartChannelBase *channel); + void start_output(USBUartChannelBase *channel); // Begin configuring all channels (full initialisation). Called from on_connected(). void enable_channels(); // Re-apply line settings to a single, already-open channel (used by - // USBUartChannel::load_settings()). - void apply_channel_settings(USBUartChannel *channel); + // USBUartChannelBase::load_settings()). + void apply_channel_settings(USBUartChannelBase *channel); // Called from loop() when input_buffer_ has insufficient space for the incoming chunk. // Default is a no-op; override in device-specific subclasses that need resync on overflow. - virtual void on_rx_overflow(USBUartChannel *channel) {} + virtual void on_rx_overflow(USBUartChannelBase *channel) {} // Lock-free data transfer from USB task to main loop static constexpr int USB_DATA_QUEUE_SIZE = 32; LockFreeQueue usb_data_queue_; - // Pool sized to queue capacity (SIZE-1) — see USBUartChannel::output_pool_ comment. + // Pool sized to queue capacity (SIZE-1) — see USBUartChannelBase::output_pool_ comment. EventPool chunk_pool_; protected: @@ -231,18 +243,19 @@ class USBUartComponent : public usb_host::USBClient { // next control transfer via config_transfer_() and return true, or return false when the // channel has no more steps. reload=true ⇒ apply only baud/parity/stop/data (skip // enable/reset/DTR-RTS). ok/response carry the previous step's result and IN data. - virtual bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) = 0; + virtual bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, + const uint8_t *response) = 0; // Optional one-time device-level setup run before the per-channel phase on init only // (e.g. CH34x chip detection). Same contract as config_step_(). Default: no steps. virtual bool config_device_step(uint8_t step, bool ok, const uint8_t *response) { return false; } - std::vector channels_{}; + std::vector channels_{}; // Config state machine - USBUartChannel *cfg_single_{nullptr}; // non-null: reload of a single channel - USBUartChannel *cfg_pending_reload_{nullptr}; // reload requested while the machine was busy - std::atomic cfg_done_{false}; // synchronizes cfg_ok_/cfg_response_ across threads - uint8_t cfg_response_[8]{}; // last IN transfer payload (for detection reads) + USBUartChannelBase *cfg_single_{nullptr}; // non-null: reload of a single channel + USBUartChannelBase *cfg_pending_reload_{nullptr}; // reload requested while the machine was busy + std::atomic cfg_done_{false}; // synchronizes cfg_ok_/cfg_response_ across threads + uint8_t cfg_response_[8]{}; // last IN transfer payload (for detection reads) uint8_t cfg_channel_idx_{0}; uint8_t cfg_step_{0}; bool cfg_active_{false}; @@ -260,7 +273,7 @@ class USBUartTypeCdcAcm : public USBUartComponent { virtual std::vector parse_descriptors(usb_device_handle_t dev_hdl); void on_connected() override; void on_disconnected() override; - bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; + bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; }; class USBUartTypeCP210X : public USBUartTypeCdcAcm { @@ -269,7 +282,7 @@ class USBUartTypeCP210X : public USBUartTypeCdcAcm { protected: std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; - bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; + bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; }; class USBUartTypeCH34X : public USBUartTypeCdcAcm { public: @@ -277,7 +290,7 @@ class USBUartTypeCH34X : public USBUartTypeCdcAcm { void dump_config() override; protected: - bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; + bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; bool config_device_step(uint8_t step, bool ok, const uint8_t *response) override; std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; @@ -291,12 +304,12 @@ class USBUartTypeFT23XX : public USBUartTypeCdcAcm { public: USBUartTypeFT23XX(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} - void start_input(USBUartChannel *channel) override; - void on_rx_overflow(USBUartChannel *channel) override; + void start_input(USBUartChannelBase *channel) override; + void on_rx_overflow(USBUartChannelBase *channel) override; protected: std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; - bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; + bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; uint8_t chip_type_{255}; }; @@ -312,14 +325,14 @@ enum Pl2303ChipType : uint8_t { }; class USBUartTypePL2303 : public USBUartTypeCdcAcm { - friend class USBUartChannel; + friend class USBUartChannelBase; public: USBUartTypePL2303(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} protected: std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; - bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; + bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; Pl2303ChipType chip_type_{PL2303_TYPE_UNKNOWN}; }; diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py index 583d6ccca9..3029e0f77b 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import pins import esphome.codegen as cg from esphome.components import i2c, sensor @@ -10,6 +12,8 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_METER, ) +from esphome.core import TimePeriodMicroseconds +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -23,7 +27,7 @@ CONF_LONG_RANGE = "long_range" CONF_TIMING_BUDGET = "timing_budget" -def check_keys(obj): +def check_keys(obj: ConfigType) -> ConfigType: if obj[CONF_ADDRESS] != 0x29 and CONF_ENABLE_PIN not in obj: msg = "Address other then 0x29 requires enable_pin definition to allow sensor\r" msg += "re-addressing. Also if you have more then one VL53 device on the same\r" @@ -32,7 +36,7 @@ def check_keys(obj): return obj -def check_timeout(value): +def check_timeout(value: Any) -> TimePeriodMicroseconds: value = cv.positive_time_period_microseconds(value) if value.total_seconds > 60: raise cv.Invalid("Maximum timeout can not be greater then 60 seconds") @@ -70,7 +74,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_signal_rate_limit(config[CONF_SIGNAL_RATE_LIMIT])) diff --git a/esphome/components/weikai/__init__.py b/esphome/components/weikai/__init__.py index bc80f167ef..8f0cf4ba33 100644 --- a/esphome/components/weikai/__init__.py +++ b/esphome/components/weikai/__init__.py @@ -12,6 +12,8 @@ from esphome.const import ( CONF_NUMBER, CONF_OUTPUT, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@DrCoolZic"] AUTO_LOAD = ["uart"] @@ -26,7 +28,7 @@ WeikaiComponent = weikai_ns.class_("WeikaiComponent", cg.Component) WeikaiChannel = weikai_ns.class_("WeikaiChannel", uart.UARTComponent) -def check_channel_max(value, max): +def check_channel_max(value: ConfigType, max: int) -> ConfigType: channel_uniq = [] channel_dup = [] for x in value[CONF_UART]: @@ -41,11 +43,11 @@ def check_channel_max(value, max): return value -def check_channel_max_4(value): +def check_channel_max_4(value: ConfigType) -> ConfigType: return check_channel_max(value, 4) -def check_channel_max_2(value): +def check_channel_max_2(value: ConfigType) -> ConfigType: return check_channel_max(value, 2) @@ -70,7 +72,7 @@ WKBASE_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -async def register_weikai(var, config): +async def register_weikai(var: MockObj, config: ConfigType) -> None: """Register an weikai device with the given config.""" cg.add(var.set_crystal(config[CONF_CRYSTAL])) cg.add(var.set_test_mode(config[CONF_TEST_MODE])) @@ -85,7 +87,7 @@ async def register_weikai(var, config): cg.add(chan.set_parity(uart_elem[CONF_PARITY])) -def validate_pin_mode(value): +def validate_pin_mode(value: ConfigType) -> ConfigType: """Checks input/output mode inconsistency""" if not (value[CONF_MODE][CONF_INPUT] or value[CONF_MODE][CONF_OUTPUT]): raise cv.Invalid("Mode must be either input or output") diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 127eb50df1..5ed5fc9094 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2396,14 +2396,6 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { } return true; } -bool WiFiScanResult::get_matches() const { return this->matches_; } -void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; } -const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; } -uint8_t WiFiScanResult::get_channel() const { return this->channel_; } -int8_t WiFiScanResult::get_rssi() const { return this->rssi_; } -bool WiFiScanResult::get_with_auth() const { return this->with_auth_; } -bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; } - bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; } void WiFiComponent::clear_roaming_state_() { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ea043fd5c6..c54fbc004b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -319,14 +319,15 @@ class WiFiScanResult { bool matches(const WiFiAP &config) const; - bool get_matches() const; - void set_matches(bool matches); - const bssid_t &get_bssid() const; + bool get_matches() const { return this->matches_; } + void set_matches(bool matches) { this->matches_ = matches; } + const bssid_t &get_bssid() const { return this->bssid_; } StringRef get_ssid() const { return this->ssid_.ref(); } - uint8_t get_channel() const; - int8_t get_rssi() const; - bool get_with_auth() const; - bool get_is_hidden() const; + uint8_t get_channel() const { return this->channel_; } + int8_t get_rssi() const { return this->rssi_; } + bool get_with_auth() const { return this->with_auth_; } + bool get_is_hidden() const { return this->is_hidden_; } + bool ssid_equals(const WiFiScanResult &other) const { return this->ssid_ == other.ssid_; } int8_t get_priority() const { return priority_; } void set_priority(int8_t priority) { priority_ = priority; } diff --git a/esphome/components/xl9535/__init__.py b/esphome/components/xl9535/__init__.py index 58ce4a30f8..5686b74173 100644 --- a/esphome/components/xl9535/__init__.py +++ b/esphome/components/xl9535/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CONF_NUMBER, CONF_OUTPUT, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CONF_XL9535 = "xl9535" @@ -29,13 +31,13 @@ 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) -def validate_mode(mode): +def validate_mode(mode: ConfigType) -> ConfigType: if not (mode[CONF_INPUT] or mode[CONF_OUTPUT]) or ( mode[CONF_INPUT] and mode[CONF_OUTPUT] ): @@ -43,7 +45,7 @@ def validate_mode(mode): return mode -def validate_pin(pin): +def validate_pin(pin: int) -> int: if pin in (8, 9): raise cv.Invalid(f"pin {pin} doesn't exist") return pin @@ -67,7 +69,7 @@ XL9535_PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register(CONF_XL9535, XL9535_PIN_SCHEMA) -async def xl9535_pin_to_code(config): +async def xl9535_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_XL9535]) diff --git a/esphome/components/zephyr_ble_server/__init__.py b/esphome/components/zephyr_ble_server/__init__.py index 658137d1a2..463b9c0887 100644 --- a/esphome/components/zephyr_ble_server/__init__.py +++ b/esphome/components/zephyr_ble_server/__init__.py @@ -3,7 +3,9 @@ import esphome.codegen as cg from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv from esphome.const import CONF_ID, Framework -from esphome.core import CORE +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType zephyr_ble_server_ns = cg.esphome_ns.namespace("zephyr_ble_server") BLEServer = zephyr_ble_server_ns.class_("BLEServer", cg.Component) @@ -32,7 +34,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) zephyr_add_prj_conf("BT", True) zephyr_add_prj_conf("BT_PERIPHERAL", True) @@ -65,7 +67,12 @@ BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema( BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA, synchronous=True, ) -async def numeric_comparison_reply_to_code(config, action_id, template_arg, args): +async def numeric_comparison_reply_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) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index bd08d3b63e..a276020be4 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -220,6 +220,19 @@ bool str_endswith_ignore_case(const char *str, size_t str_len, const char *suffi return strncasecmp(str + str_len - suffix_len, suffix, suffix_len) == 0; } +bool str_contains_ignore_case_fallback(const char *haystack, const char *needle) { + const size_t needle_len = strlen(needle); + if (needle_len == 0) { + return true; + } + for (const char *p = haystack; *p != '\0'; p++) { + if (strncasecmp(p, needle, needle_len) == 0) { + return true; + } + } + return false; +} + // str_truncate, str_until, str_lower_case, str_upper_case, str_snake_case moved to alloc_helpers.cpp char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { if (buffer_size == 0) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 994fa2c26a..5a9c120b84 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -981,6 +981,25 @@ inline bool str_endswith_ignore_case(const std::string &str, const char *suffix) return str_endswith_ignore_case(str.c_str(), str.size(), suffix, strlen(suffix)); } +/// Fallback implementation for case insensitive substring comparison. +bool str_contains_ignore_case_fallback(const char *haystack, const char *needle); + +/// Case-insensitive check if needle string is contained in haystack (no heap allocation). +inline bool str_contains_ignore_case(const char *haystack, const char *needle) { + if (!needle || !haystack) { + return false; + } + +// strcasestr is a GNU extension: newlib only declares it when _GNU_SOURCE is set. +// ESP32/ESP8266/host builds get it from their framework or from g++ on Linux; +// LibreTiny, RP2 and Zephyr do not, so they use the hand-rolled fallback. +#if defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_ZEPHYR) + return str_contains_ignore_case_fallback(haystack, needle); +#else // defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_ZEPHYR) + return strcasestr(haystack, needle) != nullptr; +#endif // defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_ZEPHYR) +} + // str_truncate moved to alloc_helpers.h - remove this include before 2026.11.0 // str_until, str_lower_case, str_upper_case moved to alloc_helpers.h - remove this comment before 2026.11.0 diff --git a/requirements.txt b/requirements.txt index 04844f67dc..5ff6dc6bc4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.1.19 esptool==5.3.1 click==8.3.3 -aioesphomeapi==45.12.0 +aioesphomeapi==45.13.1 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi zeroconf==0.150.0 puremagic==2.2.0 diff --git a/requirements_test.txt b/requirements_test.txt index cedc107b17..079c375c01 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ pylint==4.0.7 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.16.3 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating -prek==0.4.13 # also change in .github/workflows/ci.yml when updating +prek==0.4.14 # also change in .github/workflows/ci.yml when updating # Unit tests pytest==9.1.1 diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index f105ed5888..e68b1d17cc 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -1,5 +1,13 @@ """Tests for the deep sleep component.""" +import pytest + +from esphome import config_validation as cv +from esphome.components import deep_sleep +from esphome.const import CONF_WAKEUP_PIN, PlatformFramework + +from ..types import SetCoreConfigCallable + def test_deep_sleep_setup(generate_main): """ @@ -83,3 +91,35 @@ def test_deep_sleep_run_duration_dictionary(generate_main): " .gpio_cause = 30000,\n" "});" ) in main_cpp + + +def test_deep_sleep_bk72xx_wakeup_pin_mode_at_both_levels_rejected( + set_core_config: SetCoreConfigCallable, +) -> None: + """On BK72xx, wakeup_pin_mode at the top level and under the pin entry is an error.""" + set_core_config(PlatformFramework.BK72XX_ARDUINO) + config = { + CONF_WAKEUP_PIN: [ + {"pin": "GPIO12", deep_sleep.CONF_WAKEUP_PIN_MODE: "KEEP_AWAKE"} + ], + deep_sleep.CONF_WAKEUP_PIN_MODE: "INVERT_WAKEUP", + } + with pytest.raises(cv.Invalid, match="not both"): + deep_sleep.validate_config(config) + + +def test_deep_sleep_bk72xx_top_level_wakeup_pin_mode_moved_onto_single_pin( + set_core_config: SetCoreConfigCallable, +) -> None: + """On BK72xx, a top-level wakeup_pin_mode is moved onto the only pin entry.""" + set_core_config(PlatformFramework.BK72XX_ARDUINO) + config = { + CONF_WAKEUP_PIN: [{"pin": "GPIO12"}], + deep_sleep.CONF_WAKEUP_PIN_MODE: "INVERT_WAKEUP", + } + result = deep_sleep.validate_config(config) + + assert deep_sleep.CONF_WAKEUP_PIN_MODE not in result + assert ( + result[CONF_WAKEUP_PIN][0][deep_sleep.CONF_WAKEUP_PIN_MODE] == "INVERT_WAKEUP" + ) diff --git a/tests/component_tests/emontx/__init__.py b/tests/component_tests/emontx/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/emontx/test_sensor_defaults.py b/tests/component_tests/emontx/test_sensor_defaults.py new file mode 100644 index 0000000000..00d24d282e --- /dev/null +++ b/tests/component_tests/emontx/test_sensor_defaults.py @@ -0,0 +1,100 @@ +"""Tests for emontx sensor tag defaults.""" + +import pytest + +from esphome.components import sensor +from esphome.components.emontx.sensor import CONFIG_SCHEMA, apply_tag_defaults +from esphome.const import ( + CONF_ACCURACY_DECIMALS, + CONF_STATE_CLASS, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) + + +def _resolve_via_config_schema(tag: str) -> dict: + """Run a minimal config through the real CONFIG_SCHEMA pipeline, the + same path a user's YAML goes through.""" + return CONFIG_SCHEMA( + {"tag_name": tag, "emontx_id": "my_emontx", "name": f"{tag} sensor"} + ) + + +def test_config_schema_applies_tag_default_state_class(): + """If sensor_schema(state_class=...) is reintroduced, the schema-level + default wins over apply_tag_defaults' per-prefix value, and E1 would + resolve to measurement instead of total_increasing. Driving the real + CONFIG_SCHEMA (not just apply_tag_defaults) catches that, since + sensor_schema() runs before apply_tag_defaults in the cv.All() chain. + """ + result = _resolve_via_config_schema("E1") + assert result[CONF_STATE_CLASS] == sensor.validate_state_class( + STATE_CLASS_TOTAL_INCREASING + ) + + +def test_config_schema_applies_tag_default_accuracy_decimals(): + """Same root cause as the state_class regression: reintroducing + sensor_schema(accuracy_decimals=...) would make V1 resolve to the + schema-level default instead of the prefix-specific value of 2. + """ + result = _resolve_via_config_schema("V1") + assert result[CONF_ACCURACY_DECIMALS] == 2 + + +def _make_config(tag: str) -> dict: + """Minimal config dict with only tag_name set — no overrides.""" + return {"tag_name": tag} + + +@pytest.mark.parametrize( + ("tag", "expected_state_class", "expected_decimals"), + [ + # Known numeric-index prefixes + ("E1", STATE_CLASS_TOTAL_INCREASING, 0), + ("E12", STATE_CLASS_TOTAL_INCREASING, 0), + ("P1", STATE_CLASS_MEASUREMENT, 0), + ("V1", STATE_CLASS_MEASUREMENT, 2), + ("I1", STATE_CLASS_MEASUREMENT, 2), + ("T1", STATE_CLASS_MEASUREMENT, 2), + # Known patterns + ("PULSE1", STATE_CLASS_TOTAL_INCREASING, 0), + ("PULSE12", STATE_CLASS_TOTAL_INCREASING, 0), + ("PF1", STATE_CLASS_MEASUREMENT, 2), + # Unknown / free-form tags fall back to generic defaults + ("CUSTOM1", STATE_CLASS_MEASUREMENT, 0), + ("X", STATE_CLASS_MEASUREMENT, 0), + ], +) +def test_apply_tag_defaults(tag, expected_state_class, expected_decimals): + """apply_tag_defaults must inject the correct state_class and accuracy_decimals + for each tag type when no user overrides are present.""" + config = _make_config(tag) + result = apply_tag_defaults(config) + + assert result[CONF_STATE_CLASS] == sensor.validate_state_class(expected_state_class) + assert result[CONF_ACCURACY_DECIMALS] == expected_decimals + + +@pytest.mark.parametrize( + ("tag", "user_state_class", "user_decimals"), + [ + # User overrides must not be clobbered by defaults + ("E1", STATE_CLASS_MEASUREMENT, 3), + ("PULSE1", STATE_CLASS_MEASUREMENT, 1), + ("V1", STATE_CLASS_TOTAL_INCREASING, 0), + ("CUSTOM1", STATE_CLASS_TOTAL_INCREASING, 4), + ], +) +def test_apply_tag_defaults_respects_user_overrides( + tag, user_state_class, user_decimals +): + """apply_tag_defaults must not overwrite values already set by the user.""" + config = _make_config(tag) + config[CONF_STATE_CLASS] = sensor.validate_state_class(user_state_class) + config[CONF_ACCURACY_DECIMALS] = user_decimals + + result = apply_tag_defaults(config) + + assert result[CONF_STATE_CLASS] == sensor.validate_state_class(user_state_class) + assert result[CONF_ACCURACY_DECIMALS] == user_decimals diff --git a/tests/components/captive_portal/__init__.py b/tests/components/captive_portal/__init__.py new file mode 100644 index 0000000000..1ac0704a59 --- /dev/null +++ b/tests/components/captive_portal/__init__.py @@ -0,0 +1,10 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # The scan list helper is header-only and needs none of the component's real + # dependencies. Pulling them in breaks the host build: web_server_base + # includes ESPAsyncWebServer.h and ota.web_server includes md5/md5.h, neither + # of which exists there. + manifest.dependencies = [] + manifest.auto_load = [] diff --git a/tests/components/captive_portal/scan_list_test.cpp b/tests/components/captive_portal/scan_list_test.cpp new file mode 100644 index 0000000000..f67581dc0b --- /dev/null +++ b/tests/components/captive_portal/scan_list_test.cpp @@ -0,0 +1,130 @@ +#include + +#include +#include +#include + +#include "esphome/components/captive_portal/scan_list.h" + +namespace esphome::captive_portal::testing { + +namespace { + +// Stand-in for wifi::WiFiScanResult, which does not compile on the host. +struct Entry { + std::string ssid; + int8_t rssi; + bool with_auth{true}; + bool is_hidden{false}; + + // Compares length and bytes like CompactString does, so an embedded NUL counts. + bool ssid_equals(const Entry &other) const { return this->ssid == other.ssid; } + int8_t get_rssi() const { return this->rssi; } + bool get_with_auth() const { return this->with_auth; } + bool get_is_hidden() const { return this->is_hidden; } +}; + +// One row as the portal would emit it. +struct Row { + std::string ssid; + int8_t rssi; + bool lock; + + bool operator==(const Row &rhs) const { return ssid == rhs.ssid && rssi == rhs.rssi && lock == rhs.lock; } +}; + +// Walk the results the way handle_config does and collect the rows that survive. +std::vector rows(const std::vector &results) { + std::vector out; + for (size_t i = 0; i < results.size(); i++) { + bool with_auth = false; + if (!should_show_scan_entry(results, results[i], with_auth)) + continue; + out.push_back({results[i].ssid, results[i].rssi, with_auth}); + } + return out; +} + +} // namespace + +TEST(ScanList, SingleEntryShown) { + std::vector results = {{"Home", -60}}; + EXPECT_EQ(rows(results), (std::vector{{"Home", -60, true}})); +} + +TEST(ScanList, DistinctSsidsAllShownInOrder) { + std::vector results = {{"Home", -60}, {"Guest", -70}, {"Cafe", -40}}; + EXPECT_EQ(rows(results), (std::vector{{"Home", -60, true}, {"Guest", -70, true}, {"Cafe", -40, true}})); +} + +// Results are ordered by connection preference, not RSSI, so the strongest entry +// can sit anywhere in the list. +TEST(ScanList, SameSsidKeepsStrongest) { + std::vector results = {{"Home", -70}, {"Home", -50}, {"Home", -60}}; + EXPECT_EQ(rows(results), (std::vector{{"Home", -50, true}})); +} + +TEST(ScanList, EqualRssiKeepsFirst) { + std::vector results = {{"Home", -60}, {"Home", -60}, {"Home", -60}}; + bool with_auth = false; + EXPECT_TRUE(should_show_scan_entry(results, results[0], with_auth)); + EXPECT_FALSE(should_show_scan_entry(results, results[1], with_auth)); + EXPECT_FALSE(should_show_scan_entry(results, results[2], with_auth)); + EXPECT_EQ(rows(results), (std::vector{{"Home", -60, true}})); +} + +// with_auth is an out-parameter that must only be written for a shown entry. +TEST(ScanList, WithAuthUntouchedWhenNotShown) { + std::vector results = {{"Home", -50, false}, {"Home", -70, true}}; + bool with_auth = false; + EXPECT_FALSE(should_show_scan_entry(results, results[1], with_auth)); + EXPECT_FALSE(with_auth); +} + +TEST(ScanList, DuplicatesInterleavedWithOtherNetworks) { + std::vector results = {{"Home", -70}, {"Guest", -55}, {"Home", -50}, {"Guest", -65}}; + EXPECT_EQ(rows(results), (std::vector{{"Guest", -55, true}, {"Home", -50, true}})); +} + +// Hidden networks scan with an empty SSID. They are never listed and do not +// collapse into each other or into anything else. +TEST(ScanList, HiddenEntriesNeverShown) { + std::vector results = {{"", -40, true, true}, {"Home", -70}, {"", -30, true, true}}; + EXPECT_EQ(rows(results), (std::vector{{"Home", -70, true}})); +} + +// On ESP8266 the hidden flag comes from the driver alongside a real SSID, so a +// hidden access point can share its name with a visible one. It must not +// outrank that visible entry and leave the network unlisted. +TEST(ScanList, HiddenEntryDoesNotSuppressVisibleSameSsid) { + std::vector results = {{"Home", -40, true, true}, {"Home", -70}}; + EXPECT_EQ(rows(results), (std::vector{{"Home", -70, true}})); +} + +// An open access point and a secured one sharing an SSID collapse to one row that +// still asks for a password, whichever of them is strongest. +TEST(ScanList, LockSetWhenAnyEntryRequiresAuth) { + std::vector open_stronger = {{"Home", -50, false}, {"Home", -70, true}}; + EXPECT_EQ(rows(open_stronger), (std::vector{{"Home", -50, true}})); + + std::vector secured_stronger = {{"Home", -70, false}, {"Home", -50, true}}; + EXPECT_EQ(rows(secured_stronger), (std::vector{{"Home", -50, true}})); +} + +TEST(ScanList, LockClearWhenEveryEntryIsOpen) { + std::vector results = {{"Cafe", -60, false}, {"Cafe", -50, false}}; + EXPECT_EQ(rows(results), (std::vector{{"Cafe", -50, false}})); +} + +// The auth flag of an unrelated network must not leak into another SSID's row. +TEST(ScanList, LockIsPerSsid) { + std::vector results = {{"Cafe", -60, false}, {"Home", -50, true}}; + EXPECT_EQ(rows(results), (std::vector{{"Cafe", -60, false}, {"Home", -50, true}})); +} + +TEST(ScanList, EmptyListShowsNothing) { + std::vector results; + EXPECT_TRUE(rows(results).empty()); +} + +} // namespace esphome::captive_portal::testing diff --git a/tests/components/core/helpers_test.cpp b/tests/components/core/helpers_test.cpp index a9a940392f..d5219f9d47 100644 --- a/tests/components/core/helpers_test.cpp +++ b/tests/components/core/helpers_test.cpp @@ -83,4 +83,50 @@ TEST(StaticVectorTest, ConvertingConstructorSameSize) { EXPECT_EQ(dst[2], 3); } +TEST(StringContainsIgnoreCaseTest, NullPointerAlwaysFalse) { + const char *haystack = nullptr; + const char *needle = nullptr; + + EXPECT_FALSE(str_contains_ignore_case(haystack, needle)); + EXPECT_FALSE(str_contains_ignore_case("Hello World", needle)); + EXPECT_FALSE(str_contains_ignore_case(haystack, "anything")); +} + +TEST(StringContainsIgnoreCaseTest, EmptySearchMatches) { + const char *haystack = "Hello World"; + + EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "")); +} + +TEST(StringContainsIgnoreCaseTest, MiscCaseMatches) { + const char *haystack = "Hello World"; + + EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "Hello")); + EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "hello")); + EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "HELLO")); + EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "hELLO")); +} + +TEST(StringContainsIgnoreCaseTest, MiscNotMatching) { + const char *haystack = "Hello World"; + + // Expected to match + EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "Hell")); + + // Expected not to match + EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "Heaven")); + EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "Hello!")); + EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "world!")); +} + +TEST(StringContainsIgnoreCaseTest, FallbackMatchesLibc) { + const char *haystack = "Hello World"; + for (const char *needle : {"", "Hello", "hELLO", "Hell", "world", "Heaven", "Hello!", "d"}) { + EXPECT_EQ(str_contains_ignore_case_fallback(haystack, needle), str_contains_ignore_case(haystack, needle)) + << "needle: " << needle; + } + EXPECT_EQ(str_contains_ignore_case_fallback("", ""), str_contains_ignore_case("", "")); + EXPECT_EQ(str_contains_ignore_case_fallback("ab", "abc"), str_contains_ignore_case("ab", "abc")); +} + } // namespace esphome diff --git a/tests/components/emontx/test.esp32-idf.yaml b/tests/components/emontx/test.esp32-idf.yaml index a0784fcd53..e56b1bda5d 100644 --- a/tests/components/emontx/test.esp32-idf.yaml +++ b/tests/components/emontx/test.esp32-idf.yaml @@ -1,4 +1,3 @@ packages: uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml - -<<: !include common.yaml + emontx: !include common.yaml diff --git a/tests/components/emontx/test.esp8266-ard.yaml b/tests/components/emontx/test.esp8266-ard.yaml index 80a2cb2fc0..9ec9377437 100644 --- a/tests/components/emontx/test.esp8266-ard.yaml +++ b/tests/components/emontx/test.esp8266-ard.yaml @@ -1,4 +1,3 @@ packages: uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml - -<<: !include common.yaml + emontx: !include common.yaml diff --git a/tests/components/emontx/test.rp2040-ard.yaml b/tests/components/emontx/test.rp2040-ard.yaml index 410c579d4b..6f4952d8e5 100644 --- a/tests/components/emontx/test.rp2040-ard.yaml +++ b/tests/components/emontx/test.rp2040-ard.yaml @@ -1,4 +1,3 @@ packages: uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml - -<<: !include common.yaml + emontx: !include common.yaml diff --git a/tests/components/emontx/validate.esp32-idf.yaml b/tests/components/emontx/validate.esp32-idf.yaml new file mode 100644 index 0000000000..7caee78a07 --- /dev/null +++ b/tests/components/emontx/validate.esp32-idf.yaml @@ -0,0 +1,73 @@ +packages: + uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml + emontx: !include common.yaml + +# Validate that each sensor type gets the correct default state_class, +# unit_of_measurement, device_class, and accuracy_decimals when NO overrides +# are provided. The values are intentionally omitted so apply_tag_defaults is +# exercised, not the user-override path. + +sensor: + # Energy sensor (E prefix): expects state_class=total_increasing, unit=Wh, + # device_class=energy, accuracy_decimals=0 + - platform: emontx + tag_name: E1 + name: Energy 1 + emontx_id: test_emontx + + # Power sensor (P prefix): expects state_class=measurement, unit=W, + # device_class=power, accuracy_decimals=0 + - platform: emontx + tag_name: P1 + name: Power 1 + emontx_id: test_emontx + + # Voltage sensor (V prefix): expects state_class=measurement, unit=V, + # device_class=voltage, accuracy_decimals=2 + - platform: emontx + tag_name: V1 + name: Voltage 1 + emontx_id: test_emontx + + # Current sensor (I prefix): expects state_class=measurement, unit=A, + # device_class=current, accuracy_decimals=2 + - platform: emontx + tag_name: I1 + name: Current 1 + emontx_id: test_emontx + + # Temperature sensor (T prefix): expects state_class=measurement, unit=°C, + # device_class=temperature, accuracy_decimals=2 + - platform: emontx + tag_name: T1 + name: Temperature 1 + emontx_id: test_emontx + + # Pulse sensor (PULSE pattern): expects state_class=total_increasing, + # unit=pulses, device_class=energy, accuracy_decimals=0 + - platform: emontx + tag_name: PULSE1 + name: Pulse 1 + emontx_id: test_emontx + + # Power factor sensor (PF pattern): expects state_class=measurement, + # device_class=power_factor, accuracy_decimals=2 + - platform: emontx + tag_name: PF1 + name: Power Factor 1 + emontx_id: test_emontx + + # Unknown tag: no prefix match, falls back to state_class=measurement, + # accuracy_decimals=0 + - platform: emontx + tag_name: CUSTOM1 + name: Custom sensor + emontx_id: test_emontx + + # User override: verify that explicit values are respected and not clobbered + - platform: emontx + tag_name: E2 + name: Energy 2 (user override) + emontx_id: test_emontx + state_class: measurement + accuracy_decimals: 3 diff --git a/tests/components/esp32_hosted/test-embedded.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-embedded.esp32-p4-idf.yaml index 9640032b34..5cf33179ba 100644 --- a/tests/components/esp32_hosted/test-embedded.esp32-p4-idf.yaml +++ b/tests/components/esp32_hosted/test-embedded.esp32-p4-idf.yaml @@ -6,3 +6,6 @@ update: type: embedded path: $component_dir/test_firmware.bin sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31 + on_update_available: + then: + - logger.log: "Coprocessor update available" diff --git a/tests/components/esp32_hosted/test-http.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-http.esp32-p4-idf.yaml index 17cde0f35d..88b620cfe8 100644 --- a/tests/components/esp32_hosted/test-http.esp32-p4-idf.yaml +++ b/tests/components/esp32_hosted/test-http.esp32-p4-idf.yaml @@ -8,3 +8,6 @@ update: type: http source: https://esphome.github.io/esp-hosted-firmware/manifest/esp32c6.json update_interval: 6h + on_update_available: + then: + - logger.log: "Coprocessor update available" diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_climate_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_climate_tests.cpp index 36e0fc90b4..b91252c9fa 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_climate_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_climate_tests.cpp @@ -1,7 +1,67 @@ +#include +#include #include "../common.h" namespace esphome::mitsubishi_cn105::testing { +TEST(MitsubishiCN105ClimateTests, CelsiusTemperatureMappingAndTraitsMatchExpectedValues) { + TestableMitsubishiCN105Climate sut; + const auto mapping = TemperatureMapping(); + + for (int temperature = 16; temperature <= 31; ++temperature) { + EXPECT_EQ(mapping.to_mitsubishi(temperature), temperature); + EXPECT_EQ(mapping.from_mitsubishi(temperature), temperature); + } + + const auto traits = sut.traits(); + EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::CELSIUS); + EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 16.0f); + EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 31.0f); + EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 1.0f); + EXPECT_FLOAT_EQ(traits.get_visual_current_temperature_step(), 0.5f); +} + +TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingAndTraitsMatchExpectedValues) { + TestableMitsubishiCN105Climate sut; + auto mapping = TemperatureMapping(); + mapping.set_use_fahrenheit(true); + sut.set_use_fahrenheit(true); + + const std::array cases{ + std::pair{61, 16.0f}, std::pair{62, 16.5f}, std::pair{63, 17.0f}, std::pair{64, 17.5f}, std::pair{65, 18.0f}, + std::pair{66, 18.5f}, std::pair{67, 19.0f}, std::pair{68, 20.0f}, std::pair{69, 21.0f}, std::pair{70, 21.5f}, + std::pair{71, 22.0f}, std::pair{72, 22.5f}, std::pair{73, 23.0f}, std::pair{74, 23.5f}, std::pair{75, 24.0f}, + std::pair{76, 24.5f}, std::pair{77, 25.0f}, std::pair{78, 25.5f}, std::pair{79, 26.0f}, std::pair{80, 26.5f}, + std::pair{81, 27.0f}, std::pair{82, 27.5f}, std::pair{83, 28.0f}, std::pair{84, 28.5f}, std::pair{85, 29.0f}, + std::pair{86, 29.5f}, std::pair{87, 30.0f}, std::pair{88, 30.5f}, + }; + + for (const auto &[fahrenheit, mitsubishi_celsius] : cases) { + EXPECT_FLOAT_EQ(mapping.to_mitsubishi(fahrenheit), mitsubishi_celsius); + EXPECT_FLOAT_EQ(mapping.from_mitsubishi(mitsubishi_celsius), fahrenheit); + } + const auto traits = sut.traits(); + EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::FAHRENHEIT); + EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 61.0f); + EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 88.0f); + EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 1.0f); + EXPECT_FLOAT_EQ(traits.get_visual_current_temperature_step(), 1.0f); +} + +TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingUsesLinearConversionOutsideSetpointRange) { + auto mapping = TemperatureMapping(); + mapping.set_use_fahrenheit(true); + + const std::array cases{ + std::pair{0.0f, 32.0f}, std::pair{10.0f, 50.0f}, std::pair{15.5f, 59.9f}, + std::pair{31.0f, 87.8f}, std::pair{35.0f, 95.0f}, std::pair{40.0f, 104.0f}, + }; + + for (const auto &[celsius, fahrenheit] : cases) { + EXPECT_FLOAT_EQ(mapping.from_mitsubishi(celsius), fahrenheit); + } +} + TEST(MitsubishiCN105ClimateTests, SupportedSwingModeOffLeavesTraitsEmpty) { TestableMitsubishiCN105Climate sut; diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index f542880eef..ee287d2548 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -73,6 +73,7 @@ class TestableMitsubishiCN105Climate : public MitsubishiCN105Climate { using MitsubishiCN105Climate::last_non_swing_wide_vane_mode_; MitsubishiCN105::Status &status() { return const_cast(this->component_.status()); } + void set_use_fahrenheit(bool value) { this->component_.set_use_fahrenheit(value); } protected: MitsubishiCN105Component component_; diff --git a/tests/components/mitsubishi_cn105/common.yaml b/tests/components/mitsubishi_cn105/common.yaml index fc14724786..3f7e8c8f95 100644 --- a/tests/components/mitsubishi_cn105/common.yaml +++ b/tests/components/mitsubishi_cn105/common.yaml @@ -3,6 +3,7 @@ mitsubishi_cn105: uart_id: uart_bus update_interval: 30s telemetry_request_min_interval: 120s + use_fahrenheit: true vane: on_state: - logger.log: diff --git a/tests/components/remote_transmitter/test.bk72xx-ard.yaml b/tests/components/remote_transmitter/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..2a5cceddec --- /dev/null +++ b/tests/components/remote_transmitter/test.bk72xx-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO26 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml diff --git a/tests/components/remote_transmitter/test.rtl87xx-ard.yaml b/tests/components/remote_transmitter/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..769adbdf5c --- /dev/null +++ b/tests/components/remote_transmitter/test.rtl87xx-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO12 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml diff --git a/tests/components/runtime_image/__init__.py b/tests/components/runtime_image/__init__.py new file mode 100644 index 0000000000..a8ff4bb68e --- /dev/null +++ b/tests/components/runtime_image/__init__.py @@ -0,0 +1,15 @@ +from esphome.components.runtime_image import enable_format +from esphome.types import ConfigType +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # to_code is suppressed in cpptest builds; formats are normally enabled by + # process_runtime_image_config(). Enable all formats so the format-switch + # tests have two decoder types and every retained decoder is under test. + async def to_code_testing(config: ConfigType) -> None: + enable_format("BMP") + enable_format("PNG") + enable_format("JPEG") + + manifest.to_code = to_code_testing diff --git a/tests/components/runtime_image/test_decoder_reuse.cpp b/tests/components/runtime_image/test_decoder_reuse.cpp new file mode 100644 index 0000000000..87e77b00be --- /dev/null +++ b/tests/components/runtime_image/test_decoder_reuse.cpp @@ -0,0 +1,336 @@ +#include +#include + +#include +#include +#include +#include + +#include "esphome/components/runtime_image/image_decoder.h" +#include "esphome/components/runtime_image/runtime_image.h" + +namespace esphome::runtime_image::testing { + +// 3x2 24bpp BMP, every pixel a unique color (rows padded to 4 bytes) +static const uint8_t BMP_24BPP[] = { + 0x42, 0x4D, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x22, 0x11, 0x77, 0x88, 0x99, 0xEF, 0xCD, 0xAB, 0x00, + 0x00, 0x00, 0x20, 0x10, 0xE0, 0x40, 0xC0, 0x30, 0xA0, 0x60, 0x50, 0x00, 0x00, 0x00, +}; + +static const uint8_t BMP_24BPP_EXPECTED[2][3][3] = { + {{0xE0, 0x10, 0x20}, {0x30, 0xC0, 0x40}, {0x50, 0x60, 0xA0}}, + {{0x11, 0x22, 0x33}, {0x99, 0x88, 0x77}, {0xAB, 0xCD, 0xEF}}, +}; + +// 3x2 8bpp BMP with a 4-entry color table +static const uint8_t BMP_8BPP[] = { + 0x42, 0x4D, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x20, 0x10, 0x00, 0xD0, 0xE0, 0xF0, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, +}; + +static const uint8_t BMP_8BPP_EXPECTED[2][3][3] = { + {{0x10, 0x20, 0x30}, {0xF0, 0xE0, 0xD0}, {0x00, 0xFF, 0x00}}, + {{0xFF, 0x00, 0xFF}, {0x00, 0xFF, 0x00}, {0xF0, 0xE0, 0xD0}}, +}; + +// 3x2 8bpp BMP with an 8-entry color table, all colors distinct from BMP_8BPP's +static const uint8_t BMP_8BPP_BIG[] = { + 0x42, 0x4D, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x13, 0x0B, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x18, 0x08, + 0x00, 0xA8, 0xB8, 0xC8, 0x00, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x55, 0x99, + 0x11, 0x00, 0xCC, 0x00, 0x66, 0x00, 0x44, 0x22, 0xEE, 0x00, 0x01, 0x06, 0x04, 0x00, 0x07, 0x05, 0x03, 0x00, +}; + +static const uint8_t BMP_8BPP_BIG_EXPECTED[2][3][3] = { + {{0xEE, 0x22, 0x44}, {0x11, 0x99, 0x55}, {0x80, 0xFF, 0x00}}, + {{0xC8, 0xB8, 0xA8}, {0x66, 0x00, 0xCC}, {0xFF, 0x80, 0x00}}, +}; + +// 4x4 RGB PNG, every pixel a unique color +static const uint8_t PNG_RGB[] = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x04, 0x08, 0x02, 0x00, 0x00, 0x00, 0x26, 0x93, 0x09, 0x29, 0x00, 0x00, 0x00, 0x38, 0x49, + 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x60, 0x64, 0x62, 0x16, 0x50, 0x30, 0x58, 0xB0, 0xE1, 0xC0, 0xFF, 0xFF, 0x0C, + 0x0C, 0x0E, 0x0C, 0x50, 0xEC, 0xE0, 0xE0, 0xC0, 0x50, 0xCF, 0xF0, 0x9F, 0xA1, 0xFE, 0xFF, 0xFF, 0x7A, 0x86, 0xFA, + 0xFF, 0x0C, 0x0C, 0x42, 0x26, 0x61, 0xA9, 0xCE, 0x8A, 0xFF, 0xEE, 0xEC, 0x5A, 0x7D, 0xF6, 0x3D, 0x00, 0x81, 0xCB, + 0x12, 0x4D, 0xB3, 0xFB, 0xD4, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, +}; + +static const uint8_t PNG_RGB_EXPECTED[4][4][3] = { + {{0x01, 0x02, 0x03}, {0x10, 0x20, 0x30}, {0xA0, 0xB0, 0xC0}, {0xFF, 0xFF, 0x00}}, + {{0x40, 0x00, 0x00}, {0x00, 0x40, 0x00}, {0x00, 0x00, 0x40}, {0x40, 0x40, 0x40}}, + {{0x7F, 0x00, 0xFF}, {0x00, 0x7F, 0xFF}, {0xFF, 0x7F, 0x00}, {0x7F, 0xFF, 0x00}}, + {{0x12, 0x34, 0x56}, {0x65, 0x43, 0x21}, {0xFE, 0xDC, 0xBA}, {0xAB, 0xCD, 0xEF}}, +}; + +/// Exposes the protected decoder machinery so reuse and eviction can be observed directly. +class TestableRuntimeImage : public RuntimeImage { + public: + explicit TestableRuntimeImage(ImageFormat format) + : RuntimeImage(format, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE, nullptr, false, 0, 0) {} + + ImageDecoder *decoder() { return this->decoder_.get(); } + + /// Simulates the state a dynamic-format producer (PR #16337) would leave behind: + /// a cached decoder whose format no longer matches the image's format. + /// TODO: once #16337 adds a public way to change the format, drive the mismatch + /// through it and delete this seam. + void plant_decoder(ImageFormat format) { this->decoder_ = this->create_decoder_(format); } +}; + +/// Runs one full decode session. Returns true when every stage succeeded. +static bool decode_all(TestableRuntimeImage &img, const uint8_t *data, size_t len) { + std::vector buffer(data, data + len); // feed_data needs mutable bytes + if (!img.begin_decode(len)) { + return false; + } + size_t offset = 0; + while (offset < len) { + int consumed = img.feed_data(buffer.data() + offset, len - offset); + if (consumed <= 0) { + return false; // decode error, or no progress despite full data + } + offset += consumed; + } + return img.end_decode(); +} + +/// Feeds the image the way online_image's download loop does: append a small +/// chunk to a window, feed the window, drop what was consumed, repeat. A zero +/// return mid-stream means "need more data" and grows the window. +static bool decode_chunked(TestableRuntimeImage &img, const uint8_t *data, size_t len, size_t chunk_size) { + if (!img.begin_decode(len)) { + return false; + } + std::vector window; + size_t supplied = 0; + while (supplied < len || !window.empty()) { + if (supplied < len) { + size_t take = std::min(chunk_size, len - supplied); + window.insert(window.end(), data + supplied, data + supplied + take); + supplied += take; + } + int consumed = img.feed_data(window.data(), window.size()); + if (consumed < 0 || (consumed == 0 && supplied >= len)) { + return false; // decode error, or stuck with all data supplied + } + window.erase(window.begin(), window.begin() + consumed); + } + return img.end_decode(); +} + +template static void expect_pixels(TestableRuntimeImage &img, const uint8_t (&expected)[H][W][3]) { + ASSERT_EQ(img.get_width(), static_cast(W)); + ASSERT_EQ(img.get_height(), static_cast(H)); + for (size_t y = 0; y < H; y++) { + for (size_t x = 0; x < W; x++) { + SCOPED_TRACE(::testing::Message() << "pixel (" << x << "," << y << ")"); + Color color = img.get_pixel(x, y); + EXPECT_THAT((std::array{color.r, color.g, color.b}), ::testing::ElementsAreArray(expected[y][x])); + } + } +} + +TEST(RuntimeImageDecoder, DecoderStaysWarmAcrossDecodes) { + TestableRuntimeImage img(BMP); + + ASSERT_TRUE(decode_all(img, BMP_24BPP, sizeof(BMP_24BPP))); + expect_pixels(img, BMP_24BPP_EXPECTED); + ImageDecoder *first = img.decoder(); + ASSERT_NE(first, nullptr); + + ASSERT_TRUE(decode_all(img, BMP_24BPP, sizeof(BMP_24BPP))); + expect_pixels(img, BMP_24BPP_EXPECTED); + EXPECT_EQ(img.decoder(), first) << "decoder must be reused, not reallocated"; +} + +TEST(RuntimeImageDecoder, SecondDecodeStartsClean) { + TestableRuntimeImage img(BMP); + + // Palettized decode, then a 24bpp decode, then palettized again, all on the + // same decoder: each session must produce correct pixels for its own image. + ASSERT_TRUE(decode_all(img, BMP_8BPP, sizeof(BMP_8BPP))); + expect_pixels(img, BMP_8BPP_EXPECTED); + ImageDecoder *first = img.decoder(); + + ASSERT_TRUE(decode_all(img, BMP_24BPP, sizeof(BMP_24BPP))); + expect_pixels(img, BMP_24BPP_EXPECTED); + EXPECT_EQ(img.decoder(), first); + + ASSERT_TRUE(decode_all(img, BMP_8BPP, sizeof(BMP_8BPP))); + expect_pixels(img, BMP_8BPP_EXPECTED); + EXPECT_EQ(img.decoder(), first); +} + +TEST(RuntimeImageDecoder, ColorTableGrowsAndShrinksAcrossReuse) { + TestableRuntimeImage img(BMP); + + // Small palette first: the retained table is allocated at 4 entries. + ASSERT_TRUE(decode_all(img, BMP_8BPP, sizeof(BMP_8BPP))); + expect_pixels(img, BMP_8BPP_EXPECTED); + ImageDecoder *first = img.decoder(); + + // Growing to 8 entries on the reused decoder must reallocate, not overflow. + ASSERT_TRUE(decode_all(img, BMP_8BPP_BIG, sizeof(BMP_8BPP_BIG))); + expect_pixels(img, BMP_8BPP_BIG_EXPECTED); + EXPECT_EQ(img.decoder(), first); + + // Shrinking back must not surface stale colors from the larger table. + ASSERT_TRUE(decode_all(img, BMP_8BPP, sizeof(BMP_8BPP))); + expect_pixels(img, BMP_8BPP_EXPECTED); + EXPECT_EQ(img.decoder(), first); +} + +TEST(RuntimeImageDecoder, ChunkedFeedDecodesLikeDownloadLoop) { + TestableRuntimeImage img(BMP); + + ASSERT_TRUE(decode_chunked(img, BMP_24BPP, sizeof(BMP_24BPP), 16)); + expect_pixels(img, BMP_24BPP_EXPECTED); + ImageDecoder *first = img.decoder(); + + // Chunked again on the warm decoder: the cross-call resume state + // (current_index_ / paint_index_) must have been fully reset. + ASSERT_TRUE(decode_chunked(img, BMP_24BPP, sizeof(BMP_24BPP), 16)); + expect_pixels(img, BMP_24BPP_EXPECTED); + EXPECT_EQ(img.decoder(), first); +} + +TEST(RuntimeImageDecoder, FormatSwitchEvictsMismatchedDecoder) { + // PNG image holding a stale BMP decoder: begin_decode must evict and recreate. + TestableRuntimeImage png_img(PNG); + png_img.plant_decoder(BMP); + ASSERT_NE(png_img.decoder(), nullptr); + ASSERT_EQ(png_img.decoder()->get_format(), BMP); + + ASSERT_TRUE(decode_all(png_img, PNG_RGB, sizeof(PNG_RGB))); + EXPECT_EQ(png_img.decoder()->get_format(), PNG); + expect_pixels(png_img, PNG_RGB_EXPECTED); + + // And the other direction: BMP image holding a stale PNG decoder. + TestableRuntimeImage bmp_img(BMP); + bmp_img.plant_decoder(PNG); + ASSERT_NE(bmp_img.decoder(), nullptr); + ASSERT_EQ(bmp_img.decoder()->get_format(), PNG); + + ASSERT_TRUE(decode_all(bmp_img, BMP_24BPP, sizeof(BMP_24BPP))); + EXPECT_EQ(bmp_img.decoder()->get_format(), BMP); + expect_pixels(bmp_img, BMP_24BPP_EXPECTED); +} + +TEST(RuntimeImageDecoder, ReleaseKeepsDecoderWarm) { + TestableRuntimeImage img(PNG); + + ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB))); + ImageDecoder *first = img.decoder(); + ASSERT_NE(first, nullptr); + + img.release(); + EXPECT_EQ(img.decoder(), first) << "release() must keep the decoder for reuse"; + EXPECT_FALSE(img.is_decoding()); + EXPECT_EQ(img.get_width(), 0); + EXPECT_EQ(img.get_height(), 0); + + ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB))); + expect_pixels(img, PNG_RGB_EXPECTED); + EXPECT_EQ(img.decoder(), first); +} + +TEST(RuntimeImageDecoder, FailedDecodeRecovers) { + TestableRuntimeImage img(BMP); + + uint8_t garbage[32]; + memset(garbage, 'X', sizeof(garbage)); + ASSERT_TRUE(img.begin_decode(sizeof(garbage))); + EXPECT_LT(img.feed_data(garbage, sizeof(garbage)), 0) << "garbage must fail to decode"; + img.release(); + + ASSERT_TRUE(decode_all(img, BMP_24BPP, sizeof(BMP_24BPP))); + expect_pixels(img, BMP_24BPP_EXPECTED); +} + +#ifdef USE_RUNTIME_IMAGE_JPEG +// 8x8 gradient JPEG (quality 90). JPEG is lossy, so the test asserts that a +// reused decoder reproduces the exact same pixels, not absolute colors. +static const uint8_t JPEG_GRADIENT[] = { + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, + 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, + 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, + 0x15, 0x15, 0x0C, 0x0F, 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x03, + 0x04, 0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09, 0x14, 0x0D, 0x0B, 0x0D, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x08, 0x00, 0x08, 0x03, 0x01, 0x22, 0x00, + 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, + 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, + 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, + 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, + 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, + 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, + 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, + 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00, + 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, + 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, + 0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, + 0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, + 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, + 0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, + 0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00, 0xE5, 0x3E, 0x0B, 0xFE, + 0xC8, 0x7F, 0xEA, 0x3F, 0xD0, 0xBD, 0x3F, 0x86, 0x8A, 0x28, 0xAA, 0xC2, 0x62, 0x6A, 0xFB, 0x25, 0xA9, 0xD5, 0xC0, + 0x7C, 0x6B, 0x9D, 0x7F, 0x62, 0xD3, 0xFD, 0xEF, 0xF5, 0xF7, 0x9F, 0xFF, 0xD9, +}; + +static std::vector pixel_bytes(TestableRuntimeImage &img) { + const uint8_t *start = img.get_data_start(); + return std::vector(start, start + img.get_width_stride() * img.get_height()); +} + +TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) { + TestableRuntimeImage img(JPEG); + + ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT))); + ASSERT_EQ(img.get_width(), 8); + ASSERT_EQ(img.get_height(), 8); + std::vector first_pixels = pixel_bytes(img); + ImageDecoder *first = img.decoder(); + ASSERT_NE(first, nullptr); + + ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT))); + EXPECT_EQ(img.decoder(), first); + EXPECT_EQ(pixel_bytes(img), first_pixels) << "reused decoder must reproduce identical pixels"; +} +#endif // USE_RUNTIME_IMAGE_JPEG + +TEST(RuntimeImageDecoder, SessionFlagsTrackLifecycle) { + TestableRuntimeImage img(BMP); + std::vector buffer(BMP_24BPP, BMP_24BPP + sizeof(BMP_24BPP)); + + ASSERT_TRUE(img.begin_decode(buffer.size())); + EXPECT_TRUE(img.is_decoding()); + EXPECT_FALSE(img.is_decode_finished()); + + ASSERT_EQ(img.feed_data(buffer.data(), buffer.size()), static_cast(buffer.size())); + EXPECT_TRUE(img.is_decode_finished()) << "all pixel data consumed"; + + ASSERT_TRUE(img.end_decode()); + EXPECT_FALSE(img.is_decoding()) << "end_decode() must close the session"; + EXPECT_FALSE(img.is_decode_finished()) << "no session means nothing is 'finished'"; +} + +} // namespace esphome::runtime_image::testing diff --git a/tests/components/sendspin/common-action.yaml b/tests/components/sendspin/common-action.yaml index 16f19ad7d1..1bba06ab46 100644 --- a/tests/components/sendspin/common-action.yaml +++ b/tests/components/sendspin/common-action.yaml @@ -1,6 +1,6 @@ # `sendspin.switch` action enables the controller role, so we use a standalone test packages: - base: !include common.yaml + sendspin: !include common.yaml wifi: on_connect: diff --git a/tests/components/sendspin/common-ethernet.yaml b/tests/components/sendspin/common-ethernet.yaml new file mode 100644 index 0000000000..276163cda1 --- /dev/null +++ b/tests/components/sendspin/common-ethernet.yaml @@ -0,0 +1,5 @@ +packages: + sendspin_hub: !include common-hub.yaml + +ethernet: + type: OPENETH diff --git a/tests/components/sendspin/common-hub.yaml b/tests/components/sendspin/common-hub.yaml new file mode 100644 index 0000000000..7a6a9ffd4f --- /dev/null +++ b/tests/components/sendspin/common-hub.yaml @@ -0,0 +1,6 @@ +psram: + mode: quad + +sendspin: + id: sendspin_hub_id + task_stack_in_psram: true diff --git a/tests/components/sendspin/common-media_player.yaml b/tests/components/sendspin/common-media_player.yaml index d3792cf470..afb8b992f3 100644 --- a/tests/components/sendspin/common-media_player.yaml +++ b/tests/components/sendspin/common-media_player.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + sendspin: !include common.yaml media_player: - platform: sendspin diff --git a/tests/components/sendspin/common-media_source.yaml b/tests/components/sendspin/common-media_source.yaml index 5b33a54647..1977b79c04 100644 --- a/tests/components/sendspin/common-media_source.yaml +++ b/tests/components/sendspin/common-media_source.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + sendspin: !include common.yaml media_source: - platform: sendspin diff --git a/tests/components/sendspin/common-sensor.yaml b/tests/components/sendspin/common-sensor.yaml index 6d9745cff9..6467e38b90 100644 --- a/tests/components/sendspin/common-sensor.yaml +++ b/tests/components/sendspin/common-sensor.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + sendspin: !include common.yaml sensor: - platform: sendspin diff --git a/tests/components/sendspin/common-text_sensor.yaml b/tests/components/sendspin/common-text_sensor.yaml index fc6a56a21a..23111e8d37 100644 --- a/tests/components/sendspin/common-text_sensor.yaml +++ b/tests/components/sendspin/common-text_sensor.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + sendspin: !include common.yaml text_sensor: - platform: sendspin diff --git a/tests/components/sendspin/common.yaml b/tests/components/sendspin/common.yaml index 9d7da76758..980635b4e3 100644 --- a/tests/components/sendspin/common.yaml +++ b/tests/components/sendspin/common.yaml @@ -1,9 +1,5 @@ +packages: + sendspin_hub: !include common-hub.yaml + wifi: ap: - -psram: - mode: quad - -sendspin: - id: sendspin_hub_id - task_stack_in_psram: true diff --git a/tests/components/sendspin/test-action.esp32-idf.yaml b/tests/components/sendspin/test-action.esp32-idf.yaml index 70a7ee1bad..080eb59034 100644 --- a/tests/components/sendspin/test-action.esp32-idf.yaml +++ b/tests/components/sendspin/test-action.esp32-idf.yaml @@ -1 +1,2 @@ -<<: !include common-action.yaml +packages: + sendspin: !include common-action.yaml diff --git a/tests/components/sendspin/test-ethernet.esp32-idf.yaml b/tests/components/sendspin/test-ethernet.esp32-idf.yaml index 069e397d99..09a951d211 100644 --- a/tests/components/sendspin/test-ethernet.esp32-idf.yaml +++ b/tests/components/sendspin/test-ethernet.esp32-idf.yaml @@ -1,9 +1,2 @@ -ethernet: - type: OPENETH - -psram: - mode: quad - -sendspin: - id: sendspin_hub_id - task_stack_in_psram: true +packages: + sendspin: !include common-ethernet.yaml diff --git a/tests/components/sendspin/test-media_player.esp32-idf.yaml b/tests/components/sendspin/test-media_player.esp32-idf.yaml index cbbdb07c77..bcd4062bbe 100644 --- a/tests/components/sendspin/test-media_player.esp32-idf.yaml +++ b/tests/components/sendspin/test-media_player.esp32-idf.yaml @@ -1 +1,2 @@ -<<: !include common-media_player.yaml +packages: + sendspin: !include common-media_player.yaml diff --git a/tests/components/sendspin/test-media_source.esp32-idf.yaml b/tests/components/sendspin/test-media_source.esp32-idf.yaml index 47aeb2257c..faadccb06d 100644 --- a/tests/components/sendspin/test-media_source.esp32-idf.yaml +++ b/tests/components/sendspin/test-media_source.esp32-idf.yaml @@ -1 +1,2 @@ -<<: !include common-media_source.yaml +packages: + sendspin: !include common-media_source.yaml diff --git a/tests/components/sendspin/test-sensor.esp32-idf.yaml b/tests/components/sendspin/test-sensor.esp32-idf.yaml index f9127d47bc..1646902ca3 100644 --- a/tests/components/sendspin/test-sensor.esp32-idf.yaml +++ b/tests/components/sendspin/test-sensor.esp32-idf.yaml @@ -1 +1,2 @@ -<<: !include common-sensor.yaml +packages: + sendspin: !include common-sensor.yaml diff --git a/tests/components/sendspin/test-text_sensor.esp32-idf.yaml b/tests/components/sendspin/test-text_sensor.esp32-idf.yaml index 8998b8896e..69cf8e63fb 100644 --- a/tests/components/sendspin/test-text_sensor.esp32-idf.yaml +++ b/tests/components/sendspin/test-text_sensor.esp32-idf.yaml @@ -1 +1,2 @@ -<<: !include common-text_sensor.yaml +packages: + sendspin: !include common-text_sensor.yaml diff --git a/tests/components/sendspin/test.esp32-idf.yaml b/tests/components/sendspin/test.esp32-idf.yaml index dade44d145..36667f7fae 100644 --- a/tests/components/sendspin/test.esp32-idf.yaml +++ b/tests/components/sendspin/test.esp32-idf.yaml @@ -1 +1,2 @@ -<<: !include common.yaml +packages: + sendspin: !include common.yaml