From d9359a70c1ef82ab907aba6adad45a27cd5d5fab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Aug 2026 17:29:10 -0500 Subject: [PATCH 001/155] [tests] Keep PlatformIO libdeps per xdist worker to stop a compile race (#18524) --- tests/integration/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 1bf799b658..483d5392af 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -60,7 +60,11 @@ def _get_platformio_env(cache_dir: Path) -> dict[str, str]: env = os.environ.copy() env["PLATFORMIO_CORE_DIR"] = str(cache_dir) env["PLATFORMIO_CACHE_DIR"] = str(cache_dir / ".cache") - env["PLATFORMIO_LIBDEPS_DIR"] = str(cache_dir / "libdeps") + # libdeps is keyed only by env name (the device name), and fixtures share + # names; two xdist workers first-compiling the same name race pio pkg + # install in the same directory. Keep libdeps per worker. + worker = os.environ.get("PYTEST_XDIST_WORKER", "master") + env["PLATFORMIO_LIBDEPS_DIR"] = str(cache_dir / "libdeps" / worker) # Prevent cache cleaning during integration tests env["ESPHOME_SKIP_CLEAN_BUILD"] = "1" # Compile with THIS tree's esphome sources, not wherever the venv's editable From 828eac90f36ffe9dd1fa714f41b27377fda2cafd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:51:59 +1200 Subject: [PATCH 002/155] Bump version to 2026.8.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index c83d95d0ef..ed0670621d 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.8.0b6 +PROJECT_NUMBER = 2026.8.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 2296f8c0b7..17ff1e17d9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.8.0b6" +__version__ = "2026.8.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ca97c86d6580746e12e071351bf3d219ef7de51f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Aug 2026 19:06:55 -0500 Subject: [PATCH 003/155] [ci] Install requirements_dev.txt when the venv cache misses (#18502) --- .github/actions/restore-python/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index daf041819c..fab6dc6ffb 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -49,7 +49,7 @@ runs: python -m venv venv source venv/bin/activate python --version - uv pip install -r requirements.txt -r requirements_test.txt + uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt uv pip install -e . - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows' @@ -58,5 +58,5 @@ runs: python -m venv venv source ./venv/Scripts/activate python --version - uv pip install -r requirements.txt -r requirements_test.txt + uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt uv pip install -e . From b7d0b676fc0cd5f93a772f6f11d398a157ae612d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20H=C3=A4ll?= Date: Thu, 20 Aug 2026 06:31:27 +0200 Subject: [PATCH 004/155] [wifi] Take the lwIP core lock around sntp_servermode_dhcp() (#18511) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 245390b097..24cb060edb 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -580,7 +580,14 @@ bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, // the built-in SNTP client has a memory leak in certain situations. Disable this feature. // https://github.com/esphome/issues/issues/2299 - sntp_servermode_dhcp(false); + { +#if SNTP_GET_SERVERS_FROM_DHCP || SNTP_GET_SERVERS_FROM_DHCPV6 + // sntp_servermode_dhcp() is an empty macro unless lwIP is built with + // DHCP-supplied NTP servers, so only that build needs the core lock. + LwIPLock lock; +#endif + sntp_servermode_dhcp(false); + } // No manual IP is set; use DHCP client if (dhcp_status != ESP_NETIF_DHCP_STARTED) { From 5e9de7c94bde17b782e643a1d12745f67e23f3d6 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:00:44 -0500 Subject: [PATCH 005/155] Bump bundled esphome-device-builder to 1.12.1 (#18541) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 18f705b501..55aa0ac982 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.12.0 +RUN uv pip install --no-cache-dir esphome-device-builder==1.12.1 RUN \ platformio settings set enable_telemetry No \ From 132f494195869750e7ccf3ad2a66f58c0234da21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 06:59:57 -0500 Subject: [PATCH 006/155] [nrf52] Rebuild the Python env when its interpreter symlink dangles (#18540) --- esphome/components/nrf52/framework.py | 26 ++++--- tests/unit_tests/test_nrf52_framework.py | 90 +++++++++++++++++++++++- 2 files changed, 107 insertions(+), 9 deletions(-) diff --git a/esphome/components/nrf52/framework.py b/esphome/components/nrf52/framework.py index 6b32fe1fea..d487820440 100644 --- a/esphome/components/nrf52/framework.py +++ b/esphome/components/nrf52/framework.py @@ -62,6 +62,22 @@ def get_sdk_nrf_tools_path() -> Path: return path.resolve() +def _needs_venv_rebuild( + env_python_path: Path, sentinel: Path, requirements_hash: str +) -> bool: + """True when a penv must be (re)built. + + Rebuild when the interpreter is not a regular file, which covers a + dangling symlink (a cached venv outliving a host interpreter upgrade) + and a corrupt restore, or when the sentinel is missing or stale. + """ + return ( + not env_python_path.is_file() + or not sentinel.exists() + or sentinel.read_text(encoding="utf-8") != requirements_hash + ) + + def _get_python_env_path(version: str) -> Path: return get_sdk_nrf_tools_path() / "penvs" / version @@ -198,10 +214,7 @@ def setup_platformio_python_env() -> None: + "\n".join(_PLATFORMIO_PENV_REQUIREMENTS).encode() + f"python{sys.version_info.major}.{sys.version_info.minor}".encode() ).hexdigest() - if ( - not sentinel.exists() - or sentinel.read_text(encoding="utf-8") != requirements_hash - ): + if _needs_venv_rebuild(env_python_path, sentinel, requirements_hash): rmdir(penv_path, msg="Clean up PlatformIO toolchain Python environment") create_venv(penv_path, msg="PlatformIO toolchain") @@ -250,10 +263,7 @@ def check_and_install() -> None: env_python_path = get_python_env_executable_path(python_env_path, "python") sentinel = python_env_path / ".ready" requirements_hash = hashlib.sha256(_REQUIREMENTS.read_bytes()).hexdigest() - install_venv = ( - not sentinel.exists() - or sentinel.read_text(encoding="utf-8") != requirements_hash - ) + install_venv = _needs_venv_rebuild(env_python_path, sentinel, requirements_hash) if install_venv: rmdir(python_env_path, msg=f"Clean up {version} Python environment") diff --git a/tests/unit_tests/test_nrf52_framework.py b/tests/unit_tests/test_nrf52_framework.py index 0a6bddc280..c2ee0c2a75 100644 --- a/tests/unit_tests/test_nrf52_framework.py +++ b/tests/unit_tests/test_nrf52_framework.py @@ -16,6 +16,7 @@ from esphome.components.nrf52.framework import ( _get_penv_site_packages, _get_platformio_penv_path, _get_toolchain_platform_info, + _needs_venv_rebuild, check_and_install, get_build_env, get_sdk_nrf_tools_path, @@ -123,10 +124,19 @@ def mock_nrf52_ops(): # --------------------------------------------------------------------------- +def _touch_penv_python(penv: Path) -> None: + """Create the interpreter file so the rebuild gate sees a live venv.""" + python = get_python_env_executable_path(penv, "python") + python.parent.mkdir(parents=True, exist_ok=True) + python.touch() + + def _mark_venv_ready(python_env: Path) -> None: - """Write the venv sentinel with the current requirements hash.""" + """Write the venv sentinel with the current requirements hash and a + present interpreter so the rebuild gate passes.""" requirements_hash = hashlib.sha256(_REQUIREMENTS.read_bytes()).hexdigest() (python_env / ".ready").write_text(requirements_hash, encoding="utf-8") + _touch_penv_python(python_env) class TestCheckAndInstall: @@ -148,6 +158,23 @@ class TestCheckAndInstall: mock_nrf52_ops.download_from_mirrors.assert_not_called() mock_nrf52_ops.archive_extract_all.assert_not_called() + def test_missing_interpreter_rebuilds_venv( + self, + nrf52_dirs: SimpleNamespace, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """A valid sentinel must not mask a missing interpreter (a cached venv + restored after a host interpreter upgrade).""" + requirements_hash = hashlib.sha256(_REQUIREMENTS.read_bytes()).hexdigest() + (nrf52_dirs.python_env / ".ready").write_text( + requirements_hash, encoding="utf-8" + ) + # no interpreter on disk + + check_and_install() + + mock_nrf52_ops.create_venv.assert_called_once() + def test_fresh_install_runs_all_steps( self, nrf52_dirs: SimpleNamespace, @@ -348,6 +375,7 @@ class TestSetupPlatformioPythonEnv: (platformio_penv_dir / ".ready").write_text( _platformio_requirements_hash(), encoding="utf-8" ) + _touch_penv_python(platformio_penv_dir) with patch.dict(os.environ): setup_platformio_python_env() @@ -392,6 +420,22 @@ class TestSetupPlatformioPythonEnv: assert not (platformio_penv_dir / ".ready").exists() + def test_missing_interpreter_reinstalls( + self, + platformio_penv_dir: Path, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """A valid sentinel must not mask a missing interpreter.""" + (platformio_penv_dir / ".ready").write_text( + _platformio_requirements_hash(), encoding="utf-8" + ) + # no interpreter on disk + + with patch.dict(os.environ): + setup_platformio_python_env() + + mock_nrf52_ops.create_venv.assert_called_once() + def test_repeated_calls_do_not_duplicate_env_entries( self, platformio_penv_dir: Path, @@ -401,6 +445,7 @@ class TestSetupPlatformioPythonEnv: (platformio_penv_dir / ".ready").write_text( _platformio_requirements_hash(), encoding="utf-8" ) + _touch_penv_python(platformio_penv_dir) site_packages = str(_get_penv_site_packages(platformio_penv_dir)) bin_dir = str( get_python_env_executable_path(platformio_penv_dir, "python").parent @@ -422,6 +467,7 @@ class TestSetupPlatformioPythonEnv: (platformio_penv_dir / ".ready").write_text( _platformio_requirements_hash(), encoding="utf-8" ) + _touch_penv_python(platformio_penv_dir) site_packages = str(_get_penv_site_packages(platformio_penv_dir)) with patch.dict(os.environ, {"PYTHONPATH": "/existing/path"}): @@ -531,3 +577,45 @@ def testget_tools_path_default_is_global_cache( Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "sdk-nrf" ).resolve() assert get_sdk_nrf_tools_path() == expected + + +def test_needs_venv_rebuild_gates(tmp_path: Path) -> None: + """The shared penv gate rebuilds on any missing or stale piece.""" + penv = tmp_path / "penv" + penv.mkdir() + python = penv / "python" + sentinel = penv / ".ready" + good_hash = "abc123" + + # Nothing in place yet + assert _needs_venv_rebuild(python, sentinel, good_hash) + + python.write_text("") + # Interpreter present but no sentinel + assert _needs_venv_rebuild(python, sentinel, good_hash) + + sentinel.write_text(good_hash, encoding="utf-8") + # Everything in place + assert not _needs_venv_rebuild(python, sentinel, good_hash) + + # Stale requirements hash + assert _needs_venv_rebuild(python, sentinel, "otherhash") + + +@pytest.mark.skipif( + sys.platform == "win32", reason="symlink creation needs privileges on Windows" +) +def test_needs_venv_rebuild_on_dangling_interpreter_symlink(tmp_path: Path) -> None: + """A cached venv restored after a host interpreter upgrade has a + bin/python symlink whose target is gone; the valid sentinel must not + mask it.""" + penv = tmp_path / "penv" + penv.mkdir() + python = penv / "python" + sentinel = penv / ".ready" + sentinel.write_text("abc123", encoding="utf-8") + python.symlink_to(tmp_path / "hostedtoolcache" / "3.12.14" / "python3") + assert python.is_symlink() + assert not python.exists() + + assert _needs_venv_rebuild(python, sentinel, "abc123") From aafeca585920d39990457e46fec68faf8d4ae2d8 Mon Sep 17 00:00:00 2001 From: Alar Aun Date: Thu, 20 Aug 2026 16:54:28 +0300 Subject: [PATCH 007/155] [modbus_controller] Brace single-statement log bodies to fix -Wempty-body (#18543) --- esphome/components/modbus_controller/modbus_controller.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 2c568938e4..515459f62a 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -200,8 +200,9 @@ void ModbusController::update_range_(ModbusCommandItem &cmd) { return; } // A refusal is already logged by the hub; note the affected range for controller-level diagnostics. - if (!cmd.send()) + if (!cmd.send()) { ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", cmd.register_address()); + } } void ModbusController::update() { @@ -214,8 +215,9 @@ void ModbusController::update() { ESP_LOGV(TAG, "Module offline - retrying"); this->cmd_non_responses_ = 0; // allow the probe through can_send() for (auto &cmd : this->polling_command_items_) { - if (!cmd.send()) + if (!cmd.send()) { ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", cmd.register_address()); + } } } else { ESP_LOGV(TAG, "Module offline - skipping update"); From 3c47ab42d63b53026dcfa611baca05d08b79e3ea Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:55:15 +1200 Subject: [PATCH 008/155] [core] Add type annotations to component Python (1/11) (#18338) --- esphome/components/bl0906/sensor.py | 12 +++++-- esphome/components/datetime/__init__.py | 36 +++++++++++++------ esphome/components/esp32_ble/__init__.py | 30 ++++++++++++---- esphome/components/esp32_rmt/__init__.py | 14 +++++--- esphome/components/espnow/__init__.py | 27 ++++++++------ .../espnow/packet_transport/__init__.py | 3 +- esphome/components/http_request/__init__.py | 20 +++++++---- .../components/http_request/ota/__init__.py | 13 +++++-- .../http_request/update/__init__.py | 3 +- esphome/components/i2s_audio/__init__.py | 13 +++---- .../i2s_audio/microphone/__init__.py | 13 +++---- .../components/i2s_audio/speaker/__init__.py | 13 +++---- esphome/components/mcp23xxx_base/__init__.py | 8 +++-- esphome/components/mcp4461/__init__.py | 3 +- esphome/components/mcp4461/output/__init__.py | 28 ++++++++++++--- esphome/components/microphone/__init__.py | 31 ++++++++++------ esphome/components/pn532/__init__.py | 14 ++++++-- esphome/components/pn532/binary_sensor.py | 7 ++-- esphome/components/pn7150/__init__.py | 26 +++++++++++--- esphome/components/pn7160/__init__.py | 26 +++++++++++--- 20 files changed, 245 insertions(+), 95 deletions(-) diff --git a/esphome/components/bl0906/sensor.py b/esphome/components/bl0906/sensor.py index 059e10e962..1a0c2287ab 100644 --- a/esphome/components/bl0906/sensor.py +++ b/esphome/components/bl0906/sensor.py @@ -32,6 +32,9 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType # Import ICONS not included in esphome's const.py, from the local components const.py from .const import ICON_ENERGY, ICON_FREQUENCY, ICON_VOLTAGE @@ -145,13 +148,18 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ), synchronous=True, ) -async def reset_energy_to_code(config, action_id, template_arg, args): +async def reset_energy_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 87997daa3d..f8b6446006 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -21,13 +21,14 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_YEAR, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@rfdarter", "@jesserockz"] @@ -65,7 +66,7 @@ DATETIME_MODES = [ ] -def _validate_time_present(config): +def _validate_time_present(config: ConfigType) -> ConfigType: config = config.copy() if CONF_ON_TIME in config and CONF_TIME_ID not in config: time_id = cv.use_id(time.RealTimeClock)(None) @@ -139,7 +140,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema: @setup_entity("datetime") -async def setup_datetime_core_(var, config): +async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None: if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) @@ -160,7 +161,7 @@ async def setup_datetime_core_(var, config): await cg.register_parented(trigger, var) -async def register_datetime(var, config): +async def register_datetime(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) entity_type = config[CONF_TYPE].lower() @@ -169,14 +170,14 @@ async def register_datetime(var, config): await setup_datetime_core_(var, config) -async def new_datetime(config, *args): +async def new_datetime(config: ConfigType, *args: SafeExpType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID], *args) await register_datetime(var, config) return var @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(datetime_ns.using) @@ -193,7 +194,12 @@ async def to_code(config): ), synchronous=True, ) -async def datetime_date_set_to_code(config, action_id, template_arg, args): +async def datetime_date_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: action_var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(action_var, config[CONF_ID]) @@ -226,7 +232,12 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def datetime_time_set_to_code(config, action_id, template_arg, args): +async def datetime_time_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: action_var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(action_var, config[CONF_ID]) @@ -259,7 +270,12 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def datetime_datetime_set_to_code(config, action_id, template_arg, args): +async def datetime_datetime_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: action_var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(action_var, config[CONF_ID]) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index f099c68e57..79747c6f31 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -31,7 +31,8 @@ from esphome.const import ( CONF_NAME, CONF_NAME_ADD_MAC_SUFFIX, ) -from esphome.core import CORE, TimePeriod +from esphome.core import CORE, ID, TimePeriod +from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv from esphome.types import ConfigType @@ -383,7 +384,7 @@ def _validate_key_sizes(config: ConfigType) -> ConfigType: CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes) -def validate_variant(_): +def validate_variant(_: ConfigType) -> None: variant = get_esp32_variant() if variant in NO_BLUETOOTH_VARIANTS: raise cv.Invalid(f"{variant} does not support Bluetooth") @@ -443,7 +444,7 @@ def validate_connection_slots(max_connections: int) -> None: ) -def final_validation(config) -> None: +def final_validation(config: ConfigType) -> None: validate_variant(config) if (name := config.get(CONF_NAME)) is not None: full_config = fv.full_config.get() @@ -518,7 +519,7 @@ def final_validation(config) -> None: FINAL_VALIDATE_SCHEMA = final_validation -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) @@ -605,19 +606,34 @@ async def to_code(config): @automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({})) -async def ble_enabled_to_code(config, condition_id, template_arg, args): +async def ble_enabled_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: return cg.new_Pvariable(condition_id, template_arg) @automation.register_action( "ble.enable", BLEEnableAction, cv.Schema({}), synchronous=True ) -async def ble_enable_to_code(config, action_id, template_arg, args): +async def ble_enable_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: return cg.new_Pvariable(action_id, template_arg) @automation.register_action( "ble.disable", BLEDisableAction, cv.Schema({}), synchronous=True ) -async def ble_disable_to_code(config, action_id, template_arg, args): +async def ble_disable_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 1076bcabdc..a213a78778 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,17 +1,23 @@ +from collections.abc import Callable, Iterable +from typing import Any + from esphome.components import esp32 import esphome.config_validation as cv from esphome.core import CORE +from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} -def validate_rmt_not_supported(rmt_only_keys): +def validate_rmt_not_supported( + rmt_only_keys: Iterable[str], +) -> Callable[[ConfigType], ConfigType]: """Validate that RMT-only config keys are not used on variants without RMT hardware.""" rmt_only_keys = set(rmt_only_keys) - def _validator(config): + def _validator(config: ConfigType) -> ConfigType: if CORE.is_esp32: variant = esp32.get_esp32_variant() if variant in VARIANTS_NO_RMT: @@ -26,8 +32,8 @@ def validate_rmt_not_supported(rmt_only_keys): return _validator -def validate_clock_resolution(): - def _validator(value): +def validate_clock_resolution() -> Callable[[Any], int]: + def _validator(value: Any) -> int: cv.only_on_esp32(value) value = cv.int_(value) variant = esp32.get_esp32_variant() diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 373ef345d1..ee3732c406 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation, core import esphome.codegen as cg from esphome.components import wifi @@ -14,6 +16,7 @@ from esphome.const import ( CONF_WIFI, ) from esphome.core import CORE, HexInt +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -78,7 +81,7 @@ CONF_CONTINUE_ON_ERROR = "continue_on_error" CONF_WAIT_FOR_SENT = "wait_for_sent" -def _validate_max_payload_size(value: int) -> int: +def _validate_max_payload_size(value: Any) -> int: if value > ESPNOW_PAYLOAD_V1: return cv.require_framework_version( esp_idf=cv.Version(5, 4, 0), @@ -88,7 +91,7 @@ def _validate_max_payload_size(value: int) -> int: return value -def validate_channel(value): +def validate_channel(value: Any) -> int: if value is None: raise cv.Invalid("channel is required if wifi is not configured") return wifi.validate_channel(value) @@ -129,7 +132,7 @@ CONFIG_SCHEMA = cv.All( ) -async def _trigger_to_code(config): +async def _trigger_to_code(config: ConfigType) -> MockObj: if address := config.get(CONF_ADDRESS): address = address.parts trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], address) @@ -145,7 +148,7 @@ async def _trigger_to_code(config): return trigger -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -180,13 +183,13 @@ async def to_code(config): # ========================================== A C T I O N S ================================================ -def validate_peer(value): +def validate_peer(value: Any) -> Any: if isinstance(value, cv.Lambda): return cv.returning_lambda(value) return cv.mac_address(value) -def _validate_raw_data(value): +def _validate_raw_data(value: Any) -> str | list: if isinstance(value, str): if len(value) > MAX_ESPNOW_PACKET_SIZE: raise cv.Invalid( @@ -204,7 +207,9 @@ def _validate_raw_data(value): ) -async def register_peer(var, config, args): +async def register_peer( + var: MockObj, config: ConfigType, args: TemplateArgsType +) -> None: peer = config[CONF_ADDRESS] if isinstance(peer, core.MACAddress): peer = [HexInt(p) for p in peer.parts] @@ -231,7 +236,7 @@ SEND_SCHEMA = PEER_SCHEMA.extend( ) -def _validate_send_action(config): +def _validate_send_action(config: ConfigType) -> ConfigType: if not config[CONF_WAIT_FOR_SENT] and not config[CONF_CONTINUE_ON_ERROR]: raise cv.Invalid( f"'{CONF_CONTINUE_ON_ERROR}' cannot be false if '{CONF_WAIT_FOR_SENT}' is false as the automation will not wait for the failed result.", @@ -267,7 +272,7 @@ async def send_action( action_id: core.ID, template_arg: cg.TemplateArguments, args: list[tuple], -): +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -316,7 +321,7 @@ async def peer_action( action_id: core.ID, template_arg: cg.TemplateArguments, args: list[tuple], -): +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) await register_peer(var, config, args) @@ -341,7 +346,7 @@ async def channel_action( action_id: core.ID, template_arg: cg.TemplateArguments, args: list[tuple], -): +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) diff --git a/esphome/components/espnow/packet_transport/__init__.py b/esphome/components/espnow/packet_transport/__init__.py index e6d66440db..ee4706ca1c 100644 --- a/esphome/components/espnow/packet_transport/__init__.py +++ b/esphome/components/espnow/packet_transport/__init__.py @@ -9,6 +9,7 @@ from esphome.components.packet_transport import ( import esphome.config_validation as cv from esphome.core import HexInt from esphome.cpp_types import PollingComponent +from esphome.types import ConfigType from .. import ESPNowComponent, espnow_ns @@ -28,7 +29,7 @@ CONFIG_SCHEMA = transport_schema(ESPNowTransport).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: """Set up the ESP-NOW transport component.""" var, _ = await new_packet_transport(config) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 54d7f5c77b..afc39e06a8 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Any from esphome import automation import esphome.codegen as cg @@ -20,8 +21,10 @@ from esphome.const import ( PlatformFramework, __version__, ) -from esphome.core import CORE, Lambda +from esphome.core import CORE, ID, Lambda +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.helpers import IS_MACOS +from esphome.types import ConfigType DEPENDENCIES = ["network"] AUTO_LOAD = ["json", "watchdog"] @@ -63,14 +66,14 @@ CONF_BODY = "body" CONF_JSON = "json" -def validate_url(value): +def validate_url(value: Any) -> str: value = cv.url(value) if value.startswith(("http://", "https://")): return value raise cv.Invalid("URL must start with 'http://' or 'https://'") -def validate_ssl_verification(config): +def validate_ssl_verification(config: ConfigType) -> ConfigType: error_message = "" if CORE.is_rp2 and config[CONF_VERIFY_SSL]: @@ -91,7 +94,7 @@ def validate_ssl_verification(config): return config -def _declare_request_class(value): +def _declare_request_class(value: Any) -> ID: if CORE.is_host: return cv.declare_id(HttpRequestHost)(value) if CORE.is_esp32: @@ -151,7 +154,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_timeout(config[CONF_TIMEOUT])) cg.add(var.set_useragent(config[CONF_USERAGENT])) @@ -298,7 +301,12 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend( HTTP_REQUEST_SEND_ACTION_SCHEMA, synchronous=True, ) -async def http_request_action_to_code(config, action_id, template_arg, args): +async def http_request_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py index b7026e0f55..784e4ee47a 100644 --- a/esphome/components/http_request/ota/__init__.py +++ b/esphome/components/http_request/ota/__init__.py @@ -3,8 +3,10 @@ import esphome.codegen as cg from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME -from esphome.core import coroutine_with_priority +from esphome.core import ID, coroutine_with_priority from esphome.coroutine import CoroPriority +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns @@ -42,7 +44,7 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(CoroPriority.OTA_UPDATES) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await ota_to_code(var, config) await cg.register_component(var, config) @@ -72,7 +74,12 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All( OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA, synchronous=True, ) -async def ota_http_request_action_to_code(config, action_id, template_arg, args): +async def ota_http_request_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/http_request/update/__init__.py b/esphome/components/http_request/update/__init__.py index d84d80109a..4bdc30e4cf 100644 --- a/esphome/components/http_request/update/__init__.py +++ b/esphome/components/http_request/update/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import ota, update import esphome.config_validation as cv from esphome.const import CONF_SOURCE +from esphome.types import ConfigType from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns from ..ota import OtaHttpRequestComponent @@ -29,7 +30,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await update.new_update(config) ota_parent = await cg.get_variable(config[CONF_OTA_ID]) cg.add(var.set_ota_parent(ota_parent)) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 4809bf5a92..c5e82beb46 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -21,8 +21,9 @@ from esphome.components.esp32.const import ( import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE from esphome.core import CORE -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass import esphome.final_validate as fv +from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] @@ -145,7 +146,7 @@ I2S_MCLK_MULTIPLE = { _validate_bits = cv.float_with_unit("bits", "bit") -def validate_mclk_divisible_by_3(config): +def validate_mclk_divisible_by_3(config: ConfigType) -> ConfigType: if config[CONF_BITS_PER_SAMPLE] == 24 and config[CONF_MCLK_MULTIPLE] % 3 != 0: raise cv.Invalid( f"{CONF_MCLK_MULTIPLE} must be divisible by 3 when bits per sample is 24" @@ -159,7 +160,7 @@ def i2s_audio_component_schema( default_sample_rate: int, default_channel: str, default_bits_per_sample: str, -): +) -> cv.Schema: return cv.Schema( { cv.GenerateID(): cv.declare_id(class_), @@ -182,7 +183,7 @@ def i2s_audio_component_schema( ) -async def register_i2s_audio_component(var, config): +async def register_i2s_audio_component(var: MockObj, config: ConfigType) -> None: await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]])) slot_mode = config[CONF_CHANNEL] @@ -260,7 +261,7 @@ def _assign_ports() -> None: next_port += 1 -def _final_validate(_): +def _final_validate(_: ConfigType) -> None: i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] variant = get_esp32_variant() if variant not in I2S_PORTS: @@ -275,7 +276,7 @@ def _final_validate(_): FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 9c6228087c..c217317237 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_NUM_CHANNELS, CONF_SAMPLE_RATE, ) +from esphome.types import ConfigType from .. import ( CONF_ADC_TYPE, @@ -46,7 +47,7 @@ I2S_PDM_DSR = { } -def _validate_esp32_variant(config): +def _validate_esp32_variant(config: ConfigType) -> ConfigType: variant = esp32.get_esp32_variant() if config[CONF_ADC_TYPE] == "external": if config[CONF_PDM] and variant not in PDM_VARIANTS: @@ -65,13 +66,13 @@ def _validate_esp32_variant(config): raise NotImplementedError -def _validate_channel(config): +def _validate_channel(config: ConfigType) -> ConfigType: if config[CONF_CHANNEL] == CONF_MONO: raise cv.Invalid(f"I2S microphone does not support {CONF_MONO}.") return config -def _set_num_channels_from_config(config): +def _set_num_channels_from_config(config: ConfigType) -> ConfigType: if config[CONF_CHANNEL] in (CONF_LEFT, CONF_RIGHT): config[CONF_NUM_CHANNELS] = 1 else: @@ -80,7 +81,7 @@ def _set_num_channels_from_config(config): return config -def _set_stream_limits(config): +def _set_stream_limits(config: ConfigType) -> ConfigType: audio.set_stream_limits( min_bits_per_sample=config.get(CONF_BITS_PER_SAMPLE), max_bits_per_sample=config.get(CONF_BITS_PER_SAMPLE), @@ -134,7 +135,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: if config[CONF_ADC_TYPE] == "internal": raise cv.Invalid( "Internal ADC is no longer supported. Use an external I2S microphone instead." @@ -144,7 +145,7 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await register_i2s_audio_component(var, config) diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 6d3c39c68e..1849c376aa 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( CONF_SAMPLE_RATE, CONF_TIMEOUT, ) +from esphome.types import ConfigType from .. import ( CONF_I2S_DOUT_PIN, @@ -78,7 +79,7 @@ I2C_COMM_FMT_OPTIONS = { INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32] -def _set_num_channels_from_config(config): +def _set_num_channels_from_config(config: ConfigType) -> ConfigType: if config[CONF_CHANNEL] in (CONF_MONO, CONF_LEFT, CONF_RIGHT): config[CONF_NUM_CHANNELS] = 1 else: @@ -87,7 +88,7 @@ def _set_num_channels_from_config(config): return config -def _set_stream_limits(config): +def _set_stream_limits(config: ConfigType) -> ConfigType: if config.get(CONF_SPDIF_MODE, False): # SPDIF mode: 16/24/32-bit audio and stereo at configured sample rate audio.set_stream_limits( @@ -133,14 +134,14 @@ def _set_stream_limits(config): return config -def _select_speaker_class(config): +def _select_speaker_class(config: ConfigType) -> ConfigType: """Override ID type when SPDIF mode is enabled.""" if config.get(CONF_SPDIF_MODE, False): config[CONF_ID].type = I2SAudioSpeakerSPDIF return config -def _validate_esp32_variant(config): +def _validate_esp32_variant(config: ConfigType) -> ConfigType: variant = esp32.get_esp32_variant() if config[CONF_DAC_TYPE] == "internal": if variant not in INTERNAL_DAC_VARIANTS: @@ -207,7 +208,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: if config[CONF_DAC_TYPE] == "internal": raise cv.Invalid( "Internal DAC is no longer supported. Use an external I2S DAC instead." @@ -238,7 +239,7 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await register_i2s_audio_component(var, config) diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index d53499a78f..755d86e4ea 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -15,6 +15,8 @@ from esphome.const import ( CONF_PULLUP, ) from esphome.core import CORE, ID, coroutine +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType AUTO_LOAD = ["gpio_expander"] CODEOWNERS = ["@jesserockz"] @@ -41,7 +43,7 @@ MCP23XXX_CONFIG_SCHEMA = cv.Schema( @coroutine -async def register_mcp23xxx(config, num_pins): +async def register_mcp23xxx(config: ConfigType, num_pins: int) -> MockObj: id: ID = config[CONF_ID] var = cg.new_Pvariable(id) await cg.register_component(var, config) @@ -52,7 +54,7 @@ async def register_mcp23xxx(config, num_pins): return var -def validate_mode(value): +def validate_mode(value: ConfigType) -> ConfigType: if not (value[CONF_INPUT] or value[CONF_OUTPUT]): raise cv.Invalid("Mode must be either input or output") if value[CONF_INPUT] and value[CONF_OUTPUT]: @@ -81,7 +83,7 @@ MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema( @pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23XXX, MCP23XXX_PIN_SCHEMA) -async def mcp23xxx_pin_to_code(config): +async def mcp23xxx_pin_to_code(config: ConfigType) -> MockObj: parent_id: ID = config[CONF_MCP23XXX] parent = await cg.get_variable(parent_id) diff --git a/esphome/components/mcp4461/__init__.py b/esphome/components/mcp4461/__init__.py index f3ef6f4917..60cece67d7 100644 --- a/esphome/components/mcp4461/__init__.py +++ b/esphome/components/mcp4461/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType CODEOWNERS = ["@p1ngb4ck"] DEPENDENCIES = ["i2c"] @@ -30,7 +31,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable( config[CONF_ID], config[CONF_DISABLE_WIPER_0], diff --git a/esphome/components/mcp4461/output/__init__.py b/esphome/components/mcp4461/output/__init__.py index 99d4988c90..db1a1e6a29 100644 --- a/esphome/components/mcp4461/output/__init__.py +++ b/esphome/components/mcp4461/output/__init__.py @@ -3,6 +3,9 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID, CONF_INITIAL_VALUE +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType from .. import CONF_MCP4461_ID, Mcp4461Component, mcp4461_ns @@ -34,7 +37,7 @@ CONF_NONVOLATILE_WRITE_DELAY = "nonvolatile_write_delay" VOLATILE_CHANNELS = ("A", "B", "C", "D") -def _validate_nonvolatile(config) -> None: +def _validate_nonvolatile(config: ConfigType) -> None: channel = str(config[CONF_CHANNEL]) # Channels E-H address the nonvolatile registers directly — the mirroring options only @@ -89,7 +92,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( FINAL_VALIDATE_SCHEMA = _validate_nonvolatile -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_MCP4461_ID]) var = cg.new_Pvariable( config[CONF_ID], @@ -147,7 +150,12 @@ TERMINAL_ACTION_SCHEMA = cv.Schema( @automation.register_action( "mcp4461.wiper.decrease", WiperDecreaseAction, WIPER_ACTION_SCHEMA, synchronous=True ) -async def mcp4461_wiper_step_to_code(config, action_id, template_arg, args): +async def mcp4461_wiper_step_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: wiper = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, wiper) @@ -158,7 +166,12 @@ async def mcp4461_wiper_step_to_code(config, action_id, template_arg, args): WIPER_ACTION_SCHEMA, synchronous=True, ) -async def mcp4461_wiper_store_to_code(config, action_id, template_arg, args): +async def mcp4461_wiper_store_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: wiper = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, wiper) @@ -169,7 +182,12 @@ async def mcp4461_wiper_store_to_code(config, action_id, template_arg, args): TERMINAL_ACTION_SCHEMA, synchronous=True, ) -async def mcp4461_wiper_terminal_to_code(config, action_id, template_arg, args): +async def mcp4461_wiper_terminal_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: wiper = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable( action_id, template_arg, wiper, ord(config[CONF_TERMINAL]), config[CONF_ENABLE] diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index 6b5ee8c3e1..9a3f5b43e7 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from esphome import automation from esphome.automation import maybe_simple_id import esphome.codegen as cg @@ -12,8 +14,10 @@ from esphome.const import ( CONF_ON_DATA, CONF_TRIGGER_ID, ) -from esphome.core import CORE +from esphome.core import CORE, ID from esphome.coroutine import CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType AUTO_LOAD = ["audio"] CODEOWNERS = ["@jesserockz", "@kahrendt"] @@ -50,7 +54,7 @@ IsCapturingCondition = microphone_ns.class_( IsMutedCondition = microphone_ns.class_("IsMutedCondition", automation.Condition) -async def setup_microphone_core_(var, config): +async def setup_microphone_core_(var: MockObj, config: ConfigType) -> None: for conf in config.get(CONF_ON_DATA, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation( @@ -60,7 +64,7 @@ async def setup_microphone_core_(var, config): ) -async def register_microphone(var, config): +async def register_microphone(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) await setup_microphone_core_(var, config) @@ -85,7 +89,7 @@ def microphone_source_schema( max_bits_per_sample: int = 16, min_channels: int = 1, max_channels: int = 1, -): +) -> cv.All: """Schema for a microphone source Components requesting microphone data should use this schema instead of accessing a microphone directly. @@ -97,7 +101,7 @@ def microphone_source_schema( max_channels (int, optional): Maximum number of channels the requesting component supports. Defaults to 1. """ - def _validate_unique_channels(config): + def _validate_unique_channels(config: list[int]) -> list[int]: if len(config) != len(set(config)): raise cv.Invalid("Channels must be unique") return config @@ -124,7 +128,7 @@ def microphone_source_schema( def final_validate_microphone_source_schema( component_name: str, sample_rate: int = cv.UNDEFINED -): +) -> Callable[[ConfigType], ConfigType]: """Validates that the microphone source can provide audio in the correct format. In particular it validates the sample rate and the enabled channels. Note that: @@ -136,7 +140,7 @@ def final_validate_microphone_source_schema( sample_rate (int, optional): The sample rate the component requesting mic audio requires """ - def _validate_audio_compatability(config): + def _validate_audio_compatability(config: ConfigType) -> ConfigType: if sample_rate is not cv.UNDEFINED: # Issues require changing the microphone configuration # - Verifies sample rates match @@ -161,7 +165,9 @@ def final_validate_microphone_source_schema( return _validate_audio_compatability -async def microphone_source_to_code(config, passive=False): +async def microphone_source_to_code( + config: ConfigType, passive: bool = False +) -> MockObj: """Creates a MicrophoneSource variable for codegen. Setting passive to true makes the MicrophoneSource never start/stop the microphone, but only receives audio when another component has actively started the Microphone. If false, then the microphone needs to be explicitly started/stopped. @@ -183,7 +189,12 @@ async def microphone_source_to_code(config, passive=False): return mic_source -async def microphone_action(config, action_id, template_arg, args): +async def microphone_action( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -219,6 +230,6 @@ automation.register_condition( @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(microphone_ns.using) cg.add_define("USE_MICROPHONE") diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index f34df21647..6258932312 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -9,6 +9,9 @@ from esphome.const import ( CONF_ON_TAG_REMOVED, CONF_TRIGGER_ID, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter", "@jesserockz"] AUTO_LOAD = ["binary_sensor", "nfc"] @@ -41,7 +44,7 @@ PN532_SCHEMA = cv.Schema( ).extend(cv.polling_component_schema("1s")) -def CONFIG_SCHEMA(conf): +def CONFIG_SCHEMA(conf: ConfigType) -> None: if conf: raise cv.Invalid( "This component has been moved in 1.16, please see the docs for updated " @@ -56,7 +59,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def setup_pn532(var, config): +async def setup_pn532(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) for conf in config.get(CONF_ON_TAG, []): @@ -85,7 +88,12 @@ async def setup_pn532(var, config): } ), ) -async def pn532_is_writing_to_code(config, condition_id, template_arg, args): +async def pn532_is_writing_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/pn532/binary_sensor.py b/esphome/components/pn532/binary_sensor.py index b9c3103c65..8f490ba7d0 100644 --- a/esphome/components/pn532/binary_sensor.py +++ b/esphome/components/pn532/binary_sensor.py @@ -1,15 +1,18 @@ +from typing import Any + import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import CONF_UID from esphome.core import HexInt +from esphome.types import ConfigType from . import CONF_PN532_ID, PN532, pn532_ns DEPENDENCIES = ["pn532"] -def validate_uid(value): +def validate_uid(value: Any) -> str: value = cv.string_strict(value) for x in value.split("-"): if len(x) != 2: @@ -39,7 +42,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(PN532BinarySensor).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_PN532_ID]) diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 9dd3e8c5b0..4638992abf 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -12,6 +12,9 @@ from esphome.const import ( CONF_ON_TAG_REMOVED, CONF_TRIGGER_ID, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType AUTO_LOAD = ["binary_sensor", "nfc"] CODEOWNERS = ["@kbx81", "@jesserockz"] @@ -107,7 +110,12 @@ PN7150_SCHEMA = cv.Schema( SET_MESSAGE_ACTION_SCHEMA, synchronous=True, ) -async def pn7150_set_message_to_code(config, action_id, template_arg, args): +async def pn7150_set_message_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_MESSAGE], args, cg.std_string) @@ -158,7 +166,12 @@ async def pn7150_set_message_to_code(config, action_id, template_arg, args): SIMPLE_ACTION_SCHEMA, synchronous=True, ) -async def pn7150_simple_action_to_code(config, action_id, template_arg, args): +async def pn7150_simple_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 @@ -174,7 +187,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def setup_pn7150(var, config): +async def setup_pn7150(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) @@ -216,7 +229,12 @@ async def setup_pn7150(var, config): } ), ) -async def pn7150_is_writing_to_code(config, condition_id, template_arg, args): +async def pn7150_is_writing_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index ef14a29099..7f9f9172a1 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -12,6 +12,9 @@ from esphome.const import ( CONF_ON_TAG_REMOVED, CONF_TRIGGER_ID, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType AUTO_LOAD = ["binary_sensor", "nfc"] CODEOWNERS = ["@kbx81", "@jesserockz"] @@ -111,7 +114,12 @@ PN7160_SCHEMA = cv.Schema( SET_MESSAGE_ACTION_SCHEMA, synchronous=True, ) -async def pn7160_set_message_to_code(config, action_id, template_arg, args): +async def pn7160_set_message_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_MESSAGE], args, cg.std_string) @@ -162,7 +170,12 @@ async def pn7160_set_message_to_code(config, action_id, template_arg, args): SIMPLE_ACTION_SCHEMA, synchronous=True, ) -async def pn7160_simple_action_to_code(config, action_id, template_arg, args): +async def pn7160_simple_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 @@ -178,7 +191,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def setup_pn7160(var, config): +async def setup_pn7160(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) if dwl_req_pin_config := config.get(CONF_DWL_REQ_PIN): @@ -228,7 +241,12 @@ async def setup_pn7160(var, config): } ), ) -async def pn7160_is_writing_to_code(config, condition_id, template_arg, args): +async def pn7160_is_writing_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var From 8ef0f38f4efff4974bb1e96a210e128d99feb2fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 08:59:30 -0500 Subject: [PATCH 009/155] [ethernet] Skip the custom W5500 SPI driver for other ethernet types (#18533) --- esphome/components/ethernet/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 5eda0fc12c..7686b64cb4 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -811,6 +811,10 @@ _platform_filter = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO, }, + "w5500_custom_spi.cpp": { + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP32_ARDUINO, + }, } ) @@ -830,6 +834,11 @@ def _filter_source_files() -> list[str]: # to avoid shadowing. Native IDF builds always need the custom driver. if cv.Version(5, 4, 2) <= idf_version() < cv.Version(6, 0, 0): excluded.append("esp_eth_phy_jl1101.c") + # The custom W5500 SPI driver is fully #ifdef'd on USE_ESP32 and + # USE_ETHERNET_W5500 (the platform filter map above handles non-ESP32); + # skip it entirely for the other ethernet types. + if eth_type != "W5500": + excluded.append("w5500_custom_spi.cpp") return excluded From 2d62ea78d203727c0f20a824add2b78271f33d24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 09:00:11 -0500 Subject: [PATCH 010/155] [ota] Skip partition-access OTA sources when the feature is disabled (#18532) --- esphome/components/ota/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 2d4de52e8f..1e2ee947c1 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -182,4 +182,11 @@ def FILTER_SOURCE_FILES() -> list[str]: for define in CORE.defines ): files.append("ota_signature_esp_idf.cpp") + # ota_bootloader_esp_idf.cpp and ota_partitions_esp_idf.cpp are fully + # #ifdef'd on USE_OTA_PARTITIONS (set by the esphome OTA platform when + # allow_partition_access is enabled). Filter them out otherwise for the + # same reason as above. + if not any(define.name == "USE_OTA_PARTITIONS" for define in CORE.defines): + files.append("ota_bootloader_esp_idf.cpp") + files.append("ota_partitions_esp_idf.cpp") return files From a8e721abebdda3a42b1b6ecf391a90938b2187ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:03:49 -0500 Subject: [PATCH 011/155] [esp32] Apply IDF component exclusions to native toolchain builds (#18531) --- esphome/build_gen/espidf.py | 41 +++++++- esphome/components/esp32/__init__.py | 19 ++-- esphome/espidf/toolchain.py | 24 ++++- tests/unit_tests/build_gen/test_espidf.py | 115 +++++++++++++++++++--- tests/unit_tests/test_espidf_toolchain.py | 69 +++++++++++++ 5 files changed, 243 insertions(+), 25 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index cf476555e7..b65ce23307 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -3,7 +3,12 @@ import json from pathlib import Path -from esphome.components.esp32 import get_esp32_variant, idf_version +from esphome.components.esp32 import ( + get_esp32_variant, + get_excluded_builtin_components, + get_managed_component_require_names, + idf_version, +) import esphome.config_validation as cv from esphome.core import CORE from esphome.framework_helpers import ( @@ -119,24 +124,40 @@ def get_project_cmakelists(minimal: bool = False) -> str: # runs as a separate CMake script invocation that doesn't load the # project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_ # MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty). - from esphome.components.esp32 import get_managed_component_require_names - managed_components_property = "\n".join( f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)" for name in get_managed_component_require_names() ) + # Components excluded from the build (DEFAULT_EXCLUDED_IDF_COMPONENTS + # minus per-component re-includes). project.cmake reads the plain + # EXCLUDE_COMPONENTS variable when seeding the component list, so this + # must be set before project(). Emitted on minimal writes too so the + # discovery reconfigure never registers the excluded components. + excluded_components = get_excluded_builtin_components() + exclude_components_var = ( + f'set(EXCLUDE_COMPONENTS "{";".join(excluded_components)}")' + if excluded_components + else "" + ) + # Built-in IDF components exposed via our own property (not IDF's # __COMPONENT_REQUIRES_COMMON, which would append them to every # component's REQUIRES including real IDF components). Referenced by # src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped # on minimal writes because project_description.json may be stale. + # Excluded components are dropped here as well: a stale + # project_description.json from a build without exclusions may still + # list them, and requiring an excluded component pulls it back into + # the build (IDF requirement expansion overrides EXCLUDE_COMPONENTS). builtin_components_property = ( "" if minimal else "\n".join( f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)" - for name in sorted(get_available_components() or []) + for name in sorted( + set(get_available_components() or []).difference(excluded_components) + ) ) ) @@ -165,6 +186,8 @@ set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) +{exclude_components_var} + {cpp_standard_options} {cxx_compile_options} @@ -264,3 +287,13 @@ def write_project(minimal: bool = False) -> None: CORE.relative_src_path("CMakeLists.txt"), get_component_cmakelists(), ) + + # Snapshot the exclusion set so has_outdated_files() can trigger a + # discovery reconfigure when it changes. Excluded components never + # register in project_description.json, so re-including one (e.g. a + # config gains mqtt) requires a fresh discovery pass before the + # ESPHOME_PROJECT_BUILTIN_COMPONENTS property can list it. + write_file_if_changed( + CORE.relative_build_path("exclude_components.esphomeinternal"), + ";".join(get_excluded_builtin_components()), + ) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 3065cdadad..d6e0890751 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -738,6 +738,16 @@ def include_builtin_idf_component(name: str) -> None: CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name) +def get_excluded_builtin_components() -> list[str]: + """Return the sorted built-in IDF components excluded from the build. + + Single accessor for both build writers: the PlatformIO path passes it as + ``-DEXCLUDE_COMPONENTS`` and the native ESP-IDF path emits it into the + generated CMakeLists. + """ + return sorted(CORE.data.get(KEY_ESP32, {}).get(KEY_EXCLUDE_COMPONENTS, ())) + + def _enable_arduino_library(name: str) -> None: """Enable an Arduino library that is disabled by default. @@ -2122,13 +2132,10 @@ def _configure_lwip_max_sockets(conf: dict) -> None: @coroutine_with_priority(CoroPriority.FINAL) async def _write_exclude_components() -> None: """Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions.""" - if KEY_ESP32 not in CORE.data: - return - excluded = CORE.data[KEY_ESP32].get(KEY_EXCLUDE_COMPONENTS) - if excluded: - exclude_list = ";".join(sorted(excluded)) + if excluded := get_excluded_builtin_components(): cg.add_platformio_option( - "board_build.cmake_extra_args", f"-DEXCLUDE_COMPONENTS={exclude_list}" + "board_build.cmake_extra_args", + f"-DEXCLUDE_COMPONENTS={';'.join(excluded)}", ) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index bb6452acf2..07ba03e2cf 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -273,6 +273,11 @@ def has_outdated_files(): happen without any sdkconfig impact, and ``_write_idf_component_yml`` already deletes ``dependencies.lock`` on a change but that signal gets lost as soon as the lock is missing. + - ``exclude_components.esphomeinternal`` -- the resolved + EXCLUDE_COMPONENTS set. Excluded components never register in + ``project_description.json``, so re-including one needs a fresh + discovery pass before it can appear in the builtin-components + property that ``src`` REQUIRES. We deliberately don't watch: - The top-level/src ``CMakeLists.txt`` -- ESPHome owns those, and @@ -291,6 +296,9 @@ def has_outdated_files(): f"sdkconfig.{CORE.name}.esphomeinternal" ) idf_component_yml_path = CORE.relative_build_path("src/idf_component.yml") + exclude_components_path = CORE.relative_build_path( + "exclude_components.esphomeinternal" + ) dependency_lock_path = CORE.relative_build_path("dependencies.lock") build_ninja_path = CORE.relative_build_path("build/build.ninja") @@ -309,7 +317,11 @@ def has_outdated_files(): cmakecache_txt_mtime = cmakecache_txt_path.stat().st_mtime return any( f.stat().st_mtime > cmakecache_txt_mtime - for f in [sdkconfig_internal_path, idf_component_yml_path] + for f in [ + sdkconfig_internal_path, + idf_component_yml_path, + exclude_components_path, + ] if f.exists() ) @@ -386,6 +398,16 @@ def run_compile(config, verbose: bool) -> int: return rc _LOGGER.info("Regenerating CMakeLists.txt with discovered components...") write_project(minimal=False) + # Restamp the reference file has_outdated_files() compares against. + # A reconfigure that only changes properties or plain variables + # (sdkconfig options, the exclusion set) does not rewrite + # CMakeCache.txt, so without this the watched inputs stay newer + # forever and every subsequent build repeats the discovery pass. + # Done after the full write so an interrupt cannot leave a minimal + # CMakeLists behind that is already marked fresh. + cmakecache = CORE.relative_build_path("build/CMakeCache.txt") + if cmakecache.is_file(): + os.utime(cmakecache) if CORE.testing_mode: # Reconfigure again so cmake is up to date with the full # component list before the build's idf.py invocation runs -- diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index f21549b48c..ec01000920 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -11,6 +11,7 @@ import pytest from esphome.components.esp32 import ( KEY_COMPONENTS, KEY_ESP32, + KEY_EXCLUDE_COMPONENTS, KEY_IDF_VERSION, KEY_PATH, KEY_REF, @@ -28,6 +29,7 @@ def _reset_core(tmp_path: Path) -> None: CORE.data.setdefault(KEY_CORE, {}) CORE.data[KEY_ESP32] = { KEY_COMPONENTS: {}, + KEY_EXCLUDE_COMPONENTS: set(), KEY_IDF_VERSION: cv.Version(5, 5, 4), } @@ -47,6 +49,17 @@ def _write_project_description(tmp_path: Path, components: dict[str, str]) -> No ) +def _render(minimal: bool = False) -> str: + """Render the top-level CMakeLists with the standard variant/name patches.""" + with ( + patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"), + patch.object(CORE, "name", "test"), + ): + from esphome.build_gen.espidf import get_project_cmakelists + + return get_project_cmakelists(minimal=minimal) + + def test_get_available_components_returns_none_without_build_path() -> None: """No build_path set yet: must not raise on Path(None).""" CORE.build_path = None @@ -88,13 +101,7 @@ def test_get_project_cmakelists_minimal_omits_builtin_components_property( first write before the discovery pass refreshes it).""" _write_project_description(tmp_path, {"esp_lcd": "/idf/components/esp_lcd"}) - with ( - patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"), - patch.object(CORE, "name", "test"), - ): - from esphome.build_gen.espidf import get_project_cmakelists - - content = get_project_cmakelists(minimal=True) + content = _render(minimal=True) assert "ESPHOME_PROJECT_BUILTIN_COMPONENTS" not in content @@ -115,13 +122,7 @@ def test_get_project_cmakelists_full_emits_builtin_components_property( }, ) - with ( - patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"), - patch.object(CORE, "name", "test"), - ): - from esphome.build_gen.espidf import get_project_cmakelists - - content = get_project_cmakelists(minimal=False) + content = _render() assert ( "idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS esp_lcd APPEND)" @@ -136,6 +137,92 @@ def test_get_project_cmakelists_full_emits_builtin_components_property( assert "JPEGDEC APPEND" not in content +def test_get_project_cmakelists_emits_exclude_components(tmp_path: Path) -> None: + """Excluded components are passed to IDF via EXCLUDE_COMPONENTS and are + dropped from ESPHOME_PROJECT_BUILTIN_COMPONENTS even when a stale + project_description.json still lists them (requiring an excluded + component would pull it back into the build).""" + _write_project_description( + tmp_path, + { + "esp_lcd": "/idf/components/esp_lcd", + "freertos": "/idf/components/freertos", + "unity": "/idf/components/unity", + }, + ) + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity", "esp_lcd"} + + content = _render() + + assert 'set(EXCLUDE_COMPONENTS "esp_lcd;unity")' in content + # Must be set before project() so project.cmake sees it. + assert content.index("set(EXCLUDE_COMPONENTS") < content.index("project(test)") + assert ( + "idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS freertos APPEND)" + in content + ) + assert "ESPHOME_PROJECT_BUILTIN_COMPONENTS unity" not in content + assert "ESPHOME_PROJECT_BUILTIN_COMPONENTS esp_lcd" not in content + + +def test_get_project_cmakelists_minimal_emits_exclude_components() -> None: + """The discovery (minimal) write also excludes components so they never + register in project_description.json.""" + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity"} + + content = _render(minimal=True) + + assert 'set(EXCLUDE_COMPONENTS "unity")' in content + + +def test_get_project_cmakelists_no_exclude_components_line_when_empty() -> None: + """No EXCLUDE_COMPONENTS line at all when nothing is excluded.""" + content = _render() + + assert "EXCLUDE_COMPONENTS" not in content + + +def test_include_builtin_idf_component_removes_exclusion() -> None: + """include_builtin_idf_component() drops a name from the exclusion set so + a component a config actually uses is not passed to EXCLUDE_COMPONENTS.""" + from esphome.components.esp32 import ( + exclude_builtin_idf_component, + get_excluded_builtin_components, + include_builtin_idf_component, + ) + + exclude_builtin_idf_component("esp_eth") + exclude_builtin_idf_component("unity") + include_builtin_idf_component("esp_eth") + + assert get_excluded_builtin_components() == ["unity"] + + content = _render() + + assert 'set(EXCLUDE_COMPONENTS "unity")' in content + assert "esp_eth" not in content + + +def test_write_project_writes_exclude_components_stamp(tmp_path: Path) -> None: + """write_project() snapshots the exclusion set; the toolchain watches the + stamp to trigger a discovery reconfigure when the set changes (excluded + components never register in project_description.json).""" + CORE.build_flags = set() + CORE.build_path = tmp_path + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity", "esp_lcd"} + + with ( + patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"), + patch.object(CORE, "name", "test"), + ): + from esphome.build_gen.espidf import write_project + + write_project() + + stamp = tmp_path / "exclude_components.esphomeinternal" + assert stamp.read_text() == "esp_lcd;unity" + + def test_get_component_cmakelists_no_link_flags() -> None: """With no -Wl, flags the target_link_options block is emitted with an empty body.""" CORE.build_flags = set() diff --git a/tests/unit_tests/test_espidf_toolchain.py b/tests/unit_tests/test_espidf_toolchain.py index 26d812af8b..2556397aef 100644 --- a/tests/unit_tests/test_espidf_toolchain.py +++ b/tests/unit_tests/test_espidf_toolchain.py @@ -100,6 +100,33 @@ def _setup_build(setup_core: Path) -> tuple[Path, Path]: return compile_commands, cache +def test_has_outdated_files_detects_exclusion_change(setup_core: Path) -> None: + """A newer exclude_components.esphomeinternal stamp forces a reconfigure + so components that leave the exclusion set get rediscovered.""" + CORE.build_path = setup_core + build = setup_core / "build" + (build / "config").mkdir(parents=True) + (build / "config" / "sdkconfig.h").write_text("") + cmakecache = build / "CMakeCache.txt" + cmakecache.write_text("") + (build / "build.ninja").write_text("") + + with patch.object(CORE, "name", "test"): + assert not toolchain.has_outdated_files() + + stamp = setup_core / "exclude_components.esphomeinternal" + stamp.write_text("unity") + os.utime(stamp, (cmakecache.stat().st_mtime + 10,) * 2) + + assert toolchain.has_outdated_files() + + # The flag must clear once the reference file is restamped (as + # run_compile does after a successful discovery reconfigure); + # otherwise every later build would repeat the discovery pass. + os.utime(cmakecache, (stamp.stat().st_mtime + 10,) * 2) + assert not toolchain.has_outdated_files() + + def test_get_idedata_returns_none_without_compile_commands(setup_core: Path) -> None: """No compile DB yet -> None (rather than an error).""" _setup_build(setup_core) @@ -373,6 +400,48 @@ def test_run_idf_py_jobs_sets_build_jobs_env(setup_core: Path) -> None: assert "IDF_PY_BUILD_JOBS" not in env +def test_run_compile_restamps_cmakecache_after_discovery(setup_core: Path) -> None: + """After a successful discovery reconfigure the reference CMakeCache.txt + is restamped; cmake does not rewrite it when only properties or plain + variables change, so the staleness flag would otherwise never clear.""" + _setup_build(setup_core) + config = {CONF_ESPHOME: {}} + cmakecache = CORE.relative_build_path("build/CMakeCache.txt") + cmakecache.parent.mkdir(parents=True, exist_ok=True) + cmakecache.write_text("") + old = cmakecache.stat().st_mtime - 100 + os.utime(cmakecache, (old, old)) + + with ( + patch.object(toolchain, "need_reconfigure", return_value=True), + patch("esphome.build_gen.espidf.write_project"), + patch.object(toolchain, "run_reconfigure", return_value=0), + patch.object(toolchain, "run_idf_py", return_value=0), + patch.object(toolchain, "print_summary"), + ): + assert toolchain.run_compile(config, verbose=False) == 0 + + assert cmakecache.stat().st_mtime > old + + +def test_run_compile_discovery_without_cmakecache(setup_core: Path) -> None: + """A discovery pass that produced no CMakeCache.txt (nothing to restamp) + still completes normally.""" + _setup_build(setup_core) + config = {CONF_ESPHOME: {}} + + with ( + patch.object(toolchain, "need_reconfigure", return_value=True), + patch("esphome.build_gen.espidf.write_project"), + patch.object(toolchain, "run_reconfigure", return_value=0), + patch.object(toolchain, "run_idf_py", return_value=0), + patch.object(toolchain, "print_summary"), + ): + assert toolchain.run_compile(config, verbose=False) == 0 + + assert not CORE.relative_build_path("build/CMakeCache.txt").exists() + + def test_run_compile_passes_compile_process_limit(setup_core: Path) -> None: """compile_process_limit is forwarded to run_idf_py as the job limit.""" _setup_build(setup_core) From 347a6155f8783342d1bb7da05ad4a1254fe47f45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 09:07:17 -0500 Subject: [PATCH 012/155] [uptime] Skip the timestamp sensor source when no time component is configured (#18535) --- esphome/components/uptime/sensor/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/components/uptime/sensor/__init__.py b/esphome/components/uptime/sensor/__init__.py index e2a7aee1a2..debeb41444 100644 --- a/esphome/components/uptime/sensor/__init__.py +++ b/esphome/components/uptime/sensor/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ) +from esphome.core import CORE uptime_ns = cg.esphome_ns.namespace("uptime") UptimeSecondsSensor = uptime_ns.class_( @@ -59,3 +60,11 @@ async def to_code(config): if time_id_config := config.get(CONF_TIME_ID): time_id = await cg.get_variable(time_id_config) cg.add(var.set_time(time_id)) + + +def FILTER_SOURCE_FILES() -> list[str]: + # uptime_timestamp_sensor.cpp is fully #ifdef'd on USE_TIME; skip it + # when no time component is configured. + if not any(define.name == "USE_TIME" for define in CORE.defines): + return ["uptime_timestamp_sensor.cpp"] + return [] From ecca240eef2b79baa85d3eca6665a333e11c0e62 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:26:16 +1200 Subject: [PATCH 013/155] [core] Add type annotations to component Python (2/11) (#18339) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/cm1106/sensor.py | 12 +++++-- esphome/components/esp_ldo/__init__.py | 22 +++++++++---- esphome/components/ili9xxx/display.py | 13 ++++---- esphome/components/it8951/display.py | 32 +++++++++++++------ esphome/components/mapping/__init__.py | 17 ++++++---- esphome/components/mipi_dsi/display.py | 9 +++--- esphome/components/mipi_rgb/display.py | 14 ++++---- esphome/components/mipi_rgb/models/st7701s.py | 2 +- esphome/components/mipi_spi/display.py | 15 +++++---- esphome/components/online_image/image.py | 10 ++++-- .../components/packet_transport/__init__.py | 27 +++++++++------- .../packet_transport/binary_sensor.py | 5 +-- esphome/components/packet_transport/sensor.py | 3 +- esphome/components/pca9554/__init__.py | 12 ++++--- esphome/components/qspi_dbi/display.py | 16 ++++++---- esphome/components/qspi_dbi/models.py | 10 +++--- esphome/components/rpi_dpi_rgb/display.py | 9 ++++-- esphome/components/sdl/binary_sensor.py | 3 +- esphome/components/sdl/display.py | 9 ++++-- .../components/sdl/touchscreen/__init__.py | 3 +- esphome/components/seeed_mr24hpc1/__init__.py | 3 +- .../seeed_mr24hpc1/binary_sensor.py | 3 +- .../seeed_mr24hpc1/button/__init__.py | 3 +- .../seeed_mr24hpc1/number/__init__.py | 3 +- .../seeed_mr24hpc1/select/__init__.py | 3 +- esphome/components/seeed_mr24hpc1/sensor.py | 3 +- .../seeed_mr24hpc1/switch/__init__.py | 3 +- .../components/seeed_mr24hpc1/text_sensor.py | 3 +- esphome/components/seeed_mr60bha2/__init__.py | 3 +- .../seeed_mr60bha2/binary_sensor.py | 3 +- esphome/components/seeed_mr60bha2/sensor.py | 3 +- esphome/components/seeed_mr60fda2/__init__.py | 3 +- .../seeed_mr60fda2/binary_sensor.py | 3 +- .../seeed_mr60fda2/button/__init__.py | 3 +- .../seeed_mr60fda2/select/__init__.py | 3 +- esphome/components/st7701s/display.py | 11 ++++--- esphome/components/st7701s/init_sequences.py | 2 +- esphome/components/udp/__init__.py | 22 +++++++++---- .../udp/packet_transport/__init__.py | 3 +- esphome/components/usb_uart/__init__.py | 19 +++++------ 40 files changed, 220 insertions(+), 125 deletions(-) diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py index 3c82fac977..936c5fc673 100644 --- a/esphome/components/cm1106/sensor.py +++ b/esphome/components/cm1106/sensor.py @@ -13,6 +13,9 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["uart"] CODEOWNERS = ["@andrewjswan"] @@ -44,7 +47,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config) -> None: +async def to_code(config: ConfigType) -> None: """Code generation entry point.""" var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -67,7 +70,12 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None: +async def cm1106_calibration_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: """Service code generation entry point.""" paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index a489651b59..46810d422d 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -1,9 +1,14 @@ +from typing import Any + from esphome.automation import Action, register_action import esphome.codegen as cg from esphome.components.esp32 import VARIANT_ESP32P4, only_on_variant import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID, CONF_VOLTAGE +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.final_validate import full_config +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] @@ -22,7 +27,7 @@ CONF_PASSTHROUGH = "passthrough" adjusted_ids = set() -def validate_ldo_voltage(value): +def validate_ldo_voltage(value: Any) -> str | float: if isinstance(value, str) and value.lower() == CONF_PASSTHROUGH: return CONF_PASSTHROUGH value = cv.voltage(value) @@ -33,7 +38,7 @@ def validate_ldo_voltage(value): ) -def validate_ldo_config(config): +def validate_ldo_config(config: ConfigType) -> ConfigType: channel = config[CONF_CHANNEL] allow_internal = config[CONF_ALLOW_INTERNAL_CHANNEL] if allow_internal and channel not in CHANNELS_INTERNAL: @@ -77,7 +82,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(configs): +async def to_code(configs: list[ConfigType]) -> None: for config in configs: var = cg.new_Pvariable(config[CONF_ID], config[CONF_CHANNEL]) await cg.register_component(var, config) @@ -89,7 +94,7 @@ async def to_code(configs): cg.add(var.set_adjustable(config[CONF_ADJUSTABLE])) -def final_validate(configs): +def final_validate(configs: list[ConfigType]) -> None: for channel in CHANNELS: used = [config for config in configs if config[CONF_CHANNEL] == channel] if len(used) > 1: @@ -112,7 +117,7 @@ def final_validate(configs): FINAL_VALIDATE_SCHEMA = final_validate -def adjusted_ldo_id(value): +def adjusted_ldo_id(value: Any) -> ID: value = cv.use_id(EspLdo)(value) adjusted_ids.add(value) return value @@ -131,7 +136,12 @@ def adjusted_ldo_id(value): ), synchronous=True, ) -async def ldo_voltage_adjust_to_code(config, action_id, template_arg, args): +async def ldo_voltage_adjust_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: parent = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, parent) template_ = await cg.templatable(config[CONF_VOLTAGE], args, cg.float_) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index b1d332c1e5..64f87c167c 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -31,6 +31,7 @@ from esphome.const import ( ) from esphome.core import CORE, HexInt from esphome.final_validate import full_config +from esphome.types import ConfigType DEPENDENCIES = ["spi"] @@ -91,7 +92,7 @@ CONF_INVERT_DISPLAY = "invert_display" CONF_PIXEL_MODE = "pixel_mode" -def cmd(c, *args): +def cmd(c: int, *args: int) -> list[int]: """ Create a command sequence :param c: The command (8 bit) @@ -101,7 +102,7 @@ def cmd(c, *args): return [c, len(args)] + list(args) -def map_sequence(value): +def map_sequence(value: list[int]) -> list[int]: """ An initialisation sequence is a literal array of data bytes. The format is a repeated sequence of [CMD, ] @@ -111,7 +112,7 @@ def map_sequence(value): return cmd(*value) -def _validate(config): +def _validate(config: ConfigType) -> ConfigType: if ( config.get(CONF_COLOR_PALETTE) == "IMAGE_ADAPTIVE" and CONF_COLOR_PALETTE_IMAGES not in config @@ -196,7 +197,7 @@ CONFIG_SCHEMA = cv.All( ) -def final_validate(config): +def final_validate(config: ConfigType) -> None: global_config = full_config.get() # Ideally would calculate buffer size here, but that info is not available on the Python side needs_buffer = ( @@ -218,7 +219,7 @@ def final_validate(config): FINAL_VALIDATE_SCHEMA = final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: LOGGER.warning( "The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead." ) @@ -278,7 +279,7 @@ async def to_code(config): cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8_INDEXED)) from PIL import Image - def load_image(filename): + def load_image(filename: str) -> Image.Image: path = CORE.relative_config_path(filename) try: return Image.open(path) diff --git a/esphome/components/it8951/display.py b/esphome/components/it8951/display.py index bdc68b5257..57bf86c4c6 100644 --- a/esphome/components/it8951/display.py +++ b/esphome/components/it8951/display.py @@ -2,6 +2,9 @@ ESPHome configuration for the IT8951 e-paper controller. """ +from collections.abc import Callable +from typing import Any + from esphome import automation, core, pins import esphome.codegen as cg from esphome.components import display, spi @@ -33,8 +36,10 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, CONF_WIDTH, ) -from esphome.cpp_generator import RawExpression +from esphome.core import ID +from esphome.cpp_generator import MockObj, RawExpression, TemplateArgsType from esphome.final_validate import full_config +from esphome.types import ConfigType AUTO_LOAD = ["split_buffer"] DEPENDENCIES = ["spi"] @@ -97,16 +102,16 @@ class IT8951Model: models: dict[str, "IT8951Model"] = {} - def __init__(self, name: str, **defaults): + def __init__(self, name: str, **defaults: Any) -> None: name = name.upper() self.name = name self.defaults = defaults IT8951Model.models[name] = self - def get_default(self, key, fallback=None): + def get_default(self, key: str, fallback: Any = None) -> Any: return self.defaults.get(key, fallback) - def get_dimensions(self, config) -> tuple[int, int]: + def get_dimensions(self, config: ConfigType) -> tuple[int, int]: # If dimensions are in config, use them; otherwise fall back to model defaults. if CONF_DIMENSIONS in config: dimensions = config[CONF_DIMENSIONS] @@ -181,14 +186,16 @@ DIMENSION_SCHEMA = cv.Schema( ) -def _model_pin_option(model, key, schema): +def _model_pin_option( + model: IT8951Model, key: str, schema: Callable[[Any], Any] +) -> tuple[cv.Optional | cv.Required, Callable[[Any], Any]]: default = model.get_default(key) if default is None: return cv.Required(key), schema return cv.Optional(key, default=default), schema -def _model_schema(config): +def _model_schema(config: ConfigType) -> cv.Schema: model = IT8951Model.models[config[CONF_MODEL]] has_default_dimensions = ( model.get_default(CONF_WIDTH) is not None @@ -293,7 +300,7 @@ def _model_schema(config): return schema.extend(pin_extra) -def _customise_schema(config): +def _customise_schema(config: ConfigType) -> ConfigType: config = cv.Schema( { cv.Required(CONF_MODEL): cv.one_of( @@ -336,7 +343,7 @@ def _customise_schema(config): CONFIG_SCHEMA = _customise_schema -def _final_validate(config) -> None: +def _final_validate(config: ConfigType) -> None: # IT8951 reads from SPI (DevInfo, VCOM, register reads) so MISO is required. spi.final_validate_device_schema("it8951", require_miso=True, require_mosi=True)( config @@ -356,7 +363,7 @@ def _final_validate(config) -> None: FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: model = IT8951Model.models[config[CONF_MODEL]] width, height = model.get_dimensions(config) @@ -423,7 +430,12 @@ async def to_code(config): ), synchronous=True, ) -async def it8951_update_action_to_code(config, action_id, template_arg, args): +async def it8951_update_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: display_var = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, display_var) if mode := config.get(CONF_MODE): diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py index 3c7d78a27b..cd846877ae 100644 --- a/esphome/components/mapping/__init__.py +++ b/esphome/components/mapping/__init__.py @@ -1,5 +1,6 @@ from collections.abc import Callable import difflib +from typing import Any import esphome.codegen as cg from esphome.components.const import KEY_METADATA @@ -13,6 +14,7 @@ from esphome.cpp_generator import ( add_global, ) from esphome.loader import get_component +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] MULTI_CONF = True @@ -32,13 +34,16 @@ class IndexType: """ def __init__( - self, validator: Callable, data_type: MockObj, conversion: Callable = None + self, + validator: Callable, + data_type: MockObj, + conversion: Callable | None = None, ) -> None: self.validator = validator self.data_type = data_type self.conversion = conversion - async def convert_value(self, value): + async def convert_value(self, value: Any) -> Any: if self.conversion: return self.conversion(value) return await cg.get_variable(value) @@ -60,7 +65,7 @@ class MappingMetaData: self.to_ = to_ -def to_schema(value): +def to_schema(value: Any) -> str: """ Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name. :param value: @@ -82,7 +87,7 @@ BASE_SCHEMA = cv.Schema( ) -def get_object_type(to_) -> MockObjClass | None: +def get_object_type(to_: str) -> MockObjClass | None: """ Get the object type from a string. Possible formats: xxx The name of a component which defines INSTANCE_TYPE @@ -121,7 +126,7 @@ def add_metadata( get_all_mapping_metadata()[mapping_id.id] = MappingMetaData(from_, to_) -def map_schema(config): +def map_schema(config: ConfigType) -> ConfigType: config = BASE_SCHEMA(config) if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict): raise cv.Invalid("an entries dictionary is required for a mapping") @@ -163,7 +168,7 @@ def map_schema(config): CONFIG_SCHEMA = map_schema -async def to_code(config): +async def to_code(config: ConfigType) -> MockObj: varid = config[CONF_ID] metadata = get_mapping_metadata(varid.id) entries = { diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index 8c125a9606..b23982655a 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -53,6 +53,7 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.final_validate import full_config +from esphome.types import ConfigType from . import mipi_dsi_ns, models from .models import DsiDriverChip @@ -85,7 +86,7 @@ COLOR_DEPTHS = { } -def model_schema(config): +def model_schema(config: ConfigType) -> cv.All: model = MODELS[config[CONF_MODEL].upper()] transform = model.transform_schema() # CUSTOM model will need to provide a custom init sequence @@ -148,7 +149,7 @@ def model_schema(config): @model_schema_extractor(MODELS, model_schema) -def _config_schema(config): +def _config_schema(config: ConfigType) -> ConfigType: config = cv.Schema( { cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), @@ -175,7 +176,7 @@ def _config_schema(config): return config -def _final_validate(config) -> None: +def _final_validate(config: ConfigType) -> None: global_config = full_config.get() from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN @@ -189,7 +190,7 @@ CONFIG_SCHEMA = _config_schema FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: model = MODELS[config[CONF_MODEL].upper()] color_depth = COLOR_DEPTHS[get_color_depth(config)] pixel_mode = int(config[CONF_PIXEL_MODE].removesuffix("bit")) diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 897088a257..e23e19a000 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -1,5 +1,6 @@ import importlib import pkgutil +from typing import Any from esphome import pins import esphome.codegen as cg @@ -72,6 +73,7 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.final_validate import full_config +from esphome.types import ConfigType from . import models from .models import RgbDriverChip @@ -97,7 +99,7 @@ for module_info in pkgutil.iter_modules(models.__path__): MODELS = DriverChip.get_models() -def data_pin_validate(value): +def data_pin_validate(value: Any) -> ConfigType: """ It is safe to use strapping pins as RGB output data bits, as they are outputs only, and not initialised until after boot. @@ -112,14 +114,14 @@ def data_pin_validate(value): return DATA_PIN_SCHEMA(value) -def data_pin_set(length): +def data_pin_set(length: int) -> cv.All: return cv.All( [data_pin_validate], cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), ) -def model_schema(config): +def model_schema(config: ConfigType) -> cv.Schema: model = MODELS[config[CONF_MODEL].upper()] transform = model.transform_schema() # RPI model does not use an init sequence, indicates with empty list @@ -213,7 +215,7 @@ def model_schema(config): @model_schema_extractor(MODELS, model_schema) -def _config_schema(config): +def _config_schema(config: ConfigType) -> ConfigType: config = cv.Schema( { cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), @@ -248,7 +250,7 @@ def _config_schema(config): CONFIG_SCHEMA = _config_schema -def _final_validate(config) -> None: +def _final_validate(config: ConfigType) -> None: global_config = full_config.get() from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN @@ -265,7 +267,7 @@ def _final_validate(config) -> None: FINAL_VALIDATE_SCHEMA = _final_validate -async def to_code(config): +async def to_code(config: ConfigType) -> None: model = MODELS[config[CONF_MODEL].upper()] width, height, _offset_width, _offset_height, _pad_width, _pad_height = ( model.get_dimensions(config) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py index a20e9d1c01..cad5dc8e20 100644 --- a/esphome/components/mipi_rgb/models/st7701s.py +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -8,7 +8,7 @@ SDIR_CMD = 0xC7 class ST7701S(RgbDriverChip): # The ST7701s does not use the standard MADCTL bits for x/y mirroring - def add_madctl(self, sequence: list, config: dict): + def add_madctl(self, sequence: list, config: dict) -> int: transform = self.get_transform(config) madctl = 0x00 if config[CONF_COLOR_ORDER] == MODE_BGR: diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 246db237b1..e8b54da5c7 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -53,8 +53,9 @@ from esphome.const import ( CONF_TRANSFORM, CONF_WIDTH, ) -from esphome.cpp_generator import TemplateArguments +from esphome.cpp_generator import MockObjClass, TemplateArguments from esphome.final_validate import full_config +from esphome.types import ConfigType from . import CONF_BUS_MODE, CONF_SPI_16, DOMAIN, models @@ -110,7 +111,7 @@ DISPLAY_PIXEL_MODES = { } -def denominator(config): +def denominator(config: ConfigType) -> int: """ Calculate the best denominator for a buffer size fraction. The denominator should be a number between 2 and 16 that divides the display height evenly, @@ -132,7 +133,7 @@ def denominator(config): return next(x for x in range(2, 17) if frac >= 1 / x) -def model_schema(config): +def model_schema(config: ConfigType) -> cv.All | cv.Schema: model = MODELS[config[CONF_MODEL]] bus_mode = config[CONF_BUS_MODE] transform = model.transform_schema() @@ -238,7 +239,7 @@ def model_schema(config): @model_schema_extractor(MODELS, model_schema, extra={CONF_BUS_MODE: TYPE_SINGLE}) -def customise_schema(config): +def customise_schema(config: ConfigType) -> ConfigType: """ Create a customised config schema for a specific model and validate the configuration. :param config: The configuration dictionary to validate @@ -305,7 +306,7 @@ def customise_schema(config): CONFIG_SCHEMA = customise_schema -def _final_validate(config): +def _final_validate(config: ConfigType) -> None: global_config = full_config.get() model = MODELS[config[CONF_MODEL]] @@ -341,7 +342,7 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate -def get_instance(config): +def get_instance(config: ConfigType) -> tuple[MockObjClass, list]: """ Get the type of MipiSpi instance to create based on the configuration, and the template arguments. @@ -394,7 +395,7 @@ def get_instance(config): return MipiSpi, templateargs -async def to_code(config): +async def to_code(config: ConfigType) -> None: model = MODELS[config[CONF_MODEL]] var_id = config[CONF_ID] init_sequence = model.get_sequence(config, add_madctl=False, add_reset=True) diff --git a/esphome/components/online_image/image.py b/esphome/components/online_image/image.py index cb86f93e29..ae785d17f9 100644 --- a/esphome/components/online_image/image.py +++ b/esphome/components/online_image/image.py @@ -6,7 +6,8 @@ from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestCom from esphome.components.image import CONF_TRANSPARENCY, add_metadata import esphome.config_validation as cv from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL -from esphome.core import Lambda +from esphome.core import ID, Lambda +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType AUTO_LOAD = ["runtime_image"] @@ -89,7 +90,12 @@ RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id( RELEASE_IMAGE_SCHEMA, synchronous=True, ) -async def online_image_action_to_code(config, action_id, template_arg, args): +async def online_image_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/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index 7beb13ca31..c36d421a35 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -1,7 +1,9 @@ """ESPHome packet transport component.""" +from collections.abc import Callable, Iterator import hashlib import logging +from typing import Any import esphome.codegen as cg from esphome.components.binary_sensor import BinarySensor @@ -17,8 +19,9 @@ from esphome.const import ( CONF_PLATFORM, CONF_SENSORS, ) -from esphome.core import CORE -from esphome.cpp_generator import MockObjClass +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj, MockObjClass +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] AUTO_LOAD = ["xxtea"] @@ -43,7 +46,7 @@ CONF_TRANSPORT_ID = "transport_id" _LOGGER = logging.getLogger(__name__) -def sensor_validation(cls: MockObjClass): +def sensor_validation(cls: MockObjClass) -> Callable[[Any], Any]: return cv.maybe_simple_value( cv.Schema( { @@ -55,7 +58,7 @@ def sensor_validation(cls: MockObjClass): ) -def provider_name_validate(value): +def provider_name_validate(value: Any) -> str: value = cv.valid_name(value) if "_" in value: _LOGGER.warning( @@ -83,7 +86,7 @@ PROVIDER_SCHEMA = cv.Schema( ).extend(ENCRYPTION_SCHEMA) -def validate_(config): +def validate_(config: ConfigType) -> ConfigType: if CONF_ENCRYPTION in config: if CONF_SENSORS not in config and CONF_BINARY_SENSORS not in config: raise cv.Invalid("No sensors or binary sensors to encrypt") @@ -117,11 +120,11 @@ TRANSPORT_SCHEMA = ( ) -def transport_schema(cls): +def transport_schema(cls: MockObjClass) -> cv.Schema: return TRANSPORT_SCHEMA.extend({cv.GenerateID(): cv.declare_id(cls)}) -def get_sensors(transport_id): +def get_sensors(transport_id: ID) -> Iterator[ConfigType]: """Return the list of sensors for this platform.""" return ( sensor @@ -130,7 +133,7 @@ def get_sensors(transport_id): ) -def validate_packet_transport_sensor(config): +def validate_packet_transport_sensor(config: ConfigType) -> ConfigType: if CONF_NAME in config and CONF_INTERNAL not in config: raise cv.Invalid("Must provide internal: config when using name:") conf_sensors = CORE.data.setdefault(DOMAIN, {}).setdefault(CONF_SENSORS, []) @@ -138,7 +141,7 @@ def validate_packet_transport_sensor(config): return config -def packet_transport_sensor_schema(base_schema): +def packet_transport_sensor_schema(base_schema: cv.Schema) -> cv.Schema: return cv.All( base_schema.extend( { @@ -152,11 +155,11 @@ def packet_transport_sensor_schema(base_schema): ) -def hash_encryption_key(config: dict): +def hash_encryption_key(config: dict) -> list[int]: return list(hashlib.sha256(config[CONF_KEY].encode()).digest()) -async def register_packet_transport(var, config): +async def register_packet_transport(var: MockObj, config: ConfigType) -> set[str]: var = await cg.register_component(var, config) cg.add(var.set_rolling_code_enable(config[CONF_ROLLING_CODE_ENABLE])) cg.add(var.set_ping_pong_enable(config[CONF_PING_PONG_ENABLE])) @@ -203,7 +206,7 @@ async def register_packet_transport(var, config): return providers -async def new_packet_transport(config): +async def new_packet_transport(config: ConfigType) -> tuple[MockObj, set[str]]: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_platform_name(config[CONF_PLATFORM])) providers = await register_packet_transport(var, config) diff --git a/esphome/components/packet_transport/binary_sensor.py b/esphome/components/packet_transport/binary_sensor.py index 3291ff2c59..37c4688242 100644 --- a/esphome/components/packet_transport/binary_sensor.py +++ b/esphome/components/packet_transport/binary_sensor.py @@ -11,6 +11,7 @@ from esphome.const import ( ENTITY_CATEGORY_DIAGNOSTIC, ) import esphome.final_validate as fv +from esphome.types import ConfigType from . import ( CONF_ENCRYPTION, @@ -44,7 +45,7 @@ CONFIG_SCHEMA = cv.typed_schema( ) -def _final_validate(config) -> None: +def _final_validate(config: ConfigType) -> None: if config[CONF_TYPE] != CONF_STATUS: # Only run this validation if a status sensor is being configured return @@ -65,7 +66,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) comp = await cg.get_variable(config[CONF_TRANSPORT_ID]) if config[CONF_TYPE] == CONF_STATUS: diff --git a/esphome/components/packet_transport/sensor.py b/esphome/components/packet_transport/sensor.py index 15c0e33b30..018f1c3a9b 100644 --- a/esphome/components/packet_transport/sensor.py +++ b/esphome/components/packet_transport/sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components.sensor import new_sensor, sensor_schema from esphome.const import CONF_ID +from esphome.types import ConfigType from . import ( CONF_PROVIDER, @@ -12,7 +13,7 @@ from . import ( CONFIG_SCHEMA = packet_transport_sensor_schema(sensor_schema()) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await new_sensor(config) comp = await cg.get_variable(config[CONF_TRANSPORT_ID]) remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index f49a68bc3f..5272df2b55 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -11,6 +11,8 @@ from esphome.const import ( CONF_NUMBER, CONF_OUTPUT, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@hwstar", "@clydebarrow", "@bdraco"] AUTO_LOAD = ["gpio_expander"] @@ -40,7 +42,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_pin_count(config[CONF_PIN_COUNT])) await cg.register_component(var, config) @@ -49,7 +51,7 @@ async def to_code(config): cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) -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]: @@ -69,7 +71,9 @@ PCA9554_PIN_SCHEMA = pins.gpio_base_schema( ) -def pca9554_pin_final_validate(pin_config, parent_config): +def pca9554_pin_final_validate( + pin_config: ConfigType, parent_config: ConfigType +) -> None: count = parent_config[CONF_PIN_COUNT] if pin_config[CONF_NUMBER] >= count: raise cv.Invalid(f"Pin number must be in range 0-{count - 1}") @@ -78,7 +82,7 @@ def pca9554_pin_final_validate(pin_config, parent_config): @pins.PIN_SCHEMA_REGISTRY.register( CONF_PCA9554, PCA9554_PIN_SCHEMA, pca9554_pin_final_validate ) -async def pca9554_pin_to_code(config): +async def pca9554_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_PCA9554]) diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py index 48cd72ecdf..dce1a95687 100644 --- a/esphome/components/qspi_dbi/display.py +++ b/esphome/components/qspi_dbi/display.py @@ -1,4 +1,5 @@ import logging +from typing import Any from esphome import pins import esphome.codegen as cg @@ -26,6 +27,7 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.core import TimePeriod +from esphome.types import ConfigType from . import CONF_DRAW_FROM_ORIGIN from .models import DriverChip @@ -49,14 +51,14 @@ DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema DELAY_FLAG = 0xFF -def validate_dimension(value): +def validate_dimension(value: Any) -> int: value = cv.positive_int(value) if value % 2 != 0: raise cv.Invalid("Width/height/offset must be divisible by 2") return value -def map_sequence(value): +def map_sequence(value: Any) -> list[int]: """ The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred from the length of the sequence and should not be explicit. @@ -74,14 +76,14 @@ def map_sequence(value): return [value[0], len(params)] + list(params) -def _validate(config): +def _validate(config: ConfigType) -> ConfigType: chip = DriverChip.chips[config[CONF_MODEL]] if not chip.initsequence and CONF_INIT_SEQUENCE not in config: raise cv.Invalid(f"{chip.name} model requires init_sequence") return config -def power_of_two(value): +def power_of_two(value: Any) -> int: value = cv.int_range(1, 128)(value) if value & (value - 1) != 0: raise cv.Invalid("value must be a power of two") @@ -122,11 +124,11 @@ BASE_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( ) -def model_property(name, defaults, fallback): +def model_property(name: str, defaults: dict[str, Any], fallback: Any) -> cv.Optional: return cv.Optional(name, default=defaults.get(name, fallback)) -def model_schema(defaults): +def model_schema(defaults: dict[str, Any]) -> cv.Schema: transform = cv.Schema( { cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, @@ -162,7 +164,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: LOGGER.warning( "The 'qspi_dbi' component is deprecated, it is recommended to use 'mipi_spi' instead." ) diff --git a/esphome/components/qspi_dbi/models.py b/esphome/components/qspi_dbi/models.py index 8ce592e0cf..7611279509 100644 --- a/esphome/components/qspi_dbi/models.py +++ b/esphome/components/qspi_dbi/models.py @@ -1,4 +1,6 @@ # Commands +from typing import Any + from esphome.components.const import CONF_DRAW_ROUNDING from esphome.const import CONF_INVERT_COLORS, CONF_SWAP_XY @@ -26,16 +28,16 @@ PAGESEL = 0xFE class DriverChip: - chips = {} + chips: dict[str, "DriverChip"] = {} - def __init__(self, name: str, defaults=None): + def __init__(self, name: str, defaults: dict[str, Any] | None = None) -> None: name = name.upper() self.name = name self.chips[name] = self self.initsequence = [] self.defaults = defaults or {} - def cmd(self, c, *args): + def cmd(self, c: int, *args: int) -> None: """ Add a command sequence to the init sequence :param c: The command (8 bit) @@ -43,7 +45,7 @@ class DriverChip: """ self.initsequence.extend([c, len(args)] + list(args)) - def delay(self, ms): + def delay(self, ms: int) -> None: self.initsequence.extend([ms, 0xFF]) diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index 314852832c..1ca29a3259 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.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 @@ -38,6 +40,7 @@ from esphome.const import ( CONF_VSYNC_PIN, CONF_WIDTH, ) +from esphome.types import ConfigType DEPENDENCIES = ["esp32"] LOGGER = logging.getLogger(__name__) @@ -53,7 +56,7 @@ COLOR_ORDERS = { DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema -def data_pin_validate(value): +def data_pin_validate(value: Any) -> ConfigType: """ It is safe to use strapping pins as RGB output data bits, as they are outputs only, and not initialised until after boot. @@ -68,7 +71,7 @@ def data_pin_validate(value): return DATA_PIN_SCHEMA(value) -def data_pin_set(length): +def data_pin_set(length: int) -> Callable[[Any], Any]: return cv.All( [data_pin_validate], cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), @@ -128,7 +131,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: LOGGER.warning( "The 'rpi_dpi_rgb' component is deprecated, it is recommended to use 'mipi_rgb' instead." ) diff --git a/esphome/components/sdl/binary_sensor.py b/esphome/components/sdl/binary_sensor.py index e19a488800..0fdda25ed3 100644 --- a/esphome/components/sdl/binary_sensor.py +++ b/esphome/components/sdl/binary_sensor.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import CONF_KEY from esphome.core import Lambda from esphome.cpp_generator import ExpressionStatement, RawExpression +from esphome.types import ConfigType from .display import CONF_SDL_ID, Sdl @@ -275,7 +276,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) parent = await cg.get_variable(config[CONF_SDL_ID]) listener = Lambda( diff --git a/esphome/components/sdl/display.py b/esphome/components/sdl/display.py index 57266f33e2..5ced2edf5a 100644 --- a/esphome/components/sdl/display.py +++ b/esphome/components/sdl/display.py @@ -1,4 +1,6 @@ +from collections.abc import Callable import subprocess +from typing import Any import esphome.codegen as cg from esphome.components import display @@ -14,6 +16,7 @@ from esphome.const import ( CONF_Y, PLATFORM_HOST, ) +from esphome.types import ConfigType sdl_ns = cg.esphome_ns.namespace("sdl") Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component) @@ -35,7 +38,7 @@ WINDOW_OPTIONS = ( SDL_WINDOWPOS_CENTERED_MASK = 0x2FFF0000 -def get_sdl_options(value): +def get_sdl_options(value: str) -> str: if value != "": return value try: @@ -46,7 +49,7 @@ def get_sdl_options(value): raise cv.Invalid("Unable to run sdl2-config - have you installed sdl2?") from e -def get_window_options(): +def get_window_options() -> dict[cv.Optional, Callable[[Any], Any]]: return {cv.Optional(option, default=False): cv.boolean for option in WINDOW_OPTIONS} @@ -100,7 +103,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: for option in config[CONF_SDL_OPTIONS].split(): cg.add_build_flag(option) cg.add_build_flag("-DSDL_BYTEORDER=4321") diff --git a/esphome/components/sdl/touchscreen/__init__.py b/esphome/components/sdl/touchscreen/__init__.py index 9f84f91c72..d7af8da403 100644 --- a/esphome/components/sdl/touchscreen/__init__.py +++ b/esphome/components/sdl/touchscreen/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import touchscreen import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType from ..display import CONF_SDL_ID, Sdl, sdl_ns @@ -16,7 +17,7 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_SDL_ID]) await touchscreen.register_touchscreen(var, config) diff --git a/esphome/components/seeed_mr24hpc1/__init__.py b/esphome/components/seeed_mr24hpc1/__init__.py index f71239d18c..56630f18f4 100644 --- a/esphome/components/seeed_mr24hpc1/__init__.py +++ b/esphome/components/seeed_mr24hpc1/__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 DEPENDENCIES = ["uart"] # is the code owner of the relevant code base @@ -43,7 +44,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( # The async def keyword is used to define a concurrent function. # Concurrent functions are special functions designed to work with Python's asyncio library to support asynchronous I/O operations. -async def to_code(config): +async def to_code(config: ConfigType) -> None: # This line of code creates a new Pvariable (a Python object representing a C++ variable) with the variable's ID taken from the configuration. var = cg.new_Pvariable(config[CONF_ID]) # This line of code registers the newly created Pvariable as a component so that ESPHome can manage it at runtime. diff --git a/esphome/components/seeed_mr24hpc1/binary_sensor.py b/esphome/components/seeed_mr24hpc1/binary_sensor.py index 26de1e4ac1..121eb2b4b3 100644 --- a/esphome/components/seeed_mr24hpc1/binary_sensor.py +++ b/esphome/components/seeed_mr24hpc1/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_HAS_TARGET, DEVICE_CLASS_OCCUPANCY +from esphome.types import ConfigType from . import CONF_MR24HPC1_ID, MR24HPC1Component @@ -13,7 +14,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) if has_target_config := config.get(CONF_HAS_TARGET): sens = await binary_sensor.new_binary_sensor(has_target_config) diff --git a/esphome/components/seeed_mr24hpc1/button/__init__.py b/esphome/components/seeed_mr24hpc1/button/__init__.py index 1e68d7e071..3386118bcf 100644 --- a/esphome/components/seeed_mr24hpc1/button/__init__.py +++ b/esphome/components/seeed_mr24hpc1/button/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns @@ -31,7 +32,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) if restart_config := config.get(CONF_RESTART): b = await button.new_button(restart_config) diff --git a/esphome/components/seeed_mr24hpc1/number/__init__.py b/esphome/components/seeed_mr24hpc1/number/__init__.py index 4de3654e39..d01618b0e6 100644 --- a/esphome/components/seeed_mr24hpc1/number/__init__.py +++ b/esphome/components/seeed_mr24hpc1/number/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv from esphome.const import CONF_SENSITIVITY, ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType from .. import CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns @@ -63,7 +64,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) if sensitivity_config := config.get(CONF_SENSITIVITY): n = await number.new_number( diff --git a/esphome/components/seeed_mr24hpc1/select/__init__.py b/esphome/components/seeed_mr24hpc1/select/__init__.py index 14854f0795..9d46dee6f6 100644 --- a/esphome/components/seeed_mr24hpc1/select/__init__.py +++ b/esphome/components/seeed_mr24hpc1/select/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import select import esphome.config_validation as cv from esphome.const import ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType from .. import CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns @@ -38,7 +39,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) if scenemode_config := config.get(CONF_SCENE_MODE): s = await select.new_select( diff --git a/esphome/components/seeed_mr24hpc1/sensor.py b/esphome/components/seeed_mr24hpc1/sensor.py index ca15fd5be6..36ee2c0087 100644 --- a/esphome/components/seeed_mr24hpc1/sensor.py +++ b/esphome/components/seeed_mr24hpc1/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_METER, ) +from esphome.types import ConfigType from . import CONF_MR24HPC1_ID, MR24HPC1Component @@ -60,7 +61,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) if custompresenceofdetection_config := config.get( CONF_CUSTOM_PRESENCE_OF_DETECTION diff --git a/esphome/components/seeed_mr24hpc1/switch/__init__.py b/esphome/components/seeed_mr24hpc1/switch/__init__.py index 741e7de3ca..f9588d783e 100644 --- a/esphome/components/seeed_mr24hpc1/switch/__init__.py +++ b/esphome/components/seeed_mr24hpc1/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 CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns @@ -22,7 +23,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) if underlying_open_function_config := config.get(CONF_UNDERLYING_OPEN_FUNCTION): s = await switch.new_switch(underlying_open_function_config) diff --git a/esphome/components/seeed_mr24hpc1/text_sensor.py b/esphome/components/seeed_mr24hpc1/text_sensor.py index fadd9c6dbc..8f284cb20a 100644 --- a/esphome/components/seeed_mr24hpc1/text_sensor.py +++ b/esphome/components/seeed_mr24hpc1/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 CONF_MR24HPC1_ID, MR24HPC1Component @@ -47,7 +48,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr24hpc1_component = await cg.get_variable(config[CONF_MR24HPC1_ID]) if heartbeat_config := config.get(CONF_HEART_BEAT): sens = await text_sensor.new_text_sensor(heartbeat_config) diff --git a/esphome/components/seeed_mr60bha2/__init__.py b/esphome/components/seeed_mr60bha2/__init__.py index 87bdbbd003..6bf8657af9 100644 --- a/esphome/components/seeed_mr60bha2/__init__.py +++ b/esphome/components/seeed_mr60bha2/__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 = ["@limengdu"] DEPENDENCIES = ["uart"] @@ -35,7 +36,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/seeed_mr60bha2/binary_sensor.py b/esphome/components/seeed_mr60bha2/binary_sensor.py index 99940ebf6d..4130bac224 100644 --- a/esphome/components/seeed_mr60bha2/binary_sensor.py +++ b/esphome/components/seeed_mr60bha2/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_HAS_TARGET, DEVICE_CLASS_OCCUPANCY +from esphome.types import ConfigType from . import CONF_MR60BHA2_ID, MR60BHA2Component @@ -15,7 +16,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr60bha2_component = await cg.get_variable(config[CONF_MR60BHA2_ID]) if has_target_config := config.get(CONF_HAS_TARGET): diff --git a/esphome/components/seeed_mr60bha2/sensor.py b/esphome/components/seeed_mr60bha2/sensor.py index d7f667d862..a2f41a90a8 100644 --- a/esphome/components/seeed_mr60bha2/sensor.py +++ b/esphome/components/seeed_mr60bha2/sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( UNIT_BEATS_PER_MINUTE, UNIT_CENTIMETER, ) +from esphome.types import ConfigType from . import CONF_MR60BHA2_ID, MR60BHA2Component @@ -49,7 +50,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr60bha2_component = await cg.get_variable(config[CONF_MR60BHA2_ID]) if breath_rate_config := config.get(CONF_BREATH_RATE): sens = await sensor.new_sensor(breath_rate_config) diff --git a/esphome/components/seeed_mr60fda2/__init__.py b/esphome/components/seeed_mr60fda2/__init__.py index e79134deec..de6e8ad57b 100644 --- a/esphome/components/seeed_mr60fda2/__init__.py +++ b/esphome/components/seeed_mr60fda2/__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 = ["@limengdu"] DEPENDENCIES = ["uart"] @@ -35,7 +36,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/seeed_mr60fda2/binary_sensor.py b/esphome/components/seeed_mr60fda2/binary_sensor.py index 2860ac0100..63bd02acd0 100644 --- a/esphome/components/seeed_mr60fda2/binary_sensor.py +++ b/esphome/components/seeed_mr60fda2/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_OCCUPANCY, DEVICE_CLASS_SAFETY +from esphome.types import ConfigType from . import CONF_MR60FDA2_ID, MR60FDA2Component @@ -21,7 +22,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID]) if people_exist_config := config.get(CONF_PEOPLE_EXIST): diff --git a/esphome/components/seeed_mr60fda2/button/__init__.py b/esphome/components/seeed_mr60fda2/button/__init__.py index 8236248b8c..82f0fc9aea 100644 --- a/esphome/components/seeed_mr60fda2/button/__init__.py +++ b/esphome/components/seeed_mr60fda2/button/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_NONE, ) +from esphome.types import ConfigType from .. import CONF_MR60FDA2_ID, MR60FDA2Component, mr60fda2_ns @@ -33,7 +34,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID]) if get_radar_parameters_config := config.get(CONF_GET_RADAR_PARAMETERS): b = await button.new_button(get_radar_parameters_config) diff --git a/esphome/components/seeed_mr60fda2/select/__init__.py b/esphome/components/seeed_mr60fda2/select/__init__.py index 2fea150cd2..6d8864455f 100644 --- a/esphome/components/seeed_mr60fda2/select/__init__.py +++ b/esphome/components/seeed_mr60fda2/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_SENSITIVITY, ENTITY_CATEGORY_CONFIG, ICON_ACCELERATION_Z +from esphome.types import ConfigType from .. import CONF_MR60FDA2_ID, MR60FDA2Component, mr60fda2_ns @@ -33,7 +34,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID]) if install_height_config := config.get(CONF_INSTALL_HEIGHT): s = await select.new_select( diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index 7f6492812f..16d7ef8e86 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import pins import esphome.codegen as cg from esphome.components import display, spi @@ -41,6 +43,7 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.core import TimePeriod +from esphome.types import ConfigType from .init_sequences import ST7701S_INITS, cmd @@ -58,7 +61,7 @@ COLOR_ORDERS = { DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema -def data_pin_validate(value): +def data_pin_validate(value: Any) -> ConfigType: """ It is safe to use strapping pins as RGB output data bits, as they are outputs only, and not initialised until after boot. @@ -73,14 +76,14 @@ def data_pin_validate(value): return DATA_PIN_SCHEMA(value) -def data_pin_set(length): +def data_pin_set(length: int) -> cv.Schema: return cv.All( [data_pin_validate], cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), ) -def map_sequence(value): +def map_sequence(value: Any) -> list: """ An initialisation sequence can be selected from one of the pre-defined sequences in init_sequences.py, or can be a literal array of data bytes. @@ -170,7 +173,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( ) -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) await spi.register_spi_device(var, config, write_only=True) diff --git a/esphome/components/st7701s/init_sequences.py b/esphome/components/st7701s/init_sequences.py index 4786731c78..a67f3f63fb 100644 --- a/esphome/components/st7701s/init_sequences.py +++ b/esphome/components/st7701s/init_sequences.py @@ -1,7 +1,7 @@ # These are initialisation sequences for ST7701S displays. The contents are somewhat arcane. -def cmd(c, *args): +def cmd(c: int, *args: int) -> list[int]: """ Create a command sequence :param c: The command (8 bit) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 5dfd188f0f..a782d875b9 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -1,3 +1,6 @@ +from collections.abc import Callable +from typing import Any, NoReturn + from esphome import automation from esphome.automation import Trigger import esphome.codegen as cg @@ -13,7 +16,7 @@ from esphome.components.packet_transport import ( import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.core import ID -from esphome.cpp_generator import MockObj +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] @@ -45,8 +48,8 @@ UDP_SCHEMA = cv.Schema( ) -def is_relocated(option): - def validator(value): +def is_relocated(option: str) -> Callable[[Any], NoReturn]: + def validator(value: Any) -> NoReturn: raise cv.Invalid( f"The '{option}' option should now be configured in the 'packet_transport' component" ) @@ -109,13 +112,13 @@ CONFIG_SCHEMA = cv.All( ) -async def register_udp_client(var, config): +async def register_udp_client(var: MockObj, config: ConfigType) -> MockObj: udp_var = await cg.get_variable(config[CONF_UDP_ID]) cg.add(var.set_parent(udp_var)) return udp_var -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_UDP") cg.add_global(udp_ns.using) var = cg.new_Pvariable(config[CONF_ID]) @@ -147,7 +150,7 @@ async def to_code(config): cg.add(var.set_should_listen()) -def validate_raw_data(value): +def validate_raw_data(value: Any) -> bytes | list[int]: if isinstance(value, str): return value.encode("utf-8") if isinstance(value, str): @@ -171,7 +174,12 @@ def validate_raw_data(value): ), synchronous=True, ) -async def udp_write_to_code(config, action_id, template_arg, args): +async def udp_write_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) udp_var = await cg.get_variable(config[CONF_ID]) await cg.register_parented(var, udp_var) diff --git a/esphome/components/udp/packet_transport/__init__.py b/esphome/components/udp/packet_transport/__init__.py index e725276717..f2c15289a9 100644 --- a/esphome/components/udp/packet_transport/__init__.py +++ b/esphome/components/udp/packet_transport/__init__.py @@ -7,6 +7,7 @@ from esphome.components.packet_transport import ( ) from esphome.const import CONF_BINARY_SENSORS, CONF_ENCRYPTION, CONF_SENSORS from esphome.cpp_types import PollingComponent +from esphome.types import ConfigType from .. import UDP_SCHEMA, register_udp_client, udp_ns @@ -15,7 +16,7 @@ UDPTransport = udp_ns.class_("UDPTransport", PacketTransport, PollingComponent) CONFIG_SCHEMA = transport_schema(UDPTransport).extend(UDP_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var, providers = await new_packet_transport(config) udp_var = await register_udp_client(var, config) if CONF_ENCRYPTION in config or providers: diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index a921b6fbf0..edbf75f70f 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -18,6 +18,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.cpp_types import Component +from esphome.types import ConfigType AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] CODEOWNERS = ["@clydebarrow"] @@ -48,14 +49,14 @@ DEFAULT_BAUD_RATE = 9600 class Type: def __init__( self, - name, - vid, - pid, - cls, - max_channels=1, - baud_rate_required=True, - max_baud=1_000_000, - ): + name: str, + vid: int, + pid: int, + cls: str | None, + max_channels: int = 1, + baud_rate_required: bool = True, + max_baud: int = 1_000_000, + ) -> None: self.name = name cls = cls or name self.vid = vid @@ -156,7 +157,7 @@ CONFIG_SCHEMA = cv.ensure_list( ) -async def to_code(config): +async def to_code(config: list[ConfigType]) -> None: # The output chunk pool/queue are compile-time-sized templates shared by all # USBUartChannel instances, so use the largest buffer_size across every channel # of every device. Add one extra slot because LockFreeQueue is a ring From fbe4b39a165d7e2bd54a448c020f4640d2809dbe Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:58:58 +1200 Subject: [PATCH 014/155] [core] Add type annotations to component Python (3/11) (#18340) --- .../components/copy/binary_sensor/__init__.py | 3 +- esphome/components/copy/button/__init__.py | 3 +- esphome/components/copy/cover/__init__.py | 3 +- esphome/components/copy/fan/__init__.py | 3 +- esphome/components/copy/lock/__init__.py | 3 +- esphome/components/copy/number/__init__.py | 3 +- esphome/components/copy/select/__init__.py | 3 +- esphome/components/copy/sensor/__init__.py | 3 +- esphome/components/copy/switch/__init__.py | 3 +- esphome/components/copy/text/__init__.py | 3 +- .../components/copy/text_sensor/__init__.py | 3 +- esphome/components/integration/sensor.py | 23 +++++++++--- esphome/components/key_collector/__init__.py | 19 +++++++--- .../key_collector/text_sensor/__init__.py | 4 +-- esphome/components/ledc/output.py | 20 ++++++++--- esphome/components/matrix_keypad/__init__.py | 5 +-- .../matrix_keypad/binary_sensor/__init__.py | 5 +-- esphome/components/pid/climate.py | 26 +++++++++++--- esphome/components/pid/sensor/__init__.py | 3 +- esphome/components/rp2/__init__.py | 15 ++++---- esphome/components/rp2/generate_boards.py | 2 +- esphome/components/rp2/gpio.py | 16 +++++---- esphome/components/rp2040_pwm/output.py | 12 +++++-- esphome/components/sn74hc165/__init__.py | 12 ++++--- esphome/components/sun/__init__.py | 24 ++++++++++--- esphome/components/sun/sensor/__init__.py | 3 +- .../components/sun/text_sensor/__init__.py | 5 +-- esphome/components/touchscreen/__init__.py | 22 ++++++++---- .../touchscreen/binary_sensor/__init__.py | 5 +-- esphome/components/update/__init__.py | 34 ++++++++++++------ esphome/components/vbus/__init__.py | 3 +- .../components/vbus/binary_sensor/__init__.py | 3 +- esphome/components/vbus/sensor/__init__.py | 3 +- .../components/voice_assistant/__init__.py | 35 +++++++++++++++---- .../components/xiaomi_rtcgq02lm/__init__.py | 3 +- .../xiaomi_rtcgq02lm/binary_sensor.py | 3 +- esphome/components/xiaomi_rtcgq02lm/sensor.py | 3 +- 37 files changed, 246 insertions(+), 95 deletions(-) diff --git a/esphome/components/copy/binary_sensor/__init__.py b/esphome/components/copy/binary_sensor/__init__.py index 840200409f..cc8492f21e 100644 --- a/esphome/components/copy/binary_sensor/__init__.py +++ b/esphome/components/copy/binary_sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_SOURCE_ID, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -33,7 +34,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/button/__init__.py b/esphome/components/copy/button/__init__.py index 8028d6a217..768131bbe5 100644 --- a/esphome/components/copy/button/__init__.py +++ b/esphome/components/copy/button/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_SOURCE_ID, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -32,7 +33,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await button.register_button(var, config) await cg.register_component(var, config) diff --git a/esphome/components/copy/cover/__init__.py b/esphome/components/copy/cover/__init__.py index ff5bef5668..d23602fa74 100644 --- a/esphome/components/copy/cover/__init__.py +++ b/esphome/components/copy/cover/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_SOURCE_ID, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -31,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await cover.new_cover(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/fan/__init__.py b/esphome/components/copy/fan/__init__.py index a208e5f80a..ffa414c5f2 100644 --- a/esphome/components/copy/fan/__init__.py +++ b/esphome/components/copy/fan/__init__.py @@ -3,6 +3,7 @@ from esphome.components import fan import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -25,7 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await fan.new_fan(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/lock/__init__.py b/esphome/components/copy/lock/__init__.py index 46bc08273e..8d9c4b6eca 100644 --- a/esphome/components/copy/lock/__init__.py +++ b/esphome/components/copy/lock/__init__.py @@ -3,6 +3,7 @@ from esphome.components import lock import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -25,7 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await lock.new_lock(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/number/__init__.py b/esphome/components/copy/number/__init__.py index 3e2bbf2aae..9659a605f9 100644 --- a/esphome/components/copy/number/__init__.py +++ b/esphome/components/copy/number/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -33,7 +34,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await number.new_number(config, min_value=0, max_value=0, step=0) await cg.register_component(var, config) diff --git a/esphome/components/copy/select/__init__.py b/esphome/components/copy/select/__init__.py index d7ddc52c44..97776b1edd 100644 --- a/esphome/components/copy/select/__init__.py +++ b/esphome/components/copy/select/__init__.py @@ -3,6 +3,7 @@ from esphome.components import select import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -25,7 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await select.register_select(var, config, options=[]) await cg.register_component(var, config) diff --git a/esphome/components/copy/sensor/__init__.py b/esphome/components/copy/sensor/__init__.py index 57ca06aca7..5468798047 100644 --- a/esphome/components/copy/sensor/__init__.py +++ b/esphome/components/copy/sensor/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -37,7 +38,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/switch/__init__.py b/esphome/components/copy/switch/__init__.py index ee27e38c5f..0e714540f9 100644 --- a/esphome/components/copy/switch/__init__.py +++ b/esphome/components/copy/switch/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_SOURCE_ID, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -31,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await switch.new_switch(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/text/__init__.py b/esphome/components/copy/text/__init__.py index f1ca404b7b..59fdce6c96 100644 --- a/esphome/components/copy/text/__init__.py +++ b/esphome/components/copy/text/__init__.py @@ -3,6 +3,7 @@ from esphome.components import text import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_MODE, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -26,7 +27,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await text.new_text(config) await cg.register_component(var, config) diff --git a/esphome/components/copy/text_sensor/__init__.py b/esphome/components/copy/text_sensor/__init__.py index 7b38ff1a64..146beae5ea 100644 --- a/esphome/components/copy/text_sensor/__init__.py +++ b/esphome/components/copy/text_sensor/__init__.py @@ -3,6 +3,7 @@ from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType from .. import copy_ns @@ -25,7 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 8d784df672..82e8ba8df8 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -11,7 +11,10 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_VALUE, ) +from esphome.core import ID from esphome.core.entity_helpers import inherit_property_from +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType integration_ns = cg.esphome_ns.namespace("integration") IntegrationSensor = integration_ns.class_( @@ -39,14 +42,14 @@ CONF_TIME_UNIT = "time_unit" CONF_INTEGRATION_METHOD = "integration_method" -def inherit_unit_of_measurement(uom, config): +def inherit_unit_of_measurement(uom: str, config: ConfigType) -> str: suffix = config[CONF_TIME_UNIT] if uom.endswith("/" + suffix): return uom[0 : -len("/" + suffix)] return uom + suffix -def inherit_accuracy_decimals(decimals, config): +def inherit_accuracy_decimals(decimals: int, config: ConfigType) -> int: return decimals + 2 @@ -90,7 +93,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -113,7 +116,12 @@ async def to_code(config): ), synchronous=True, ) -async def sensor_integration_reset_to_code(config, action_id, template_arg, args): +async def sensor_integration_reset_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -130,7 +138,12 @@ async def sensor_integration_reset_to_code(config, action_id, template_arg, args ), synchronous=True, ) -async def sensor_integration_set_value_to_code(config, action_id, template_arg, args): +async def sensor_integration_set_value_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_VALUE], args, cg.float_) diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index 1f4519df2d..bf47b6df88 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -15,8 +15,9 @@ from esphome.const import ( CONF_TIMEOUT, CONF_TRIGGER_ID, ) +from esphome.core import ID from esphome.cpp_generator import MockObj, literal -from esphome.types import TemplateArgsType +from esphome.types import ConfigType, TemplateArgsType CODEOWNERS = ["@ssieb"] @@ -90,7 +91,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for source_conf in config.get(CONF_SOURCE_ID, ()): @@ -144,7 +145,12 @@ async def to_code(config): ), synchronous=True, ) -async def enable_to_code(config, action_id, template_arg, args): +async def enable_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -160,7 +166,12 @@ async def enable_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def disable_to_code(config, action_id, template_arg, args): +async def disable_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/key_collector/text_sensor/__init__.py b/esphome/components/key_collector/text_sensor/__init__.py index 1676cf7bdf..e32d15df2e 100644 --- a/esphome/components/key_collector/text_sensor/__init__.py +++ b/esphome/components/key_collector/text_sensor/__init__.py @@ -4,7 +4,7 @@ from esphome.components.text_sensor import TextSensor import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.cpp_generator import literal -from esphome.types import TemplateArgsType +from esphome.types import ConfigType, TemplateArgsType from .. import CONF_ON_RESULT, CONF_SOURCE_ID, TRIGGER_TYPES, KeyCollector @@ -15,7 +15,7 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema(TextSensor).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_SOURCE_ID]) var = cg.new_Pvariable(config[CONF_ID]) await text_sensor.register_text_sensor(var, config) diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 95df1fba23..637e607b6d 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation, pins import esphome.codegen as cg from esphome.components import output @@ -9,20 +11,23 @@ from esphome.const import ( CONF_PHASE_ANGLE, CONF_PIN, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["esp32"] -def calc_max_frequency(bit_depth): +def calc_max_frequency(bit_depth: int) -> float: return 80e6 / (2**bit_depth) -def calc_min_frequency(bit_depth): +def calc_min_frequency(bit_depth: int) -> float: max_div_num = ((2**20) - 1) / 256.0 return 80e6 / (max_div_num * (2**bit_depth)) -def validate_frequency(value): +def validate_frequency(value: Any) -> float: value = cv.frequency(value) min_freq = calc_min_frequency(20) max_freq = calc_max_frequency(1) @@ -56,7 +61,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) @@ -79,7 +84,12 @@ async def to_code(config): ), synchronous=True, ) -async def ledc_set_frequency_to_code(config, action_id, template_arg, args): +async def ledc_set_frequency_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_) diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 868b149211..47cf4793b1 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -4,6 +4,7 @@ from esphome.components import key_provider from esphome.components.const import CONF_ROWS import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID +from esphome.types import ConfigType CODEOWNERS = ["@ssieb"] @@ -27,7 +28,7 @@ CONF_HAS_DIODES = "has_diodes" CONF_HAS_PULLDOWNS = "has_pulldowns" -def check_keys(obj): +def check_keys(obj: ConfigType) -> ConfigType: if CONF_KEYS in obj and len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len( obj[CONF_COLUMNS] ): @@ -62,7 +63,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) row_pins = [] diff --git a/esphome/components/matrix_keypad/binary_sensor/__init__.py b/esphome/components/matrix_keypad/binary_sensor/__init__.py index 8e63ed43ce..6c6e0aad73 100644 --- a/esphome/components/matrix_keypad/binary_sensor/__init__.py +++ b/esphome/components/matrix_keypad/binary_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import CONF_COL, CONF_ID, CONF_KEY, CONF_ROW +from esphome.types import ConfigType from .. import CONF_KEYPAD_ID, MatrixKeypad, matrix_keypad_ns @@ -12,7 +13,7 @@ MatrixKeypadBinarySensor = matrix_keypad_ns.class_( ) -def check_button(obj): +def check_button(obj: ConfigType) -> ConfigType: if CONF_ROW in obj or CONF_COL in obj: if CONF_KEY in obj: raise cv.Invalid("You can't provide both a key and a position") @@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: if CONF_KEY in config: var = cg.new_Pvariable(config[CONF_ID], config[CONF_KEY][0]) else: diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 3e4ff754c9..4945547f2e 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -3,6 +3,9 @@ import esphome.codegen as cg from esphome.components import climate, output, sensor import esphome.config_validation as cv from esphome.const import CONF_HUMIDITY_SENSOR, CONF_ID, CONF_SENSOR +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType pid_ns = cg.esphome_ns.namespace("pid") PIDClimate = pid_ns.class_("PIDClimate", climate.Climate, cg.Component) @@ -82,7 +85,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await climate.new_climate(config) await cg.register_component(var, config) @@ -141,7 +144,12 @@ async def to_code(config): ), synchronous=True, ) -async def pid_reset_integral_term(config, action_id, template_arg, args): +async def pid_reset_integral_term( + 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) @@ -163,7 +171,12 @@ async def pid_reset_integral_term(config, action_id, template_arg, args): ), 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) cg.add(var.set_noiseband(config[CONF_NOISEBAND])) @@ -185,7 +198,12 @@ async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def set_control_parameters(config, action_id, template_arg, args): +async def set_control_parameters( + 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/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py index d26e88e38a..94d641de47 100644 --- a/esphome/components/pid/sensor/__init__.py +++ b/esphome/components/pid/sensor/__init__.py @@ -3,6 +3,7 @@ from esphome.components import sensor from esphome.components.const import CONF_CLIMATE_ID import esphome.config_validation as cv from esphome.const import CONF_TYPE, ICON_GAUGE, STATE_CLASS_MEASUREMENT, UNIT_PERCENT +from esphome.types import ConfigType from ..climate import PIDClimate, pid_ns @@ -40,7 +41,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_CLIMATE_ID]) var = await sensor.new_sensor(config) await cg.register_component(var, config) diff --git a/esphome/components/rp2/__init__.py b/esphome/components/rp2/__init__.py index 60fcd4f8b0..ed975ec01a 100644 --- a/esphome/components/rp2/__init__.py +++ b/esphome/components/rp2/__init__.py @@ -34,6 +34,7 @@ from esphome.core import ( from esphome.core.config import BOARD_MAX_LENGTH from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed from esphome.platformio.toolchain import copy_ccache_script +from esphome.storage_json import StorageJSON from esphome.types import ConfigType from . import boards @@ -145,7 +146,7 @@ def only_on_variant( return validator_ -def get_download_types(storage_json): +def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: """Binary-download entries for a built RP2040 firmware. Used by device-builder (esphome/device-builder), via @@ -181,7 +182,7 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"https://github.com/earlephilhower/arduino-pico/releases/download/{ver}/rp2040-{ver}.zip" -def _parse_platform_version(value): +def _parse_platform_version(value: Any) -> str: value = cv.string(value) if value.startswith("http"): return value @@ -205,7 +206,7 @@ RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(6, 0, 0) RECOMMENDED_ARDUINO_PLATFORM_VERSION = "9c167c6b8aac4f4cfa6d55a0c4e5b848795150c0" -def _arduino_check_versions(value): +def _arduino_check_versions(value: ConfigType) -> ConfigType: value = value.copy() lookups = { "dev": (cv.Version(6, 0, 0), "https://github.com/earlephilhower/arduino-pico"), @@ -316,7 +317,7 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(CoroPriority.PLATFORM) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add(rp2_ns.setup_preferences()) # Allow LDF to properly discover dependency including those in preprocessor @@ -588,7 +589,7 @@ def _generate_lwipopts_h() -> None: write_file_if_changed(lwip_dir / "lwipopts.h", content) -def add_pio_file(component: str, key: str, data: str): +def add_pio_file(component: str, key: str, data: str) -> None: try: cv.validate_id_name(key) except cv.Invalid as e: @@ -629,7 +630,7 @@ def generate_pio_files() -> bool: # Called by writer.py -def copy_files(): +def copy_files() -> None: dir = Path(__file__).parent post_build_file = dir / "post_build.py.script" copy_file_if_changed( @@ -670,7 +671,7 @@ def _addr2line(tool: str, elf: Path, addr: str) -> str: return f"{addr} (decode failed)" -def process_stacktrace(config, line: str, backtrace_state: bool) -> bool: +def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool: """Decode RP2040 crash handler output using addr2line.""" if _CRASH_RE.search(line): _LOGGER.error("RP2040 crash detected - decoding addresses") diff --git a/esphome/components/rp2/generate_boards.py b/esphome/components/rp2/generate_boards.py index cd3f50182c..4066ef6b34 100644 --- a/esphome/components/rp2/generate_boards.py +++ b/esphome/components/rp2/generate_boards.py @@ -256,7 +256,7 @@ def generate(arduino_pico_path: Path) -> str: return result.stdout.decode() -def main(): +def main() -> None: if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} ", file=sys.stderr) sys.exit(1) diff --git a/esphome/components/rp2/gpio.py b/esphome/components/rp2/gpio.py index e4db6a831c..d325131178 100644 --- a/esphome/components/rp2/gpio.py +++ b/esphome/components/rp2/gpio.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv @@ -14,6 +16,8 @@ from esphome.const import ( CONF_PULLUP, ) from esphome.core import CORE +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import boards from .const import KEY_BOARD, KEY_RP2, rp2_ns @@ -21,7 +25,7 @@ from .const import KEY_BOARD, KEY_RP2, rp2_ns RP2GPIOPin = rp2_ns.class_("RP2GPIOPin", cg.InternalGPIOPin) -def _lookup_pin(value): +def _lookup_pin(value: str) -> int: board = CORE.data[KEY_RP2][KEY_BOARD] board_pins = boards.RP2_BOARD_PINS.get(board, {}) @@ -35,7 +39,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 " @@ -54,12 +58,12 @@ def _translate_pin(value): return _lookup_pin(value) -def _board_max_virtual_pin(board): +def _board_max_virtual_pin(board: str) -> int | None: """Get the max CYW43 virtual pin for this board, or None if no virtual pins.""" return boards.BOARDS.get(board, {}).get("max_virtual_pin") -def validate_gpio_pin(value): +def validate_gpio_pin(value: Any) -> int: value = _translate_pin(value) board = CORE.data[KEY_RP2][KEY_BOARD] max_virtual = _board_max_virtual_pin(board) @@ -71,7 +75,7 @@ def validate_gpio_pin(value): return value -def validate_supports(value): +def validate_supports(value: ConfigType) -> ConfigType: board = CORE.data[KEY_RP2][KEY_BOARD] if ( _board_max_virtual_pin(board) is None @@ -100,7 +104,7 @@ RP2_PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register("rp2", RP2_PIN_SCHEMA) -async def rp2_pin_to_code(config): +async def rp2_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/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py index a2fda58c9e..a0344e8054 100644 --- a/esphome/components/rp2040_pwm/output.py +++ b/esphome/components/rp2040_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 CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["rp2"] @@ -22,7 +25,7 @@ CONFIG_SCHEMA = output.FLOAT_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 cg.register_component(var, config) await output.register_output(var, config) @@ -44,7 +47,12 @@ async def to_code(config): ), synchronous=True, ) -async def rp2040_set_frequency_to_code(config, action_id, template_arg, args): +async def rp2040_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/sn74hc165/__init__.py b/esphome/components/sn74hc165/__init__.py index f2ba5fedd1..4f21312fec 100644 --- a/esphome/components/sn74hc165/__init__.py +++ b/esphome/components/sn74hc165/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CONF_MODE, CONF_NUMBER, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] DEPENDENCIES = [] @@ -38,7 +40,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) data_pin = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) @@ -54,7 +56,7 @@ async def to_code(config): cg.add(var.set_sr_count(config[CONF_SR_COUNT])) -def _validate_input_mode(value): +def _validate_input_mode(value: bool) -> bool: if value is not True: raise cv.Invalid("Only input mode is supported") return value @@ -77,7 +79,9 @@ SN74HC165_PIN_SCHEMA = cv.All( ) -def sn74hc165_pin_final_validate(pin_config, parent_config): +def sn74hc165_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}") @@ -86,7 +90,7 @@ def sn74hc165_pin_final_validate(pin_config, parent_config): @pins.PIN_SCHEMA_REGISTRY.register( CONF_SN74HC165, SN74HC165_PIN_SCHEMA, sn74hc165_pin_final_validate ) -async def sn74hc165_pin_to_code(config): +async def sn74hc165_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_SN74HC165]) diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index c065a82958..33a5c677bd 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -1,5 +1,6 @@ import contextlib import re +from typing import Any from esphome import automation import esphome.codegen as cg @@ -12,6 +13,9 @@ from esphome.const import ( CONF_TIME_ID, CONF_TRIGGER_ID, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter"] sun_ns = cg.esphome_ns.namespace("sun") @@ -40,7 +44,7 @@ ELEVATION_MAP = { } -def elevation(value): +def elevation(value: Any) -> float: if isinstance(value, str): with contextlib.suppress(cv.Invalid): value = ELEVATION_MAP[ @@ -60,7 +64,7 @@ LAT_LON_REGEX = re.compile( ) -def parse_latlon(value): +def parse_latlon(value: Any) -> float: if isinstance(value, str) and value.endswith("°"): # strip trailing degree character value = value[:-1] @@ -114,7 +118,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time(time_)) @@ -150,7 +154,12 @@ async def to_code(config): } ), ) -async def sun_above_horizon_to_code(config, condition_id, template_arg, args): +async def sun_above_horizon_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) templ = await cg.templatable(config[CONF_ELEVATION], args, cg.double) @@ -171,7 +180,12 @@ async def sun_above_horizon_to_code(config, condition_id, template_arg, args): } ), ) -async def sun_below_horizon_to_code(config, condition_id, template_arg, args): +async def sun_below_horizon_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) templ = await cg.templatable(config[CONF_ELEVATION], args, cg.double) diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py index a1ced8ff5b..d2e9fa750d 100644 --- a/esphome/components/sun/sensor/__init__.py +++ b/esphome/components/sun/sensor/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_DEGREES, ) +from esphome.types import ConfigType from .. import CONF_SUN_ID, Sun, sun_ns @@ -37,7 +38,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/sun/text_sensor/__init__.py b/esphome/components/sun/text_sensor/__init__.py index fc733d3435..523471bd41 100644 --- a/esphome/components/sun/text_sensor/__init__.py +++ b/esphome/components/sun/text_sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( ICON_WEATHER_SUNSET_DOWN, ICON_WEATHER_SUNSET_UP, ) +from esphome.types import ConfigType from .. import CONF_ELEVATION, CONF_SUN_ID, DEFAULT_ELEVATION, Sun, elevation, sun_ns @@ -22,7 +23,7 @@ SUN_TYPES = { } -def validate_optional_icon(config): +def validate_optional_icon(config: ConfigType) -> ConfigType: if CONF_ICON not in config: config = config.copy() config[CONF_ICON] = { @@ -48,7 +49,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index cf0c5fca19..c8b918007b 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -1,3 +1,7 @@ +from typing import Any + +import voluptuous as vol + from esphome import automation import esphome.codegen as cg from esphome.components import display @@ -14,6 +18,8 @@ from esphome.const import ( CONF_TRANSFORM, ) from esphome.core import CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@jesserockz", "@nielsnl68"] DEPENDENCIES = ["display"] @@ -40,7 +46,7 @@ CONF_Y_MIN = "y_min" CONF_Y_MAX = "y_max" -def validate_calibration(calibration_config): +def validate_calibration(calibration_config: ConfigType) -> ConfigType: x_min = calibration_config[CONF_X_MIN] x_max = calibration_config[CONF_X_MAX] y_min = calibration_config[CONF_Y_MIN] @@ -60,7 +66,9 @@ def validate_calibration(calibration_config): return calibration_config -def option_with_default(option: str, defaults: dict, required: bool = False): +def option_with_default( + option: str, defaults: dict, required: bool = False +) -> vol.Marker: if option in defaults or not required: return cv.Optional(option, default=defaults.get(option, cv.UNDEFINED)) return cv.Required(option) @@ -119,9 +127,9 @@ def _transform_schema(defaults: dict) -> dict: def touchscreen_schema( - default_touch_timeout=cv.UNDEFINED, - calibration_required=False, - defaults: dict = None, + default_touch_timeout: Any = cv.UNDEFINED, + calibration_required: bool = False, + defaults: dict | None = None, ) -> cv.Schema: defaults = defaults or {} return cv.Schema( @@ -143,7 +151,7 @@ def touchscreen_schema( TOUCHSCREEN_SCHEMA = touchscreen_schema(cv.UNDEFINED) -async def register_touchscreen(var, config): +async def register_touchscreen(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) disp = await cg.get_variable(config[CONF_DISPLAY]) @@ -192,6 +200,6 @@ async def register_touchscreen(var, config): @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(touchscreen_ns.using) cg.add_define("USE_TOUCHSCREEN") diff --git a/esphome/components/touchscreen/binary_sensor/__init__.py b/esphome/components/touchscreen/binary_sensor/__init__.py index 5ce0defb31..6a66d00ea6 100644 --- a/esphome/components/touchscreen/binary_sensor/__init__.py +++ b/esphome/components/touchscreen/binary_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor, display import esphome.config_validation as cv from esphome.const import CONF_PAGE_ID, CONF_PAGES +from esphome.types import ConfigType from .. import CONF_TOUCHSCREEN_ID, TouchListener, Touchscreen, touchscreen_ns @@ -22,7 +23,7 @@ CONF_Y_MAX = "y_max" CONF_USE_RAW = "use_raw" -def _validate_coords(config): +def _validate_coords(config: ConfigType) -> ConfigType: if ( config[CONF_X_MAX] < config[CONF_X_MIN] or config[CONF_Y_MAX] < config[CONF_Y_MIN] @@ -66,7 +67,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_TOUCHSCREEN_ID]) diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 18d333a5ef..5ebe58881d 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -14,14 +14,15 @@ from esphome.const import ( DEVICE_CLASS_FIRMWARE, ENTITY_CATEGORY_CONFIG, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_device_class, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True @@ -95,7 +96,7 @@ def update_schema( @setup_entity("update") -async def setup_update_core_(var, config): +async def setup_update_core_(var: MockObj, config: ConfigType) -> None: setup_device_class(config) if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE): @@ -113,7 +114,7 @@ async def setup_update_core_(var, config): await web_server.add_entity_config(var, web_server_config) -async def register_update(var, config): +async def register_update(var: MockObj, config: ConfigType) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) queue_entity_register("update", config) @@ -121,14 +122,14 @@ async def register_update(var, config): await setup_update_core_(var, config) -async def new_update(config): +async def new_update(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) await register_update(var, config) return var @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(update_ns.using) @@ -145,7 +146,12 @@ async def to_code(config): ), synchronous=True, ) -async def update_perform_action_to_code(config, action_id, template_arg, args): +async def update_perform_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]) @@ -164,7 +170,12 @@ async def update_perform_action_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def update_check_action_to_code(config, action_id, template_arg, args): +async def update_check_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 @@ -180,8 +191,11 @@ async def update_check_action_to_code(config, action_id, template_arg, args): ), ) async def update_is_available_condition_to_code( - config, condition_id, template_arg, args -): + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py index 2663496456..94857050f2 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__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 = ["@ssieb"] @@ -29,7 +30,7 @@ CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend( ) -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/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py index 85f1172166..5c09a025f8 100644 --- a/esphome/components/vbus/binary_sensor/__init__.py +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( DEVICE_CLASS_PROBLEM, ENTITY_CATEGORY_DIAGNOSTIC, ) +from esphome.types import ConfigType from .. import ( CONF_DELTASOL_BS2, @@ -256,7 +257,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) diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py index 9c3665eb1c..e8a6ea7bfa 100644 --- a/esphome/components/vbus/sensor/__init__.py +++ b/esphome/components/vbus/sensor/__init__.py @@ -29,6 +29,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_WATT_HOURS, ) +from esphome.types import ConfigType from .. import ( CONF_DELTASOL_BS2, @@ -650,7 +651,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) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index f41adfd8de..d30eaf4768 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -14,6 +14,9 @@ from esphome.const import ( CONF_ON_START, CONF_SPEAKER, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType AUTO_LOAD = ["audio", "ring_buffer", "socket"] DEPENDENCIES = ["api", "microphone"] @@ -78,7 +81,7 @@ ConnectedCondition = voice_assistant_ns.class_( Timer = voice_assistant_ns.struct("Timer") -def tts_stream_validate(config): +def tts_stream_validate(config: ConfigType) -> ConfigType: if CONF_SPEAKER not in config and ( CONF_ON_TTS_STREAM_START in config or CONF_ON_TTS_STREAM_END in config ): @@ -199,7 +202,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -420,7 +423,12 @@ VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssis ), synchronous=True, ) -async def voice_assistant_listen_to_code(config, action_id, template_arg, args): +async def voice_assistant_listen_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]) if CONF_SILENCE_DETECTION in config: @@ -434,7 +442,12 @@ async def voice_assistant_listen_to_code(config, action_id, template_arg, args): @register_action( "voice_assistant.stop", StopAction, VOICE_ASSISTANT_ACTION_SCHEMA, synchronous=True ) -async def voice_assistant_stop_to_code(config, action_id, template_arg, args): +async def voice_assistant_stop_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -443,7 +456,12 @@ async def voice_assistant_stop_to_code(config, action_id, template_arg, args): @register_condition( "voice_assistant.is_running", IsRunningCondition, VOICE_ASSISTANT_ACTION_SCHEMA ) -async def voice_assistant_is_running_to_code(config, condition_id, template_arg, args): +async def voice_assistant_is_running_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -452,7 +470,12 @@ async def voice_assistant_is_running_to_code(config, condition_id, template_arg, @register_condition( "voice_assistant.connected", ConnectedCondition, VOICE_ASSISTANT_ACTION_SCHEMA ) -async def voice_assistant_connected_to_code(config, condition_id, template_arg, args): +async def voice_assistant_connected_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/xiaomi_rtcgq02lm/__init__.py b/esphome/components/xiaomi_rtcgq02lm/__init__.py index 3e235d985f..7b289a8ee3 100644 --- a/esphome/components/xiaomi_rtcgq02lm/__init__.py +++ b/esphome/components/xiaomi_rtcgq02lm/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg 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.types import ConfigType AUTO_LOAD = ["ble_device_base", "xiaomi_ble"] CODEOWNERS = ["@jesserockz"] @@ -26,7 +27,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 ble_device_base.register_ble_device(var, config) diff --git a/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py index 8d0508b59b..57420125cb 100644 --- a/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py +++ b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py @@ -11,6 +11,7 @@ from esphome.const import ( DEVICE_CLASS_MOTION, ) from esphome.core import TimePeriod +from esphome.types import ConfigType from . import XiaomiRTCGQ02LM @@ -45,7 +46,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_ID]) if CONF_MOTION in config: diff --git a/esphome/components/xiaomi_rtcgq02lm/sensor.py b/esphome/components/xiaomi_rtcgq02lm/sensor.py index e49f1c960b..e0e4b4640b 100644 --- a/esphome/components/xiaomi_rtcgq02lm/sensor.py +++ b/esphome/components/xiaomi_rtcgq02lm/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ) +from esphome.types import ConfigType from . import XiaomiRTCGQ02LM @@ -29,7 +30,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_ID]) if CONF_BATTERY_LEVEL in config: From b6a9761dae4b015a30d9dd2492c408ff3ea424b1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:59:51 +1200 Subject: [PATCH 015/155] [core] Add type annotations to component Python (4/11) (#18341) --- esphome/components/aic3204/audio_dac.py | 12 +++++- esphome/components/audio_adc/__init__.py | 13 ++++-- esphome/components/audio_dac/__init__.py | 20 ++++++++-- esphome/components/bme68x_bsec2/__init__.py | 7 ++-- esphome/components/bme68x_bsec2/sensor.py | 6 ++- .../components/bme68x_bsec2/text_sensor.py | 6 ++- .../components/dfrobot_sen0395/__init__.py | 23 +++++++++-- .../dfrobot_sen0395/binary_sensor.py | 3 +- .../dfrobot_sen0395/switch/__init__.py | 3 +- esphome/components/dlms_meter/__init__.py | 14 ++++--- .../dlms_meter/binary_sensor/__init__.py | 3 +- .../components/dlms_meter/sensor/__init__.py | 5 ++- .../dlms_meter/text_sensor/__init__.py | 5 ++- esphome/components/ina2xx_base/__init__.py | 11 +++-- esphome/components/logger/__init__.py | 30 +++++++++----- esphome/components/logger/select/__init__.py | 3 +- esphome/components/ltr501/sensor.py | 13 +++--- esphome/components/ltr_als_ps/sensor.py | 11 +++-- esphome/components/msa3xx/__init__.py | 3 +- esphome/components/msa3xx/binary_sensor.py | 3 +- esphome/components/msa3xx/sensor.py | 3 +- esphome/components/msa3xx/text_sensor.py | 6 ++- esphome/components/ota/__init__.py | 10 +++-- esphome/components/safe_mode/__init__.py | 16 +++++--- .../components/safe_mode/button/__init__.py | 3 +- .../components/safe_mode/switch/__init__.py | 3 +- esphome/components/spi/__init__.py | 40 ++++++++++--------- esphome/components/st7789v/display.py | 10 +++-- esphome/components/substitutions/jinja.py | 12 +++--- esphome/components/thermostat/climate.py | 18 ++++++--- .../waveshare_io_ch32v003/__init__.py | 8 ++-- .../waveshare_io_ch32v003/output/__init__.py | 5 ++- .../waveshare_io_ch32v003/sensor/__init__.py | 3 +- esphome/components/web_server/__init__.py | 12 +++--- esphome/components/web_server/ota/__init__.py | 2 +- 35 files changed, 229 insertions(+), 116 deletions(-) diff --git a/esphome/components/aic3204/audio_dac.py b/esphome/components/aic3204/audio_dac.py index b478b573a3..50e2f81f1b 100644 --- a/esphome/components/aic3204/audio_dac.py +++ b/esphome/components/aic3204/audio_dac.py @@ -4,6 +4,9 @@ from esphome.components import i2c from esphome.components.audio_dac import AudioDac import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODE +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] DEPENDENCIES = ["i2c"] @@ -39,7 +42,12 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value( SET_AUTO_MUTE_ACTION_SCHEMA, synchronous=True, ) -async def aic3204_set_volume_to_code(config, action_id, template_arg, args): +async def aic3204_set_volume_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -49,7 +57,7 @@ async def aic3204_set_volume_to_code(config, action_id, template_arg, args): return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py index 3c3a4988b5..c2bdfb6cb0 100644 --- a/esphome/components/audio_adc/__init__.py +++ b/esphome/components/audio_adc/__init__.py @@ -2,7 +2,9 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MIC_GAIN -from esphome.core import CoroPriority, coroutine_with_priority +from esphome.core import ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] IS_PLATFORM_COMPONENT = True @@ -28,7 +30,12 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value( SET_MIC_GAIN_ACTION_SCHEMA, synchronous=True, ) -async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): +async def audio_adc_set_mic_gain_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -39,6 +46,6 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_AUDIO_ADC") cg.add_global(audio_adc_ns.using) diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py index 46c277ce51..1351793afd 100644 --- a/esphome/components/audio_dac/__init__.py +++ b/esphome/components/audio_dac/__init__.py @@ -3,7 +3,9 @@ from esphome.automation import maybe_simple_id import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_VOLUME -from esphome.core import CoroPriority, coroutine_with_priority +from esphome.core import ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] IS_PLATFORM_COMPONENT = True @@ -37,7 +39,12 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( "audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True ) -async def audio_dac_mute_action_to_code(config, action_id, template_arg, args): +async def audio_dac_mute_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -48,7 +55,12 @@ async def audio_dac_mute_action_to_code(config, action_id, template_arg, args): SET_VOLUME_ACTION_SCHEMA, synchronous=True, ) -async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): +async def audio_dac_set_volume_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -59,6 +71,6 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_AUDIO_DAC") cg.add_global(audio_dac_ns.using) diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index c12eb39d2d..8208672b6a 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, ) +from esphome.cpp_generator import MockObj from esphome.external_files import RemoteFile from esphome.types import ConfigType @@ -94,7 +95,7 @@ def _compute_url(config: dict) -> str: return f"https://raw.githubusercontent.com/boschsensortec/Bosch-BSEC2-Library/{BSEC2_LIBRARY_VERSION}/src/config/{model}/{model}_{algo}_{volts}_{sample_rate}_{operating_age}/{filename}.txt" -def download_bme68x_blob(config): +def download_bme68x_blob(config: ConfigType) -> ConfigType: url = _compute_url(config) path = _compute_local_file_path(url) external_files.download_content(url, path) @@ -138,7 +139,7 @@ def _extract_blob_ref(entry: ConfigType) -> RemoteFile | None: PREFETCH_FILES = external_files.single_stage_prefetch(_extract_blob_ref) -def validate_bme68x(config): +def validate_bme68x(config: ConfigType) -> ConfigType: if CONF_ALGORITHM_OUTPUT not in config: return config @@ -178,7 +179,7 @@ CONFIG_SCHEMA_BASE = ( ) -async def to_code_base(config): +async def to_code_base(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/bme68x_bsec2/sensor.py b/esphome/components/bme68x_bsec2/sensor.py index 52587dba99..863cd9d601 100644 --- a/esphome/components/bme68x_bsec2/sensor.py +++ b/esphome/components/bme68x_bsec2/sensor.py @@ -29,6 +29,8 @@ from esphome.const import ( UNIT_PARTS_PER_MILLION, UNIT_PERCENT, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_BME68X_BSEC2_ID, SAMPLE_RATE_OPTIONS, BME68xBSEC2Component @@ -119,7 +121,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def setup_conf(config, key, hub): +async def setup_conf(config: ConfigType, key: str, hub: MockObj) -> None: if conf := config.get(key): sens = await sensor.new_sensor(conf) cg.add(getattr(hub, f"set_{key}_sensor")(sens)) @@ -127,7 +129,7 @@ async def setup_conf(config, key, hub): cg.add(getattr(hub, f"set_{key}_sample_rate")(sample_rate)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) for key in TYPES: await setup_conf(config, key, hub) diff --git a/esphome/components/bme68x_bsec2/text_sensor.py b/esphome/components/bme68x_bsec2/text_sensor.py index fce00afe34..5c6f9f696c 100644 --- a/esphome/components/bme68x_bsec2/text_sensor.py +++ b/esphome/components/bme68x_bsec2/text_sensor.py @@ -2,6 +2,8 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_IAQ_ACCURACY +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_BME68X_BSEC2_ID, BME68xBSEC2Component @@ -21,13 +23,13 @@ CONFIG_SCHEMA = cv.Schema( ) -async def setup_conf(config, key, hub): +async def setup_conf(config: ConfigType, key: str, hub: MockObj) -> None: if conf := config.get(key): sens = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) for key in TYPES: await setup_conf(config, key, hub) diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index 943c510279..51562f923c 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -1,9 +1,14 @@ +from typing import Any + from esphome import automation from esphome.automation import maybe_simple_id import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_SENSITIVITY +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@niklasweber"] DEPENDENCIES = ["uart"] @@ -38,7 +43,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) @@ -54,14 +59,19 @@ async def to_code(config): ), synchronous=True, ) -async def dfrobot_sen0395_reset_to_code(config, action_id, template_arg, args): +async def dfrobot_sen0395_reset_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -def range_segment_list(input): +def range_segment_list(input: Any) -> list: """Validate input is a list of ranges which can be used to configure the dfrobot mmwave radar A list of segments should be provided. A minimum of one segment is required and a maximum of @@ -154,7 +164,12 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema( MMWAVE_SETTINGS_SCHEMA, synchronous=True, ) -async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args): +async def dfrobot_sen0395_settings_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/dfrobot_sen0395/binary_sensor.py b/esphome/components/dfrobot_sen0395/binary_sensor.py index 193ef925a4..e299c35a42 100644 --- a/esphome/components/dfrobot_sen0395/binary_sensor.py +++ b/esphome/components/dfrobot_sen0395/binary_sensor.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import DEVICE_CLASS_MOTION +from esphome.types import ConfigType from . import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component @@ -16,7 +17,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID]) binary_sens = await binary_sensor.new_binary_sensor(config) diff --git a/esphome/components/dfrobot_sen0395/switch/__init__.py b/esphome/components/dfrobot_sen0395/switch/__init__.py index 8e492080de..22aaa1640c 100644 --- a/esphome/components/dfrobot_sen0395/switch/__init__.py +++ b/esphome/components/dfrobot_sen0395/switch/__init__.py @@ -3,6 +3,7 @@ from esphome.components import switch import esphome.config_validation as cv from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG from esphome.cpp_generator import MockObjClass +from esphome.types import ConfigType from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component @@ -55,7 +56,7 @@ CONFIG_SCHEMA = cv.typed_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID]) var = await switch.new_switch(config) await cg.register_component(var, config) diff --git a/esphome/components/dlms_meter/__init__.py b/esphome/components/dlms_meter/__init__.py index b747f73a14..00a1694cc3 100644 --- a/esphome/components/dlms_meter/__init__.py +++ b/esphome/components/dlms_meter/__init__.py @@ -1,5 +1,6 @@ import logging import re +from typing import Any import esphome.codegen as cg from esphome.components import esp32, uart @@ -12,6 +13,7 @@ from esphome.const import ( CONF_RECEIVE_TIMEOUT, ) from esphome.core import CORE +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -33,13 +35,13 @@ DlmsMeterComponent = dlms_meter_component_ns.class_( ) -def obis_code(value): +def obis_code(value: Any) -> str: # Normalize the OBIS code to the strict A.B.C.D.E.F format bytes_list = parse_obis_code_bytes(value) return ".".join(str(b) for b in bytes_list) -def parse_obis_code_bytes(value): +def parse_obis_code_bytes(value: Any) -> list[int]: value = cv.string(value) normalized = re.sub(r"[\-\:\*]", ".", value) parts = normalized.split(".") @@ -57,19 +59,19 @@ def parse_obis_code_bytes(value): return bytes_list -def custom_pattern_dict(value): +def custom_pattern_dict(value: Any) -> ConfigType: if isinstance(value, str): return {CONF_PATTERN: value} return value -def validate_custom_pattern(value): +def validate_custom_pattern(value: ConfigType) -> ConfigType: if CONF_DEFAULT_OBIS in value and CONF_NAME not in value: raise cv.Invalid(f"'{CONF_DEFAULT_OBIS}' requires '{CONF_NAME}' to be set") return value -def validate_provider_deprecation(config): +def validate_provider_deprecation(config: ConfigType) -> ConfigType: if CONF_PROVIDER in config: provider = str(config[CONF_PROVIDER]).lower() if provider == "netznoe": @@ -154,7 +156,7 @@ CONFIG_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("dlms_meter", require_rx=True) -async def to_code(config): +async def to_code(config: ConfigType) -> None: dec_key_expr = cg.RawExpression("std::nullopt") if dec_key := config.get(CONF_DECRYPTION_KEY): key_bytes = [str(int(dec_key[i : i + 2], 16)) for i in range(0, 32, 2)] diff --git a/esphome/components/dlms_meter/binary_sensor/__init__.py b/esphome/components/dlms_meter/binary_sensor/__init__.py index f9bc1d9df7..a15e58b957 100644 --- a/esphome/components/dlms_meter/binary_sensor/__init__.py +++ b/esphome/components/dlms_meter/binary_sensor/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv +from esphome.types import ConfigType from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code @@ -14,7 +15,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) var = await binary_sensor.new_binary_sensor(config) cg.add(hub.register_binary_sensor(config[CONF_OBIS_CODE], var)) diff --git a/esphome/components/dlms_meter/sensor/__init__.py b/esphome/components/dlms_meter/sensor/__init__.py index ec4639351d..8ded150cd0 100644 --- a/esphome/components/dlms_meter/sensor/__init__.py +++ b/esphome/components/dlms_meter/sensor/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.types import ConfigType from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code @@ -47,7 +48,7 @@ DYNAMIC_SCHEMA = sensor.sensor_schema().extend( ) -def deprecation_warning(config): +def deprecation_warning(config: ConfigType) -> ConfigType: _LOGGER.warning( "The dlms_meter sensor schema using predefined keys (e.g., 'voltage_l1') is deprecated and will be removed in 2026.11.0. " "Please update your configuration to use the new schema with 'obis_code'." @@ -145,7 +146,7 @@ OLD_SCHEMA = cv.All( CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) if obis := config.get(CONF_OBIS_CODE): diff --git a/esphome/components/dlms_meter/text_sensor/__init__.py b/esphome/components/dlms_meter/text_sensor/__init__.py index 0bfb43a285..c2ff0779ee 100644 --- a/esphome/components/dlms_meter/text_sensor/__init__.py +++ b/esphome/components/dlms_meter/text_sensor/__init__.py @@ -3,6 +3,7 @@ import logging import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv +from esphome.types import ConfigType from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code @@ -23,7 +24,7 @@ DYNAMIC_SCHEMA = text_sensor.text_sensor_schema().extend( ) -def deprecation_warning(config): +def deprecation_warning(config: ConfigType) -> ConfigType: _LOGGER.warning( "The dlms_meter text_sensor schema using predefined keys (e.g., 'timestamp') is deprecated and will be removed in 2026.11.0. " "Please update your configuration to use the new schema with 'obis_code'." @@ -46,7 +47,7 @@ OLD_SCHEMA = cv.All( CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) if obis := config.get(CONF_OBIS_CODE): diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py index 15e2faba07..7bb589f0b1 100644 --- a/esphome/components/ina2xx_base/__init__.py +++ b/esphome/components/ina2xx_base/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + import esphome.codegen as cg from esphome.components import sensor from esphome.components.const import UNIT_AMPERE_HOUR @@ -26,6 +28,9 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.core import EnumValue +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@latonita"] @@ -76,7 +81,7 @@ SENSOR_MODEL_OPTIONS = { } -def validate_model_config(config): +def validate_model_config(config: ConfigType) -> ConfigType: model = config[CONF_MODEL] for key in config: @@ -92,7 +97,7 @@ def validate_model_config(config): return config -def validate_adc_time(value): +def validate_adc_time(value: Any) -> EnumValue: value = cv.positive_time_period_microseconds(value).total_microseconds return cv.enum(ADC_TIMES, int=True)(value) @@ -198,7 +203,7 @@ INA2XX_SCHEMA = cv.Schema( ).extend(cv.polling_component_schema("60s")) -async def setup_ina2xx(var, config): +async def setup_ina2xx(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index f307f5d5d1..07b8b03084 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,4 +1,5 @@ import re +from typing import Any from esphome import automation from esphome.automation import LambdaAction, StatelessLambdaAction @@ -58,7 +59,8 @@ from esphome.const import ( PLATFORM_RTL87XX, PlatformFramework, ) -from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -164,7 +166,7 @@ HARDWARE_UART_TO_SERIAL = { is_log_level = cv.one_of(*LOG_LEVELS, upper=True) -def uart_selection(value): +def uart_selection(value: Any) -> str: if CORE.is_esp32: variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: @@ -187,7 +189,7 @@ def uart_selection(value): raise NotImplementedError -def validate_local_no_higher_than_global(config): +def validate_local_no_higher_than_global(config: ConfigType) -> ConfigType: global_level = config[CONF_LEVEL] global_level_index = LOG_LEVEL_SEVERITY.index(global_level) errs = [] @@ -204,7 +206,7 @@ def validate_local_no_higher_than_global(config): return config -def validate_initial_no_higher_than_global(config): +def validate_initial_no_higher_than_global(config: ConfigType) -> ConfigType: if initial_level := config.get(CONF_INITIAL_LEVEL): global_level = config[CONF_LEVEL] if LOG_LEVEL_SEVERITY.index(initial_level) > LOG_LEVEL_SEVERITY.index( @@ -217,7 +219,7 @@ def validate_initial_no_higher_than_global(config): return config -def validate_wait_for_cdc(config): +def validate_wait_for_cdc(config: ConfigType) -> ConfigType: if config.get(CONF_WAIT_FOR_CDC) and config.get(CONF_HARDWARE_UART) != USB_CDC: raise cv.Invalid("wait_for_cdc requires hardware_uart: USB_CDC") return config @@ -518,7 +520,7 @@ async def _late_logger_init(config: ConfigType) -> None: CORE.add_job(final_step) -def validate_printf(value): +def validate_printf(value: ConfigType) -> ConfigType: # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python cfmt = r""" ( # start of capture group 1 @@ -559,7 +561,12 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All( @automation.register_action( CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA, synchronous=True ) -async def logger_log_action_to_code(config, action_id, template_arg, args): +async def logger_log_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]] @@ -584,7 +591,12 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def logger_set_level_to_code(config, action_id, template_arg, args): +async def logger_set_level_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: level = LOG_LEVELS[config[CONF_LEVEL]] logger = await cg.get_variable(config[CONF_LOGGER_ID]) if tag := config.get(CONF_TAG): @@ -656,7 +668,7 @@ def request_log_listener() -> None: @coroutine_with_priority(CoroPriority.FINAL) -async def final_step(): +async def final_step() -> None: """Final code generation step to configure optional logger features.""" domain_data = CORE.data.get(DOMAIN, {}) if domain_data.get(KEY_LEVEL_LISTENERS, False): diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py index 6ce663978e..00f67422f3 100644 --- a/esphome/components/logger/select/__init__.py +++ b/esphome/components/logger/select/__init__.py @@ -4,6 +4,7 @@ import esphome.config_validation as cv from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_BUG from esphome.core import CORE from esphome.cpp_helpers import register_component, register_parented +from esphome.types import ConfigType from .. import ( CONF_LOGGER_ID, @@ -26,7 +27,7 @@ CONFIG_SCHEMA = select.select_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: request_logger_level_listeners() parent = await cg.get_variable(config[CONF_LOGGER_ID]) levels = list(LOG_LEVELS) diff --git a/esphome/components/ltr501/sensor.py b/esphome/components/ltr501/sensor.py index c1fa9009b3..c2091a6336 100644 --- a/esphome/components/ltr501/sensor.py +++ b/esphome/components/ltr501/sensor.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.components import i2c, sensor @@ -24,6 +26,7 @@ from esphome.const import ( UNIT_LUX, UNIT_MILLISECOND, ) +from esphome.types import ConfigType CODEOWNERS = ["@latonita"] DEPENDENCIES = ["i2c"] @@ -87,17 +90,17 @@ PS_GAINS = { } -def validate_integration_time(value): +def validate_integration_time(value: Any) -> Any: value = cv.positive_time_period_milliseconds(value).total_milliseconds return cv.enum(INTEGRATION_TIMES, int=True)(value) -def validate_repeat_rate(value): +def validate_repeat_rate(value: Any) -> Any: value = cv.positive_time_period_milliseconds(value).total_milliseconds return cv.enum(MEASUREMENT_REPEAT_RATES, int=True)(value) -def validate_time_and_repeat_rate(config): +def validate_time_and_repeat_rate(config: ConfigType) -> ConfigType: integraton_time = config[CONF_INTEGRATION_TIME] repeat_rate = config[CONF_REPEAT] if integraton_time > repeat_rate: @@ -107,7 +110,7 @@ def validate_time_and_repeat_rate(config): return config -def validate_als_gain_and_integration_time(config): +def validate_als_gain_and_integration_time(config: ConfigType) -> ConfigType: integraton_time = config[CONF_INTEGRATION_TIME] if config[CONF_GAIN] == "1X" and integraton_time > 100: raise cv.Invalid( @@ -221,7 +224,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ltr_als_ps/sensor.py b/esphome/components/ltr_als_ps/sensor.py index 893415f028..af09282e2d 100644 --- a/esphome/components/ltr_als_ps/sensor.py +++ b/esphome/components/ltr_als_ps/sensor.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.components import i2c, sensor @@ -23,6 +25,7 @@ from esphome.const import ( UNIT_LUX, UNIT_MILLISECOND, ) +from esphome.types import ConfigType CODEOWNERS = ["@latonita"] DEPENDENCIES = ["i2c"] @@ -93,17 +96,17 @@ PS_GAINS = { } -def validate_integration_time(value): +def validate_integration_time(value: Any) -> Any: value = cv.positive_time_period_milliseconds(value).total_milliseconds return cv.enum(INTEGRATION_TIMES, int=True)(value) -def validate_repeat_rate(value): +def validate_repeat_rate(value: Any) -> Any: value = cv.positive_time_period_milliseconds(value).total_milliseconds return cv.enum(MEASUREMENT_REPEAT_RATES, int=True)(value) -def validate_time_and_repeat_rate(config): +def validate_time_and_repeat_rate(config: ConfigType) -> ConfigType: integraton_time = config[CONF_INTEGRATION_TIME] repeat_rate = config[CONF_REPEAT] if integraton_time > repeat_rate: @@ -211,7 +214,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/msa3xx/__init__.py b/esphome/components/msa3xx/__init__.py index 04514b584f..0beece6710 100644 --- a/esphome/components/msa3xx/__init__.py +++ b/esphome/components/msa3xx/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( CONF_TRANSFORM, CONF_TYPE, ) +from esphome.types import ConfigType CODEOWNERS = ["@latonita"] DEPENDENCIES = ["i2c"] @@ -123,7 +124,7 @@ MSA_SENSOR_SCHEMA = cv.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/msa3xx/binary_sensor.py b/esphome/components/msa3xx/binary_sensor.py index 732a0ed291..ef27c98e66 100644 --- a/esphome/components/msa3xx/binary_sensor.py +++ b/esphome/components/msa3xx/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_ACTIVE, CONF_NAME, DEVICE_CLASS_VIBRATION, ICON_VIBRATE +from esphome.types import ConfigType from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA @@ -31,7 +32,7 @@ CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_MSA3XX_ID]) for sensor in EVENT_SENSORS: diff --git a/esphome/components/msa3xx/sensor.py b/esphome/components/msa3xx/sensor.py index 63f050fa05..22bcb94025 100644 --- a/esphome/components/msa3xx/sensor.py +++ b/esphome/components/msa3xx/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_METER_PER_SECOND_SQUARED, ) +from esphome.types import ConfigType from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA @@ -34,7 +35,7 @@ CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_MSA3XX_ID]) for accel_key in ACCELERATION_SENSORS: if accel_key in config: diff --git a/esphome/components/msa3xx/text_sensor.py b/esphome/components/msa3xx/text_sensor.py index c53a4aa139..6693ec8542 100644 --- a/esphome/components/msa3xx/text_sensor.py +++ b/esphome/components/msa3xx/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_NAME +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA @@ -25,13 +27,13 @@ CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend( ) -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 text_sensor.new_text_sensor(sensor_config) cg.add(getattr(hub, f"set_{key}_text_sensor")(var)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_MSA3XX_ID]) for key in ORIENTATION_SENSORS: diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 1e2ee947c1..5240db9e8f 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -12,6 +12,8 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType OTA_STATE_LISTENER_KEY = "ota_state_listener" @@ -49,7 +51,7 @@ OTAStateChangeTrigger = ota_ns.class_( ) -def _ota_final_validate(config): +def _ota_final_validate(config: ConfigType) -> None: if len(config) < 1: raise cv.Invalid( f"At least one platform must be specified for '{CONF_OTA}'; add '{CONF_PLATFORM}: {CONF_ESPHOME}' for original OTA functionality" @@ -95,7 +97,7 @@ BASE_OTA_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.OTA_UPDATES) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_OTA") CORE.add_job(final_step) @@ -103,7 +105,7 @@ async def to_code(config): cg.add_library("Updater", None) -async def ota_to_code(var, config): +async def ota_to_code(var: MockObj, config: ConfigType) -> None: await cg.past_safe_mode() use_state_callback = False for conf in config.get(CONF_ON_STATE_CHANGE, []): @@ -145,7 +147,7 @@ def request_ota_state_listeners() -> None: @coroutine_with_priority(CoroPriority.FINAL) -async def final_step(): +async def final_step() -> None: """Final code generation step to configure optional OTA features.""" if CORE.data.get(OTA_STATE_LISTENER_KEY, False): cg.add_define("USE_OTA_STATE_LISTENER") diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index 70096a56bc..9bc8a263c8 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -10,8 +10,9 @@ from esphome.const import ( CONF_STORAGE, KEY_PAST_SAFE_MODE, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.cpp_generator import RawExpression +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, RawExpression, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"] @@ -24,7 +25,7 @@ SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action) -def _remove_id_if_disabled(value): +def _remove_id_if_disabled(value: ConfigType) -> ConfigType: value = value.copy() if value[CONF_DISABLED]: value.pop(CONF_ID) @@ -62,7 +63,12 @@ CONFIG_SCHEMA = cv.All( ), synchronous=True, ) -async def safe_mode_mark_successful_to_code(config, action_id, template_arg, args): +async def safe_mode_mark_successful_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) cg.add(var.set_parent(parent)) @@ -75,7 +81,7 @@ _CALLBACK_AUTOMATIONS = ( @coroutine_with_priority(CoroPriority.APPLICATION) -async def to_code(config): +async def to_code(config: ConfigType) -> None: if not config[CONF_DISABLED]: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py index 0731ca50f5..89e2475799 100644 --- a/esphome/components/safe_mode/button/__init__.py +++ b/esphome/components/safe_mode/button/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import SafeModeComponent, safe_mode_ns @@ -26,7 +27,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await button.new_button(config) await cg.register_component(var, config) diff --git a/esphome/components/safe_mode/switch/__init__.py b/esphome/components/safe_mode/switch/__init__.py index d656eee84a..529b023d68 100644 --- a/esphome/components/safe_mode/switch/__init__.py +++ b/esphome/components/safe_mode/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_SAFE_MODE, ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT +from esphome.types import ConfigType from .. import SafeModeComponent, safe_mode_ns @@ -21,7 +22,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/spi/__init__.py b/esphome/components/spi/__init__.py index 608adc7514..d7b85ee20d 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -83,7 +83,7 @@ def _render_hz(value: float) -> str: return formatted + unit -def _frequency_validator(value): +def _frequency_validator(value: Any) -> float: platform = get_target_platform() frequency = PLATFORM_SPI_CLOCKS[platform] value = cv.frequency(value) @@ -153,17 +153,17 @@ RP_SPI_PINSETS = [ ] -def get_target_platform(): +def get_target_platform() -> str: return CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] -def get_target_variant(): +def get_target_variant() -> str: return CORE.data[KEY_ESP32].get(KEY_VARIANT, "") # Get a list of available hardware interfaces based on target and variant. # The returned value is a list of lists of names -def get_hw_interface_list(): +def get_hw_interface_list() -> list[list[str]]: target_platform = get_target_platform() if target_platform == PLATFORM_ESP8266: return [["spi", "hspi"]] @@ -196,7 +196,7 @@ def one_of_interface_validator(additional_values: list[str] | None = None) -> An if additional_values is None: additional_values = [] - def validator(value: str) -> str: + def validator(value: Any) -> str: return cv.one_of( *sum(get_hw_interface_list(), additional_values), lower=True, @@ -206,7 +206,7 @@ def one_of_interface_validator(additional_values: list[str] | None = None) -> An # Given an SPI name, return the index of it in the available list -def get_spi_index(name): +def get_spi_index(name: str) -> int: for i, ilist in enumerate(get_hw_interface_list()): if name in ilist: return i @@ -218,7 +218,7 @@ def get_spi_index(name): # \param spi the config data for the spi instance # \param index the selected hw interface number, -1 if not yet known # TODO verify that the pins are internal -def validate_hw_pins(spi, index=-1): +def validate_hw_pins(spi: ConfigType, index: int = -1) -> bool: clk_pin = spi[CONF_CLK_PIN] if clk_pin[CONF_INVERTED]: return False @@ -265,7 +265,7 @@ def validate_hw_pins(spi, index=-1): return False -def get_hw_spi(config, available): +def get_hw_spi(config: ConfigType, available: list[int]) -> int | None: """Get an available hardware spi interface suitable for this config""" matching = list(filter(lambda idx: validate_hw_pins(config, idx), available)) if len(matching) != 0: @@ -273,7 +273,7 @@ def get_hw_spi(config, available): return None -def validate_spi_config(config): +def validate_spi_config(config: list[ConfigType]) -> list[ConfigType]: available = list(range(len(get_hw_interface_list()))) for spi in config: interface = spi[CONF_INTERFACE] @@ -317,7 +317,7 @@ def validate_spi_config(config): # Given an SPI index, convert to a string that represents the C++ object for it. -def get_spi_interface(index): +def get_spi_interface(index: int) -> str: platform = get_target_platform() if platform == PLATFORM_ESP32: # ESP32 uses ESP-IDF SPI driver for both Arduino and IDF frameworks @@ -353,7 +353,7 @@ SPI_SINGLE_SCHEMA = cv.All( ) -def spi_mode_schema(mode): +def spi_mode_schema(mode: str) -> cv.Schema: if mode == TYPE_SINGLE: return SPI_SINGLE_SCHEMA pin_count = 4 if mode == TYPE_QUAD else 8 @@ -400,7 +400,7 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(CoroPriority.BUS) -async def to_code(configs): +async def to_code(configs: list[ConfigType]) -> None: cg.add_define("USE_SPI") cg.add_global(spi_ns.using) if CORE.using_arduino and not CORE.is_esp32: @@ -427,11 +427,11 @@ async def to_code(configs): def spi_device_schema( - cs_pin_required=True, - default_data_rate=cv.UNDEFINED, - default_mode=cv.UNDEFINED, - mode=TYPE_SINGLE, -): + cs_pin_required: bool = True, + default_data_rate: Any = cv.UNDEFINED, + default_mode: Any = cv.UNDEFINED, + mode: str = TYPE_SINGLE, +) -> cv.Schema: """Create a schema for an SPI device. :param cs_pin_required: If true, make the CS_PIN required in the config. :param default_data_rate: Optional data_rate to use as default @@ -456,7 +456,7 @@ def spi_device_schema( async def register_spi_device( - var: cg.Pvariable, config: ConfigType, write_only: bool = False + var: cg.MockObj, config: ConfigType, write_only: bool = False ) -> None: parent = await cg.get_variable(config[CONF_SPI_ID]) cg.add(var.set_spi_parent(parent)) @@ -473,7 +473,9 @@ async def register_spi_device( cg.add(var.set_release_device(release_device)) -def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: bool): +def final_validate_device_schema( + name: str, *, require_mosi: bool, require_miso: bool +) -> cv.Schema: hub_schema = {} if require_miso: hub_schema[ diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 3b4d6d99ea..fa72c7c328 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -1,4 +1,5 @@ import logging +from typing import Any from esphome import pins import esphome.codegen as cg @@ -19,6 +20,7 @@ from esphome.const import ( CONF_ROTATION, CONF_WIDTH, ) +from esphome.types import ConfigType from . import st7789v_ns @@ -38,7 +40,9 @@ MODEL_PRESETS = "model_presets" REQUIRE_PS = "require_ps" -def model_spec(require_ps=False, presets=None): +def model_spec( + require_ps: bool = False, presets: dict[str, Any] | None = None +) -> dict[str, Any]: if presets is None: presets = {} return {MODEL_PRESETS: presets, REQUIRE_PS: require_ps} @@ -119,7 +123,7 @@ MODELS = { } -def validate_st7789v(config): +def validate_st7789v(config: ConfigType) -> ConfigType: model_data = MODELS[config[CONF_MODEL]] presets = model_data[MODEL_PRESETS] for key, value in presets.items(): @@ -178,7 +182,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: LOGGER.warning( "The 'st7789v' component is deprecated, it is recommended to use 'mipi_spi' instead." ) diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index 36a7425a69..230ad09df9 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -43,23 +43,23 @@ SAFE_GLOBALS = { class JinjaError(Exception): - def __init__(self, context_trace: dict, expr: str): + def __init__(self, context_trace: dict, expr: str) -> None: self.context_trace = context_trace self.eval_stack = [expr] - def parent(self): + def parent(self) -> BaseException | None: return self.__context__ - def error_name(self): + def error_name(self) -> str: return type(self.parent()).__name__ - def context_trace_str(self): + def context_trace_str(self) -> str: return "\n".join( f" {k} = {repr(v)} ({type(v).__name__})" for k, v in self.context_trace.items() ) - def stack_trace_str(self): + def stack_trace_str(self) -> str: return "\n".join( f" {len(self.eval_stack) - i}: {expr}{i == 0 and ' <-- ' + self.error_name() or ''}" for i, expr in enumerate(self.eval_stack) @@ -67,7 +67,7 @@ class JinjaError(Exception): class TrackerContext(jinja.runtime.Context): - def resolve_or_missing(self, key): + def resolve_or_missing(self, key: str) -> Any: val = super().resolve_or_missing(key) if val is Missing: # Variable not in the template context — check if a resolver callback diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index d609e22ac2..3cc4dc7009 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.components import climate, sensor @@ -70,6 +72,7 @@ from esphome.const import ( CONF_TARGET_TEMPERATURE_CHANGE_ACTION, CONF_VISUAL, ) +from esphome.types import ConfigType CONF_DEFAULT_PRESET = "default_preset" CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION = "humidity_control_dehumidify_action" @@ -124,7 +127,12 @@ PRESET_CONFIG_SCHEMA = cv.Schema( ) -def validate_temperature_preset(preset, root_config, name, requirements): +def validate_temperature_preset( + preset: ConfigType, + root_config: ConfigType, + name: str, + requirements: dict[str, list[str]], +) -> None: # verify temperature settings for the provided preset / default / away configuration for config_temp, req_actions in requirements.items(): for req_action in req_actions: @@ -140,7 +148,7 @@ def validate_temperature_preset(preset, root_config, name, requirements): ) -def generate_comparable_preset(config, name): +def generate_comparable_preset(config: ConfigType, name: str) -> str: comparable_preset = f"{CONF_PRESET}:\n - {CONF_NAME}: {name}\n" if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: @@ -151,7 +159,7 @@ def generate_comparable_preset(config, name): return comparable_preset -def validate_heat_cool_mode(value) -> list: +def validate_heat_cool_mode(value: Any) -> list: """Validate heat_cool_mode - accepts either True or an automation.""" if value is True: # Convert True to empty automation list @@ -164,7 +172,7 @@ def validate_heat_cool_mode(value) -> list: return automation.validate_automation(single=True)(value) -def validate_thermostat(config): +def validate_thermostat(config: ConfigType) -> ConfigType: # verify corresponding action(s) exist(s) for any defined climate mode or action requirements = { CONF_HEAT_COOL_MODE: [ @@ -681,7 +689,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await climate.new_climate(config) await cg.register_component(var, config) diff --git a/esphome/components/waveshare_io_ch32v003/__init__.py b/esphome/components/waveshare_io_ch32v003/__init__.py index b692b858a3..29a939c523 100644 --- a/esphome/components/waveshare_io_ch32v003/__init__.py +++ b/esphome/components/waveshare_io_ch32v003/__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 CODEOWNERS = ["@latonita"] @@ -41,13 +43,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(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]: @@ -71,7 +73,7 @@ WAVESHARE_IO_PIN_SCHEMA = pins.gpio_base_schema( @pins.PIN_SCHEMA_REGISTRY.register(CONF_WAVESHARE_IO_CH32V003, WAVESHARE_IO_PIN_SCHEMA) -async def waveshare_io_pin_to_code(config): +async def waveshare_io_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_WAVESHARE_IO_CH32V003]) diff --git a/esphome/components/waveshare_io_ch32v003/output/__init__.py b/esphome/components/waveshare_io_ch32v003/output/__init__.py index 9af9ce7e4b..7438769928 100644 --- a/esphome/components/waveshare_io_ch32v003/output/__init__.py +++ b/esphome/components/waveshare_io_ch32v003/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_MAX_VALUE, CONF_MIN_VALUE +from esphome.types import ConfigType from .. import ( CONF_WAVESHARE_IO_CH32V003_ID, @@ -23,7 +24,7 @@ DUTY_DEFAULT_MIN = 1 DUTY_DEFAULT_MAX = 247 -def validate_pwm_limits(config): +def validate_pwm_limits(config: ConfigType) -> ConfigType: """Validate that safe_pwm_levels.min_value <= safe_pwm_levels.max_value.""" min_val = config.get(CONF_SAFE_PWM_LEVELS, {}).get(CONF_MIN_VALUE, DUTY_DEFAULT_MIN) @@ -61,7 +62,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 output.register_output(var, config) await cg.register_parented(var, config[CONF_WAVESHARE_IO_CH32V003_ID]) diff --git a/esphome/components/waveshare_io_ch32v003/sensor/__init__.py b/esphome/components/waveshare_io_ch32v003/sensor/__init__.py index 1e060bdfe4..8ec2702da6 100644 --- a/esphome/components/waveshare_io_ch32v003/sensor/__init__.py +++ b/esphome/components/waveshare_io_ch32v003/sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) +from esphome.types import ConfigType from .. import ( CONF_WAVESHARE_IO_CH32V003_ID, @@ -46,7 +47,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_WAVESHARE_IO_CH32V003_ID]) await cg.register_component(var, config) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index b2c0ea14ad..a50c14a2f7 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -4,6 +4,7 @@ import base64 import gzip import logging import re +from typing import Any import esphome.codegen as cg from esphome.components import web_server_base @@ -39,6 +40,7 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj import esphome.final_validate as fv from esphome.types import ConfigType @@ -128,7 +130,7 @@ def validate_ota(config: ConfigType) -> ConfigType: _ORIGIN_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*://[^/\s]+$") -def validate_origin(value: str) -> str: +def validate_origin(value: Any) -> str: # "*" is the wildcard that allows any origin. if value == "*": return value @@ -306,7 +308,7 @@ CONFIG_SCHEMA = cv.All( ) -def add_sorting_groups(web_server_var, config): +def add_sorting_groups(web_server_var: MockObj, config: list[ConfigType]) -> None: for group in config: sorting_groups[group[CONF_ID]] = group[CONF_NAME] group_sorting_weight = group.get(CONF_SORTING_WEIGHT, 50) @@ -317,7 +319,7 @@ def add_sorting_groups(web_server_var, config): ) -async def add_entity_config(entity, config): +async def add_entity_config(entity: MockObj, config: ConfigType) -> None: web_server = await cg.get_variable(config[CONF_WEB_SERVER_ID]) sorting_weight = config.get(CONF_SORTING_WEIGHT, 50) sorting_group_hash = hash(config.get(CONF_SORTING_GROUP_ID)) @@ -332,7 +334,7 @@ async def add_entity_config(entity, config): ) -def build_index_html(config) -> str: +def build_index_html(config: ConfigType) -> str: html = "" css_include = config.get(CONF_CSS_INCLUDE) js_include = config.get(CONF_JS_INCLUDE) @@ -366,7 +368,7 @@ def add_resource_as_progmem( @coroutine_with_priority(CoroPriority.WEB) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) var = cg.new_Pvariable(config[CONF_ID], paren) diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 260e6aea6d..03a5c2ca9b 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -80,7 +80,7 @@ FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate @coroutine_with_priority(CoroPriority.WEB_SERVER_OTA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await ota_to_code(var, config) await cg.register_component(var, config) From e83439eaaeed12653926473ff9fbfcbc168254f2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 04:07:10 +1200 Subject: [PATCH 016/155] [core] Add type annotations to component Python (7/11) (#18344) --- esphome/components/as5600/__init__.py | 24 ++++++----- esphome/components/as5600/sensor/__init__.py | 3 +- esphome/components/audio/__init__.py | 17 ++++---- esphome/components/duty_time/sensor.py | 40 ++++++++++++++++--- esphome/components/esp32_hosted/__init__.py | 10 ++--- esphome/components/mixer/speaker/__init__.py | 16 ++++++-- esphome/components/rc522/__init__.py | 4 +- esphome/components/rc522/binary_sensor.py | 7 +++- .../components/resampler/speaker/__init__.py | 11 +++-- esphome/components/rtttl/__init__.py | 28 ++++++++++--- esphome/components/scd4x/sensor.py | 19 +++++++-- esphome/components/sen5x/sensor.py | 13 +++++- esphome/components/sendspin/__init__.py | 6 +-- .../components/sendspin/sensor/__init__.py | 4 +- esphome/components/sound_level/sensor.py | 12 +++++- esphome/components/sps30/sensor.py | 12 +++++- esphome/components/sx127x/__init__.py | 24 ++++++++--- .../sx127x/packet_transport/__init__.py | 3 +- esphome/components/tm1651/__init__.py | 40 ++++++++++++++++--- esphome/components/ufire_ec/sensor.py | 19 +++++++-- esphome/components/ufire_ise/sensor.py | 26 ++++++++++-- 21 files changed, 261 insertions(+), 77 deletions(-) diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py index c05e556376..780712c3bd 100644 --- a/esphome/components/as5600/__init__.py +++ b/esphome/components/as5600/__init__.py @@ -1,3 +1,6 @@ +from collections.abc import Callable +from typing import Any + from esphome import pins import esphome.codegen as cg from esphome.components import i2c @@ -11,6 +14,7 @@ from esphome.const import ( CONF_RANGE, CONF_WATCHDOG, ) +from esphome.types import ConfigType CODEOWNERS = ["@ammmze"] DEPENDENCIES = ["i2c"] @@ -72,13 +76,13 @@ POSITION_TO_ANGLE = 360 / RESOLUTION MIN_RANGE = round(18 * ANGLE_TO_POSITION) -def angle(min=-360, max=360): +def angle(min: float = -360, max: float = 360) -> Callable[[Any], Any]: return cv.All( cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max) ) -def angle_to_position(value, min=-360, max=360): +def angle_to_position(value: Any, min: float = -360, max: float = 360) -> int: try: value = angle(min=min, max=max)(value) return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION @@ -86,17 +90,17 @@ def angle_to_position(value, min=-360, max=360): raise cv.Invalid(f"When using angle, {e.error_message}") from e -def percent_to_position(value): +def percent_to_position(value: Any) -> int: value = cv.possibly_negative_percentage(value) return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION -def position(min=-MAX_POSITION, max=MAX_POSITION): +def position(min: int = -MAX_POSITION, max: int = MAX_POSITION) -> Callable[[Any], Any]: """Validate that the config option is a position. Accepts integers, degrees, or percentage (of 360 degrees). """ - def validator(value): + def validator(value: Any) -> int: if isinstance(value, str) and value.endswith("%"): value = percent_to_position(value) @@ -112,7 +116,7 @@ def position(min=-MAX_POSITION, max=MAX_POSITION): return validator -def position_range(): +def position_range() -> Callable[[Any], Any]: """Validate that value given is a valid range for the device. A valid range is one of the following: - a value of 0 (meaning full range) @@ -129,7 +133,7 @@ def position_range(): zero_validator, ) - def validator(value): + def validator(value: Any) -> Any: is_negative_str = isinstance(value, str) and value.startswith("-") is_negative_num = isinstance(value, (float, int)) and value < 0 if is_negative_str or is_negative_num: @@ -139,13 +143,13 @@ def position_range(): return validator -def has_valid_range_config(): +def has_valid_range_config() -> Callable[[ConfigType], ConfigType]: """Validate that that the config start + end position results in a valid positional range, which must be >= 18degrees """ range_validator = position_range() - def validator(config): + def validator(config: ConfigType) -> ConfigType: # if we don't have an end position, then there is nothing to do if CONF_END_POSITION not in config: return config @@ -203,7 +207,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index cf67a3f203..847b89f121 100644 --- a/esphome/components/as5600/sensor/__init__.py +++ b/esphome/components/as5600/sensor/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( ICON_ROTATE_RIGHT, STATE_CLASS_MEASUREMENT, ) +from esphome.types import ConfigType from .. import AS5600Component, as5600_ns @@ -77,7 +78,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_AS5600_ID]) await cg.register_component(var, config) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 1c522cbb5d..277df0506a 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,4 +1,6 @@ +from collections.abc import Callable from dataclasses import dataclass, field +from typing import Any import esphome.codegen as cg from esphome.components.esp32 import ( @@ -15,6 +17,7 @@ from esphome.const import ( ) from esphome.core import CORE import esphome.final_validate as fv +from esphome.types import ConfigType AUTO_LOAD = ["ring_buffer"] CODEOWNERS = ["@kahrendt"] @@ -125,10 +128,10 @@ CONF_THREADSAFE = "threadsafe" _MEMORY_LOCATION_VALIDATOR = cv.one_of(*MEMORY_LOCATIONS, lower=True) -def _maybe_empty_codec(schema): +def _maybe_empty_codec(schema: cv.Schema) -> Callable[[Any], Any]: """Wrap a codec dict schema so that a bare key (None value) is treated as an empty dict.""" - def validator(value): + def validator(value: Any) -> Any: if value is None: value = {} return schema(value) @@ -200,14 +203,14 @@ def set_stream_limits( max_channels: int = cv.UNDEFINED, min_sample_rate: int = cv.UNDEFINED, max_sample_rate: int = cv.UNDEFINED, -): +) -> Callable[[ConfigType], None]: """Sets the limits for the audio stream that audio component can handle When the component sinks audio (e.g., a speaker), these indicate the limits to the audio it can receive. When the component sources audio (e.g., a microphone), these indicate the limits to the audio it can send. """ - def set_limits_in_config(config): + def set_limits_in_config(config: ConfigType) -> None: if min_bits_per_sample is not cv.UNDEFINED: config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample if max_bits_per_sample is not cv.UNDEFINED: @@ -233,7 +236,7 @@ def final_validate_audio_schema( sample_rate: int = cv.UNDEFINED, enabled_channels: list[int] = cv.UNDEFINED, audio_device_issue: bool = False, -): +) -> cv.Schema: """Validates audio compatibility when passed between different components. The component derived from ``AUDIO_COMPONENT_SCHEMA`` should call ``set_stream_limits`` in a validator to specify its compatible settings @@ -251,7 +254,7 @@ def final_validate_audio_schema( audio_device_issue (bool, optional): Format the error message to indicate the problem is in the configuration for the ``audio_device`` component. Defaults to False. """ - def validate_audio_compatiblity(audio_config): + def validate_audio_compatiblity(audio_config: ConfigType) -> ConfigType: audio_schema = {} if bits_per_sample is not cv.UNDEFINED: @@ -329,7 +332,7 @@ def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> N add_idf_sdkconfig_option(internal_key, True) -async def to_code(config): +async def to_code(config: ConfigType) -> None: # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) include_builtin_idf_component("esp_http_client") diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py index 456859f8e4..6d878a80a5 100644 --- a/esphome/components/duty_time/sensor.py +++ b/esphome/components/duty_time/sensor.py @@ -19,6 +19,9 @@ from esphome.const import ( STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CONF_LAST_TIME = "last_time" @@ -66,7 +69,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) cg.add(var.set_restore(config[CONF_RESTORE])) @@ -93,7 +96,12 @@ DUTY_TIME_ID_SCHEMA = maybe_simple_id( @register_action( "sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA, synchronous=True ) -async def sensor_runtime_start_to_code(config, action_id, template_arg, args): +async def sensor_runtime_start_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -102,7 +110,12 @@ async def sensor_runtime_start_to_code(config, action_id, template_arg, args): @register_action( "sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA, synchronous=True ) -async def sensor_runtime_stop_to_code(config, action_id, template_arg, args): +async def sensor_runtime_stop_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -111,7 +124,12 @@ async def sensor_runtime_stop_to_code(config, action_id, template_arg, args): @register_action( "sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA, synchronous=True ) -async def sensor_runtime_reset_to_code(config, action_id, template_arg, args): +async def sensor_runtime_reset_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -120,7 +138,12 @@ async def sensor_runtime_reset_to_code(config, action_id, template_arg, args): @register_condition( "sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA ) -async def duty_time_is_running_to_code(config, condition_id, template_arg, args): +async def duty_time_is_running_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren, True) @@ -128,6 +151,11 @@ async def duty_time_is_running_to_code(config, condition_id, template_arg, args) @register_condition( "sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA ) -async def duty_time_is_not_running_to_code(config, condition_id, template_arg, args): +async def duty_time_is_not_running_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren, False) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 7dc61ce382..ab9455250c 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -64,7 +64,7 @@ SDIO_SCHEMA = BASE_SCHEMA.extend( ) -def _validate_sdio(config): +def _validate_sdio(config: ConfigType) -> ConfigType: if config[CONF_BUS_WIDTH] == 4: for pin in (CONF_D1_PIN, CONF_D2_PIN, CONF_D3_PIN): if pin not in config: @@ -98,7 +98,7 @@ SPI_SCHEMA = BASE_SCHEMA.extend( ) -def _validate_spi(config): +def _validate_spi(config: ConfigType) -> ConfigType: variant = config[CONF_VARIANT] defaults = _SPI_VARIANT_DEFAULTS.get(variant, _SPI_DEFAULT) @@ -141,7 +141,7 @@ def _final_validate(config: ConfigType) -> None: FINAL_VALIDATE_SCHEMA = _final_validate -def _configure_sdio(config): +def _configure_sdio(config: ConfigType) -> None: slot = config[CONF_SLOT] esp32.add_idf_sdkconfig_option( f"CONFIG_ESP_HOSTED_SDIO_SLOT_{slot}", @@ -183,7 +183,7 @@ def _configure_sdio(config): ) -def _configure_spi(config): +def _configure_spi(config: ConfigType) -> None: esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_HOST_INTERFACE", True) # SPI mode is set via per-variant choice options variant = config[CONF_VARIANT] @@ -231,7 +231,7 @@ def _configure_spi(config): esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_LOW", True) -async def to_code(config): +async def to_code(config: ConfigType) -> None: add_define("USE_ESP32_HOSTED") transport = config[CONF_TYPE] transport_prefix = "SDIO" if transport == "sdio" else "SPI" diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index 47164a9997..a3746c019a 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -15,8 +15,11 @@ from esphome.const import ( CONF_TIMEOUT, PLATFORM_ESP32, ) +from esphome.core import ID from esphome.core.entity_helpers import inherit_property_from +from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv +from esphome.types import ConfigType AUTO_LOAD = ["audio"] CODEOWNERS = ["@kahrendt"] @@ -48,7 +51,7 @@ SOURCE_SPEAKER_SCHEMA = speaker.SPEAKER_SCHEMA.extend( ) -def _validate_source_speaker(config): +def _validate_source_speaker(config: ConfigType) -> ConfigType: fconf = fv.full_config.get() # Get ID for the output speaker and add it to the source speakers config to easily inherit properties @@ -70,7 +73,7 @@ def _validate_source_speaker(config): return config -def _validate_output_speaker(config): +def _validate_output_speaker(config: ConfigType) -> ConfigType: audio.final_validate_audio_schema( "mixer", audio_device=CONF_OUTPUT_SPEAKER, @@ -112,7 +115,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -161,7 +164,12 @@ async def to_code(config): ), synchronous=True, ) -async def ducking_set_to_code(config, action_id, template_arg, args): +async def ducking_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) decibel_reduction = await cg.templatable( diff --git a/esphome/components/rc522/__init__.py b/esphome/components/rc522/__init__.py index ce0d408c04..e9e8dd7b73 100644 --- a/esphome/components/rc522/__init__.py +++ b/esphome/components/rc522/__init__.py @@ -8,6 +8,8 @@ from esphome.const import ( CONF_RESET_PIN, CONF_TRIGGER_ID, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@glmnet"] AUTO_LOAD = ["binary_sensor"] @@ -38,7 +40,7 @@ RC522_SCHEMA = cv.Schema( ).extend(cv.polling_component_schema("1s")) -async def setup_rc522(var, config): +async def setup_rc522(var: MockObj, config: ConfigType) -> None: await cg.register_component(var, config) if CONF_RESET_PIN in config: diff --git a/esphome/components/rc522/binary_sensor.py b/esphome/components/rc522/binary_sensor.py index 87f81c2223..f295b75df7 100644 --- a/esphome/components/rc522/binary_sensor.py +++ b/esphome/components/rc522/binary_sensor.py @@ -1,15 +1,18 @@ +from typing import Any + import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import CONF_UID from esphome.core import HexInt +from esphome.types import ConfigType from . import CONF_RC522_ID, RC522, rc522_ns DEPENDENCIES = ["rc522"] -def validate_uid(value): +def validate_uid(value: Any) -> str: value = cv.string_strict(value) for x in value.split("-"): if len(x) != 2: @@ -39,7 +42,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(RC522BinarySensor).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_RC522_ID]) diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index ea080adc6b..7de468cb50 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + import esphome.codegen as cg from esphome.components import audio, psram, speaker import esphome.config_validation as cv @@ -13,6 +15,7 @@ from esphome.const import ( PLATFORM_ESP32, ) from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType AUTO_LOAD = ["audio"] CODEOWNERS = ["@kahrendt"] @@ -27,7 +30,7 @@ CONF_TAPS = "taps" PASSTHROUGH = "passthrough" -def _set_stream_limits(config): +def _set_stream_limits(config: ConfigType) -> ConfigType: audio.set_stream_limits( min_bits_per_sample=16, max_bits_per_sample=32, @@ -36,7 +39,7 @@ def _set_stream_limits(config): return config -def _validate_audio_compatibility(config): +def _validate_audio_compatibility(config: ConfigType) -> None: inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config) inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config) @@ -57,7 +60,7 @@ def _validate_audio_compatibility(config): )(config) -def _validate_taps(taps): +def _validate_taps(taps: Any) -> int: value = cv.int_range(min=16, max=128)(taps) if value % 4 != 0: raise cv.Invalid("Number of taps must be divisible by 4") @@ -88,7 +91,7 @@ CONFIG_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility -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 speaker.register_speaker(var, config) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 4880f9ac41..b6c4183586 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -6,7 +6,10 @@ from esphome.components.output import FloatOutput from esphome.components.speaker import Speaker import esphome.config_validation as cv from esphome.const import CONF_GAIN, CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_SPEAKER +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -37,7 +40,7 @@ CONFIG_SCHEMA = cv.All( ) -def validate_parent_output_config(value): +def validate_parent_output_config(value: ConfigType) -> None: platform = value.get(CONF_PLATFORM) PWM_GOOD = ["esp8266_pwm", "ledc"] PWM_BAD = [ @@ -78,7 +81,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -110,7 +113,12 @@ async def to_code(config): ), synchronous=True, ) -async def rtttl_play_to_code(config, action_id, template_arg, args): +async def rtttl_play_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_RTTTL], args, cg.std_string) @@ -128,7 +136,12 @@ async def rtttl_play_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def rtttl_stop_to_code(config, action_id, template_arg, args): +async def rtttl_stop_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -143,7 +156,12 @@ async def rtttl_stop_to_code(config, action_id, template_arg, args): } ), ) -async def rtttl_is_playing_to_code(config, condition_id, template_arg, args): +async def rtttl_is_playing_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 6f14118660..af3ff3a7af 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -26,6 +26,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 CODEOWNERS = ["@sjtrny", "@martgras"] DEPENDENCIES = ["i2c"] @@ -108,7 +111,7 @@ SETTING_MAP = { } -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) @@ -143,7 +146,12 @@ SCD4X_ACTION_SCHEMA = maybe_simple_id( SCD4X_ACTION_SCHEMA, synchronous=True, ) -async def scd4x_frc_to_code(config, action_id, template_arg, args): +async def scd4x_frc_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16) @@ -164,7 +172,12 @@ SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id( SCD4X_RESET_ACTION_SCHEMA, synchronous=True, ) -async def scd4x_reset_to_code(config, action_id, template_arg, args): +async def scd4x_reset_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 761a1885ea..e86c8bf899 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/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 @@ -41,6 +43,8 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_PERCENT, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@martgras"] @@ -115,7 +119,7 @@ def _gas_sensor( ) -def float_previously_pct(value): +def float_previously_pct(value: Any) -> Any: if isinstance(value, str) and "%" in value: raise cv.Invalid( f"The value '{value}' is a percentage. Suggested value: {float(value.strip('%')) / 100}" @@ -284,6 +288,11 @@ SEN5X_ACTION_SCHEMA = maybe_simple_id( SEN5X_ACTION_SCHEMA, synchronous=True, ) -async def sen54_fan_to_code(config, action_id, template_arg, args): +async def sen54_fan_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/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 082639374f..570fd3fadd 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -15,7 +15,7 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.core import CORE, ID -from esphome.cpp_generator import TemplateArgsType +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType # mdns for autodiscovery @@ -219,7 +219,7 @@ async def sendspin_switch_to_code( 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 @@ -297,7 +297,7 @@ async def to_code(config: ConfigType) -> None: codecs.append(CODEC_FORMAT_OPUS) codecs.append(CODEC_FORMAT_PCM) - def _audio_format(codec, channels): + def _audio_format(codec: MockObj, channels: int) -> cg.StructInitializer: return cg.StructInitializer( AudioSupportedFormatObject, ("codec", codec), diff --git a/esphome/components/sendspin/sensor/__init__.py b/esphome/components/sendspin/sensor/__init__.py index dc9b86c2a3..d6016ed91d 100644 --- a/esphome/components/sendspin/sensor/__init__.py +++ b/esphome/components/sendspin/sensor/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv @@ -50,7 +52,7 @@ def _request_roles(config: ConfigType) -> ConfigType: _HUB_ID_SCHEMA = cv.Schema({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)}) -def _metadata_schema(**sensor_kwargs): +def _metadata_schema(**sensor_kwargs: Any) -> cv.Schema: """Schema for event-driven numeric metadata sensors (duration/year/track).""" return ( sensor.sensor_schema( diff --git a/esphome/components/sound_level/sensor.py b/esphome/components/sound_level/sensor.py index 44f31979b4..d217534041 100644 --- a/esphome/components/sound_level/sensor.py +++ b/esphome/components/sound_level/sensor.py @@ -11,6 +11,9 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_DECIBEL, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType AUTO_LOAD = ["audio"] CODEOWNERS = ["@kahrendt"] @@ -63,7 +66,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) @@ -95,7 +98,12 @@ SOUND_LEVEL_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( "sound_level.stop", StopAction, SOUND_LEVEL_ACTION_SCHEMA, synchronous=True ) -async def sound_level_action_to_code(config, action_id, template_arg, args): +async def sound_level_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/sps30/sensor.py b/esphome/components/sps30/sensor.py index 40557f2cbd..681166cd3c 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -26,6 +26,9 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_MICROMETER, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@martgras"] DEPENDENCIES = ["i2c"] @@ -120,7 +123,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) @@ -197,7 +200,12 @@ SPS30_ACTION_SCHEMA = maybe_simple_id( SPS30_ACTION_SCHEMA, synchronous=True, ) -async def sps30_action_to_code(config, action_id, template_arg, args): +async def sps30_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/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 8fa7247192..34f2d4122f 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation, pins import esphome.codegen as cg from esphome.components import spi @@ -5,6 +7,8 @@ from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -136,7 +140,7 @@ SetModeStandbyAction = sx127x_ns.class_( ) -def validate_raw_data(value): +def validate_raw_data(value: Any) -> bytes | list[int]: if isinstance(value, str): return value.encode("utf-8") if isinstance(value, list): @@ -146,7 +150,7 @@ def validate_raw_data(value): ) -def validate_config(config): +def validate_config(config: ConfigType) -> ConfigType: if config[CONF_MODULATION] == "LORA": bws = [ "7_8kHz", @@ -230,7 +234,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 spi.register_spi_device(var, config) @@ -312,7 +316,12 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def no_args_action_to_code(config, action_id, template_arg, args): +async def 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 @@ -333,7 +342,12 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( SEND_PACKET_ACTION_SCHEMA, synchronous=True, ) -async def send_packet_action_to_code(config, action_id, template_arg, args): +async def send_packet_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]) data = config[CONF_DATA] diff --git a/esphome/components/sx127x/packet_transport/__init__.py b/esphome/components/sx127x/packet_transport/__init__.py index 2f3a0f6e2b..33204a7d83 100644 --- a/esphome/components/sx127x/packet_transport/__init__.py +++ b/esphome/components/sx127x/packet_transport/__init__.py @@ -6,6 +6,7 @@ from esphome.components.packet_transport import ( ) import esphome.config_validation as cv from esphome.cpp_types import PollingComponent +from esphome.types import ConfigType from .. import CONF_SX127X_ID, SX127x, SX127xListener, sx127x_ns @@ -20,7 +21,7 @@ CONFIG_SCHEMA = transport_schema(SX127xTransport).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var, _ = await new_packet_transport(config) sx127x = await cg.get_variable(config[CONF_SX127X_ID]) cg.add(var.set_parent(sx127x)) diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index 7d957df3be..c0cc6f1d2c 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -9,6 +9,9 @@ from esphome.const import ( CONF_ID, CONF_LEVEL, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@mrtoy-me"] @@ -43,7 +46,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) clk_pin = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) @@ -75,7 +78,12 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( ), synchronous=True, ) -async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): +async def tm1651_set_brightness_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_BRIGHTNESS], args, cg.uint8) @@ -95,7 +103,12 @@ async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def tm1651_set_level_to_code(config, action_id, template_arg, args): +async def tm1651_set_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]) template_ = await cg.templatable(config[CONF_LEVEL], args, cg.uint8) @@ -115,7 +128,12 @@ async def tm1651_set_level_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): +async def tm1651_set_level_percent_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_LEVEL_PERCENT], args, cg.uint8) @@ -129,7 +147,12 @@ async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True, ) -async def output_turn_off_to_code(config, action_id, template_arg, args): +async def output_turn_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 @@ -138,7 +161,12 @@ async def output_turn_off_to_code(config, action_id, template_arg, args): @automation.register_action( "tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True ) -async def output_turn_on_to_code(config, action_id, template_arg, args): +async def output_turn_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]) return var diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py index 1d8775ccf0..9d989ad4e6 100644 --- a/esphome/components/ufire_ec/sensor.py +++ b/esphome/components/ufire_ec/sensor.py @@ -14,6 +14,9 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_MILLISIEMENS_PER_CENTIMETER, ) +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) cg.add(var.set_temperature_compensation(config[CONF_TEMPERATURE_COMPENSATION])) @@ -99,7 +102,12 @@ UFIRE_EC_CALIBRATE_PROBE_SCHEMA = cv.Schema( UFIRE_EC_CALIBRATE_PROBE_SCHEMA, synchronous=True, ) -async def ufire_ec_calibrate_probe_to_code(config, action_id, template_arg, args): +async def ufire_ec_calibrate_probe_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) solution_ = await cg.templatable(config[CONF_SOLUTION], args, cg.float_) @@ -122,6 +130,11 @@ UFIRE_EC_RESET_SCHEMA = cv.Schema( UFIRE_EC_RESET_SCHEMA, synchronous=True, ) -async def ufire_ec_reset_to_code(config, action_id, template_arg, args): +async def ufire_ec_reset_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/ufire_ise/sensor.py b/esphome/components/ufire_ise/sensor.py index 23254b2f47..c7e3b6f28d 100644 --- a/esphome/components/ufire_ise/sensor.py +++ b/esphome/components/ufire_ise/sensor.py @@ -13,6 +13,9 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PH, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -60,7 +63,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) @@ -93,7 +96,12 @@ UFIRE_ISE_CALIBRATE_PROBE_SCHEMA = cv.Schema( UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, synchronous=True, ) -async def ufire_ise_calibrate_probe_low_to_code(config, action_id, template_arg, args): +async def ufire_ise_calibrate_probe_low_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_SOLUTION], args, cg.float_) @@ -107,7 +115,12 @@ async def ufire_ise_calibrate_probe_low_to_code(config, action_id, template_arg, UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, synchronous=True, ) -async def ufire_ise_calibrate_probe_high_to_code(config, action_id, template_arg, args): +async def ufire_ise_calibrate_probe_high_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_SOLUTION], args, cg.float_) @@ -124,6 +137,11 @@ UFIRE_ISE_RESET_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(UFireISEComponent UFIRE_ISE_RESET_SCHEMA, synchronous=True, ) -async def ufire_ise_reset_to_code(config, action_id, template_arg, args): +async def ufire_ise_reset_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) From 545f762568609fa7f57e841852308e6c9f2d7dd4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 04:08:26 +1200 Subject: [PATCH 017/155] [core] Add type annotations to component Python (8/11) (#18345) --- .../components/atm90e32/button/__init__.py | 3 +- .../components/atm90e32/number/__init__.py | 3 +- esphome/components/atm90e32/sensor.py | 3 +- .../atm90e32/text_sensor/__init__.py | 3 +- esphome/components/bl0940/button/__init__.py | 3 +- esphome/components/bl0940/number/__init__.py | 5 +-- esphome/components/bl0940/sensor.py | 19 ++++++----- esphome/components/bm8563/time.py | 26 ++++++++++++--- esphome/components/event/__init__.py | 24 ++++++++++---- esphome/components/ld2410/__init__.py | 12 +++++-- esphome/components/ld2410/binary_sensor.py | 3 +- esphome/components/ld2410/button/__init__.py | 3 +- esphome/components/ld2410/number/__init__.py | 3 +- esphome/components/ld2410/select/__init__.py | 3 +- esphome/components/ld2410/sensor.py | 3 +- esphome/components/ld2410/switch/__init__.py | 3 +- esphome/components/ld2410/text_sensor.py | 3 +- esphome/components/ld2412/__init__.py | 3 +- esphome/components/ld2412/binary_sensor.py | 3 +- esphome/components/ld2412/button/__init__.py | 3 +- esphome/components/ld2412/number/__init__.py | 3 +- esphome/components/ld2412/select/__init__.py | 3 +- esphome/components/ld2412/sensor.py | 3 +- esphome/components/ld2412/switch/__init__.py | 3 +- esphome/components/ld2412/text_sensor.py | 3 +- esphome/components/ld2420/__init__.py | 3 +- .../ld2420/binary_sensor/__init__.py | 3 +- esphome/components/ld2420/button/__init__.py | 3 +- esphome/components/ld2420/number/__init__.py | 3 +- esphome/components/ld2420/select/__init__.py | 3 +- esphome/components/ld2420/sensor/__init__.py | 3 +- .../components/ld2420/text_sensor/__init__.py | 3 +- esphome/components/ld2450/__init__.py | 3 +- esphome/components/ld2450/binary_sensor.py | 3 +- esphome/components/ld2450/button/__init__.py | 3 +- esphome/components/ld2450/number/__init__.py | 3 +- esphome/components/ld2450/select/__init__.py | 3 +- esphome/components/ld2450/sensor.py | 3 +- esphome/components/ld2450/switch/__init__.py | 3 +- esphome/components/ld2450/text_sensor.py | 3 +- esphome/components/max6956/__init__.py | 23 ++++++++++--- esphome/components/max6956/output/__init__.py | 3 +- esphome/components/max7219digit/display.py | 33 ++++++++++++++++--- esphome/components/micronova/__init__.py | 10 ++++-- .../components/micronova/button/__init__.py | 3 +- .../components/micronova/number/__init__.py | 3 +- .../components/micronova/sensor/__init__.py | 3 +- .../components/micronova/switch/__init__.py | 3 +- .../micronova/text_sensor/__init__.py | 3 +- esphome/components/pipsolar/__init__.py | 3 +- .../pipsolar/binary_sensor/__init__.py | 3 +- .../components/pipsolar/output/__init__.py | 12 +++++-- .../components/pipsolar/sensor/__init__.py | 3 +- .../components/pipsolar/switch/__init__.py | 3 +- .../pipsolar/text_sensor/__init__.py | 3 +- esphome/components/text/__init__.py | 30 ++++++++++------- .../components/text/text_sensor/__init__.py | 3 +- 57 files changed, 238 insertions(+), 97 deletions(-) diff --git a/esphome/components/atm90e32/button/__init__.py b/esphome/components/atm90e32/button/__init__.py index 19f62ccfbd..274cce6adb 100644 --- a/esphome/components/atm90e32/button/__init__.py +++ b/esphome/components/atm90e32/button/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import button import esphome.config_validation as cv from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE +from esphome.types import ConfigType from .. import atm90e32_ns from ..sensor import ATM90E32Component @@ -67,7 +68,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_ID]) if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION): diff --git a/esphome/components/atm90e32/number/__init__.py b/esphome/components/atm90e32/number/__init__.py index 848680b875..9c2865dde3 100644 --- a/esphome/components/atm90e32/number/__init__.py +++ b/esphome/components/atm90e32/number/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( UNIT_AMPERE, UNIT_VOLT, ) +from esphome.types import ConfigType from .. import atm90e32_ns from ..sensor import ATM90E32Component @@ -90,7 +91,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_ID]) if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE): diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index dc46138add..38b24c7cf6 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -41,6 +41,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.types import ConfigType from . import atm90e32_ns @@ -191,7 +192,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_instance_id(str(config[CONF_ID]))) await cg.register_component(var, config) diff --git a/esphome/components/atm90e32/text_sensor/__init__.py b/esphome/components/atm90e32/text_sensor/__init__.py index ab96f6c207..30585cb873 100644 --- a/esphome/components/atm90e32/text_sensor/__init__.py +++ b/esphome/components/atm90e32/text_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C +from esphome.types import ConfigType from ..sensor import ATM90E32Component @@ -34,7 +35,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_ID]) if phase_cfg := config.get(CONF_PHASE_STATUS): diff --git a/esphome/components/bl0940/button/__init__.py b/esphome/components/bl0940/button/__init__.py index 04d11e6e30..e87a647392 100644 --- a/esphome/components/bl0940/button/__init__.py +++ b/esphome/components/bl0940/button/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import button import esphome.config_validation as cv from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART +from esphome.types import ConfigType from .. import CONF_BL0940_ID, bl0940_ns from ..sensor import BL0940 @@ -21,7 +22,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await button.new_button(config) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_BL0940_ID]) diff --git a/esphome/components/bl0940/number/__init__.py b/esphome/components/bl0940/number/__init__.py index 92ab2837b3..b5a66e682a 100644 --- a/esphome/components/bl0940/number/__init__.py +++ b/esphome/components/bl0940/number/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, UNIT_PERCENT, ) +from esphome.types import ConfigType from .. import CONF_BL0940_ID, bl0940_ns from ..sensor import BL0940 @@ -27,7 +28,7 @@ CalibrationNumber = bl0940_ns.class_( ) -def validate_min_max(config): +def validate_min_max(config: ConfigType) -> ConfigType: if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: raise cv.Invalid("max_value must be greater than min_value") return config @@ -69,7 +70,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: # Get the BL0940 component instance bl0940 = await cg.get_variable(config[CONF_BL0940_ID]) diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index 96445d5c38..7e6403c3bc 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -23,6 +23,7 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, ) +from esphome.types import ConfigType from . import bl0940_ns @@ -69,27 +70,29 @@ DEFAULT_BL0940_LEGACY_EREF = 3.6e6 / 297 # methods to calculate voltage and current reference values -def calculate_voltage_reference(vref, r_one, r_two): +def calculate_voltage_reference(vref: float, r_one: float, r_two: float) -> float: # formula: 79931 / Vref * (R1 * 1000) / (R1 + R2) return 79931 / vref * (r_one * 1000) / (r_one + r_two) -def calculate_current_reference(vref, r_shunt): +def calculate_current_reference(vref: float, r_shunt: float) -> float: # formula: 324004 * RL / Vref return 324004 * r_shunt / vref -def calculate_power_reference(voltage_reference, current_reference): +def calculate_power_reference( + voltage_reference: float, current_reference: float +) -> float: # calculate power reference based on voltage and current reference return voltage_reference * current_reference * 4046 / 324004 / 79931 -def calculate_energy_reference(power_reference): +def calculate_energy_reference(power_reference: float) -> float: # formula: power_reference * 3600000 / (1638.4 * 256) return power_reference * 3600000 / (1638.4 * 256) -def validate_legacy_mode(config): +def validate_legacy_mode(config: ConfigType) -> ConfigType: # Only allow schematic calibration options if legacy_mode is False if config.get(CONF_LEGACY_MODE, True): forbidden = [ @@ -106,7 +109,7 @@ def validate_legacy_mode(config): return config -def set_command_defaults(config): +def set_command_defaults(config: ConfigType) -> ConfigType: # Set defaults for read_command and write_command based on legacy_mode legacy = config.get(CONF_LEGACY_MODE, True) if legacy: @@ -118,7 +121,7 @@ def set_command_defaults(config): return config -def set_reference_values(config): +def set_reference_values(config: ConfigType) -> ConfigType: # Set default reference values based on legacy_mode if config.get(CONF_LEGACY_MODE, True): config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) @@ -223,7 +226,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py index ba264f00bf..5ef162bb7c 100644 --- a/esphome/components/bm8563/time.py +++ b/esphome/components/bm8563/time.py @@ -3,6 +3,9 @@ import esphome.codegen as cg from esphome.components import i2c, time import esphome.config_validation as cv from esphome.const import CONF_DURATION, CONF_ID +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -35,7 +38,12 @@ CONFIG_SCHEMA = ( ), synchronous=True, ) -async def bm8563_write_time_to_code(config, action_id, template_arg, args): +async def bm8563_write_time_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var @@ -52,7 +60,12 @@ async def bm8563_write_time_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def bm8563_start_timer_to_code(config, action_id, template_arg, args): +async def bm8563_start_timer_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) @@ -70,13 +83,18 @@ async def bm8563_start_timer_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def bm8563_read_time_to_code(config, action_id, template_arg, args): +async def bm8563_read_time_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index e205e4b910..881107b713 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -16,14 +16,15 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_MOTION, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_device_class, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@nohat"] IS_PLATFORM_COMPONENT = True @@ -93,7 +94,9 @@ _CALLBACK_AUTOMATIONS = ( @setup_entity("event") -async def setup_event_core_(var, config, *, event_types: list[str]): +async def setup_event_core_( + var: MockObj, config: ConfigType, *, event_types: list[str] +) -> None: await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) cg.add(var.set_event_types(event_types)) @@ -108,7 +111,9 @@ async def setup_event_core_(var, config, *, event_types: list[str]): await web_server.add_entity_config(var, web_server_config) -async def register_event(var, config, *, event_types: list[str]): +async def register_event( + var: MockObj, config: ConfigType, *, event_types: list[str] +) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) queue_entity_register("event", config) @@ -116,7 +121,7 @@ async def register_event(var, config, *, event_types: list[str]): await setup_event_core_(var, config, event_types=event_types) -async def new_event(config, *, event_types: list[str]): +async def new_event(config: ConfigType, *, event_types: list[str]) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) await register_event(var, config, event_types=event_types) return var @@ -133,7 +138,12 @@ TRIGGER_EVENT_SCHEMA = cv.Schema( @automation.register_action( "event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA, synchronous=True ) -async def event_fire_to_code(config, action_id, template_arg, args): +async def event_fire_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) templ = await cg.templatable(config[CONF_EVENT_TYPE], args, cg.std_string) @@ -142,5 +152,5 @@ async def event_fire_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(event_ns.using) diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index 360e56330a..19786f38d3 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -4,6 +4,9 @@ import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_THROTTLE, CONF_TIMEOUT +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] @@ -69,7 +72,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) @@ -102,7 +105,12 @@ BLUETOOTH_PASSWORD_SET_SCHEMA = cv.Schema( BLUETOOTH_PASSWORD_SET_SCHEMA, synchronous=True, ) -async def bluetooth_password_set_to_code(config, action_id, template_arg, args): +async def bluetooth_password_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_PASSWORD], args, cg.std_string) diff --git a/esphome/components/ld2410/binary_sensor.py b/esphome/components/ld2410/binary_sensor.py index fb5b5cabff..2b68733532 100644 --- a/esphome/components/ld2410/binary_sensor.py +++ b/esphome/components/ld2410/binary_sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( ICON_ACCOUNT, ICON_MOTION_SENSOR, ) +from esphome.types import ConfigType from . import CONF_LD2410_ID, LD2410Component @@ -46,7 +47,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if has_target_config := config.get(CONF_HAS_TARGET): sens = await binary_sensor.new_binary_sensor(has_target_config) diff --git a/esphome/components/ld2410/button/__init__.py b/esphome/components/ld2410/button/__init__.py index fa6f31ee25..59a9558331 100644 --- a/esphome/components/ld2410/button/__init__.py +++ b/esphome/components/ld2410/button/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( ICON_RESTART, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns @@ -44,7 +45,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if factory_reset_config := config.get(CONF_FACTORY_RESET): b = await button.new_button(factory_reset_config) diff --git a/esphome/components/ld2410/number/__init__.py b/esphome/components/ld2410/number/__init__.py index 01dbcc785d..3500d704a1 100644 --- a/esphome/components/ld2410/number/__init__.py +++ b/esphome/components/ld2410/number/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_SECOND, ) +from esphome.types import ConfigType from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns @@ -85,7 +86,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if timeout_config := config.get(CONF_TIMEOUT): n = await number.new_number( diff --git a/esphome/components/ld2410/select/__init__.py b/esphome/components/ld2410/select/__init__.py index 9c4f654aa1..e89e3d5997 100644 --- a/esphome/components/ld2410/select/__init__.py +++ b/esphome/components/ld2410/select/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ICON_SCALE, ICON_THERMOMETER, ) +from esphome.types import ConfigType from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns @@ -48,7 +49,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION): s = await select.new_select( diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index 459018e263..ca42b3a1d3 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -15,6 +15,7 @@ from esphome.const import ( UNIT_CENTIMETER, UNIT_PERCENT, ) +from esphome.types import ConfigType from . import CONF_LD2410_ID, LD2410Component @@ -155,7 +156,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if moving_distance_config := config.get(CONF_MOVING_DISTANCE): sens = await sensor.new_sensor(moving_distance_config) diff --git a/esphome/components/ld2410/switch/__init__.py b/esphome/components/ld2410/switch/__init__.py index 4276b28a71..6d8053ddd6 100644 --- a/esphome/components/ld2410/switch/__init__.py +++ b/esphome/components/ld2410/switch/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_PULSE, ) +from esphome.types import ConfigType from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns @@ -35,7 +36,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if engineering_mode_config := config.get(CONF_ENGINEERING_MODE): s = await switch.new_switch(engineering_mode_config) diff --git a/esphome/components/ld2410/text_sensor.py b/esphome/components/ld2410/text_sensor.py index a34c8ec0d2..25c61a4825 100644 --- a/esphome/components/ld2410/text_sensor.py +++ b/esphome/components/ld2410/text_sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_CHIP, ) +from esphome.types import ConfigType from . import CONF_LD2410_ID, LD2410Component @@ -26,7 +27,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) if version_config := config.get(CONF_VERSION): sens = await text_sensor.new_text_sensor(version_config) diff --git a/esphome/components/ld2412/__init__.py b/esphome/components/ld2412/__init__.py index e701d0bda9..82db319861 100644 --- a/esphome/components/ld2412/__init__.py +++ b/esphome/components/ld2412/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_THROTTLE +from esphome.types import ConfigType AUTO_LOAD = ["ld24xx"] CODEOWNERS = ["@Rihan9"] @@ -40,7 +41,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/ld2412/binary_sensor.py b/esphome/components/ld2412/binary_sensor.py index 98fa5965cd..80cff014c0 100644 --- a/esphome/components/ld2412/binary_sensor.py +++ b/esphome/components/ld2412/binary_sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( ICON_ACCOUNT, ICON_MOTION_SENSOR, ) +from esphome.types import ConfigType from . import CONF_LD2412_ID, LD2412Component @@ -48,7 +49,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if dynamic_background_correction_status_config := config.get( CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS diff --git a/esphome/components/ld2412/button/__init__.py b/esphome/components/ld2412/button/__init__.py index e0ca285265..5a1ea2e6a5 100644 --- a/esphome/components/ld2412/button/__init__.py +++ b/esphome/components/ld2412/button/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( ICON_RESTART, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component @@ -54,7 +55,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if factory_reset_config := config.get(CONF_FACTORY_RESET): b = await button.new_button(factory_reset_config) diff --git a/esphome/components/ld2412/number/__init__.py b/esphome/components/ld2412/number/__init__.py index b6e1c8d039..1a81c330ad 100644 --- a/esphome/components/ld2412/number/__init__.py +++ b/esphome/components/ld2412/number/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_SECOND, ) +from esphome.types import ConfigType from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component @@ -85,7 +86,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD): n = await number.new_number( diff --git a/esphome/components/ld2412/select/__init__.py b/esphome/components/ld2412/select/__init__.py index a54cd700ed..02ecf2c30f 100644 --- a/esphome/components/ld2412/select/__init__.py +++ b/esphome/components/ld2412/select/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ICON_SCALE, ICON_THERMOMETER, ) +from esphome.types import ConfigType from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component @@ -48,7 +49,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if baud_rate_config := config.get(CONF_BAUD_RATE): s = await select.new_select( diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py index f562afe0ee..0b6e676931 100644 --- a/esphome/components/ld2412/sensor.py +++ b/esphome/components/ld2412/sensor.py @@ -16,6 +16,7 @@ from esphome.const import ( UNIT_EMPTY, UNIT_PERCENT, ) +from esphome.types import ConfigType from . import CONF_LD2412_ID, LD2412Component @@ -156,7 +157,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if detection_distance_config := config.get(CONF_DETECTION_DISTANCE): sens = await sensor.new_sensor(detection_distance_config) diff --git a/esphome/components/ld2412/switch/__init__.py b/esphome/components/ld2412/switch/__init__.py index 7a87e9e483..e7f71222fd 100644 --- a/esphome/components/ld2412/switch/__init__.py +++ b/esphome/components/ld2412/switch/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_PULSE, ) +from esphome.types import ConfigType from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component @@ -35,7 +36,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if bluetooth_config := config.get(CONF_BLUETOOTH): s = await switch.new_switch(bluetooth_config) diff --git a/esphome/components/ld2412/text_sensor.py b/esphome/components/ld2412/text_sensor.py index 22fba5193e..c8e9f42ef3 100644 --- a/esphome/components/ld2412/text_sensor.py +++ b/esphome/components/ld2412/text_sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_CHIP, ) +from esphome.types import ConfigType from . import CONF_LD2412_ID, LD2412Component @@ -26,7 +27,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) if version_config := config.get(CONF_VERSION): sens = await text_sensor.new_text_sensor(version_config) diff --git a/esphome/components/ld2420/__init__.py b/esphome/components/ld2420/__init__.py index 71a5fa13e4..5a5aabeba0 100644 --- a/esphome/components/ld2420/__init__.py +++ b/esphome/components/ld2420/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType CODEOWNERS = ["@descipher"] @@ -33,7 +34,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/ld2420/binary_sensor/__init__.py b/esphome/components/ld2420/binary_sensor/__init__.py index 5ebc4a9f63..76b42c0362 100644 --- a/esphome/components/ld2420/binary_sensor/__init__.py +++ b/esphome/components/ld2420/binary_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv from esphome.const import CONF_HAS_TARGET, CONF_ID, DEVICE_CLASS_OCCUPANCY +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -23,7 +24,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) if CONF_HAS_TARGET in config: diff --git a/esphome/components/ld2420/button/__init__.py b/esphome/components/ld2420/button/__init__.py index dfeb121c91..cfcffd0922 100644 --- a/esphome/components/ld2420/button/__init__.py +++ b/esphome/components/ld2420/button/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ICON_RESTART, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -50,7 +51,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2420_component = await cg.get_variable(config[CONF_LD2420_ID]) if apply_config := config.get(CONF_APPLY_CONFIG): b = await button.new_button(apply_config) diff --git a/esphome/components/ld2420/number/__init__.py b/esphome/components/ld2420/number/__init__.py index a2637b7b06..448639c911 100644 --- a/esphome/components/ld2420/number/__init__.py +++ b/esphome/components/ld2420/number/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( ICON_TIMELAPSE, UNIT_SECOND, ) +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -113,7 +114,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2420_component = await cg.get_variable(config[CONF_LD2420_ID]) if gate_timeout_config := config.get(CONF_PRESENCE_TIMEOUT): n = await number.new_number( diff --git a/esphome/components/ld2420/select/__init__.py b/esphome/components/ld2420/select/__init__.py index b9059c120f..cd66064e47 100644 --- a/esphome/components/ld2420/select/__init__.py +++ b/esphome/components/ld2420/select/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import select import esphome.config_validation as cv from esphome.const import ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -23,7 +24,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: LD2420_component = await cg.get_variable(config[CONF_LD2420_ID]) if operating_mode_config := config.get(CONF_OPERATING_MODE): sel = await select.new_select( diff --git a/esphome/components/ld2420/sensor/__init__.py b/esphome/components/ld2420/sensor/__init__.py index 97acdabd7b..f98d63585b 100644 --- a/esphome/components/ld2420/sensor/__init__.py +++ b/esphome/components/ld2420/sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_CENTIMETER, ) +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -30,7 +31,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) if CONF_MOVING_DISTANCE in config: diff --git a/esphome/components/ld2420/text_sensor/__init__.py b/esphome/components/ld2420/text_sensor/__init__.py index 14d982e5fb..cee8f25c1f 100644 --- a/esphome/components/ld2420/text_sensor/__init__.py +++ b/esphome/components/ld2420/text_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_ID, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP +from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns @@ -24,7 +25,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) if CONF_FW_VERSION in config: diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index 585c9f7bf5..4c37f4fcd1 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE +from esphome.types import ConfigType AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] @@ -49,7 +50,7 @@ _CALLBACK_AUTOMATIONS = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/ld2450/binary_sensor.py b/esphome/components/ld2450/binary_sensor.py index 89e629253a..779d151fd9 100644 --- a/esphome/components/ld2450/binary_sensor.py +++ b/esphome/components/ld2450/binary_sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, ) +from esphome.types import ConfigType from . import CONF_LD2450_ID, LD2450Component @@ -39,7 +40,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if has_target_config := config.get(CONF_HAS_TARGET): sens = await binary_sensor.new_binary_sensor(has_target_config) diff --git a/esphome/components/ld2450/button/__init__.py b/esphome/components/ld2450/button/__init__.py index 682487d750..42cadd2052 100644 --- a/esphome/components/ld2450/button/__init__.py +++ b/esphome/components/ld2450/button/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( ICON_RESTART, ICON_RESTART_ALERT, ) +from esphome.types import ConfigType from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns @@ -35,7 +36,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if factory_reset_config := config.get(CONF_FACTORY_RESET): b = await button.new_button(factory_reset_config) diff --git a/esphome/components/ld2450/number/__init__.py b/esphome/components/ld2450/number/__init__.py index 799c0703f2..4f242076d6 100644 --- a/esphome/components/ld2450/number/__init__.py +++ b/esphome/components/ld2450/number/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( UNIT_MILLIMETER, UNIT_SECOND, ) +from esphome.types import ConfigType from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns @@ -78,7 +79,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if presence_timeout_config := config.get(CONF_PRESENCE_TIMEOUT): n = await number.new_number( diff --git a/esphome/components/ld2450/select/__init__.py b/esphome/components/ld2450/select/__init__.py index 4f237dc94f..d91b42426a 100644 --- a/esphome/components/ld2450/select/__init__.py +++ b/esphome/components/ld2450/select/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ICON_THERMOMETER, ) +from esphome.types import ConfigType from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns @@ -31,7 +32,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if baud_rate_config := config.get(CONF_BAUD_RATE): s = await select.new_select( diff --git a/esphome/components/ld2450/sensor.py b/esphome/components/ld2450/sensor.py index ae13900e7a..40462e202d 100644 --- a/esphome/components/ld2450/sensor.py +++ b/esphome/components/ld2450/sensor.py @@ -15,6 +15,7 @@ from esphome.const import ( UNIT_DEGREES, UNIT_MILLIMETER, ) +from esphome.types import ConfigType from . import CONF_LD2450_ID, LD2450Component @@ -226,7 +227,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if target_count_config := config.get(CONF_TARGET_COUNT): diff --git a/esphome/components/ld2450/switch/__init__.py b/esphome/components/ld2450/switch/__init__.py index 0c0c92377b..084f79ee1b 100644 --- a/esphome/components/ld2450/switch/__init__.py +++ b/esphome/components/ld2450/switch/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( ICON_BLUETOOTH, ICON_PULSE, ) +from esphome.types import ConfigType from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns @@ -35,7 +36,7 @@ CONFIG_SCHEMA = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if bluetooth_config := config.get(CONF_BLUETOOTH): s = await switch.new_switch(bluetooth_config) diff --git a/esphome/components/ld2450/text_sensor.py b/esphome/components/ld2450/text_sensor.py index 4e5d7d419b..a8b978ef48 100644 --- a/esphome/components/ld2450/text_sensor.py +++ b/esphome/components/ld2450/text_sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( ICON_CHIP, ICON_SIGN_DIRECTION, ) +from esphome.types import ConfigType from . import CONF_LD2450_ID, LD2450Component @@ -49,7 +50,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) if version_config := config.get(CONF_VERSION): sens = await text_sensor.new_text_sensor(version_config) diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index e9fae4cceb..5e45d71899 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -11,6 +11,9 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLUP, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@looping40"] @@ -54,7 +57,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) @@ -62,7 +65,7 @@ async def to_code(config): cg.add(var.set_brightness_global(config[CONF_BRIGHTNESS_GLOBAL])) -def validate_mode(value): +def validate_mode(value: ConfigType) -> ConfigType: if not (value[CONF_INPUT] or value[CONF_OUTPUT]): raise cv.Invalid("Mode must be either input or output") if value[CONF_INPUT] and value[CONF_OUTPUT]: @@ -87,7 +90,7 @@ MAX6956_PIN_SCHEMA = pins.gpio_base_schema( @pins.PIN_SCHEMA_REGISTRY.register(CONF_MAX6956, MAX6956_PIN_SCHEMA) -async def max6956_pin_to_code(config): +async def max6956_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_MAX6956]) @@ -114,7 +117,12 @@ async def max6956_pin_to_code(config): ), synchronous=True, ) -async def max6956_set_brightness_global_to_code(config, action_id, template_arg, args): +async def max6956_set_brightness_global_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_BRIGHTNESS_GLOBAL], args, cg.uint8) @@ -136,7 +144,12 @@ async def max6956_set_brightness_global_to_code(config, action_id, template_arg, ), synchronous=True, ) -async def max6956_set_brightness_mode_to_code(config, action_id, template_arg, args): +async def max6956_set_brightness_mode_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable( diff --git a/esphome/components/max6956/output/__init__.py b/esphome/components/max6956/output/__init__.py index 352ba04a95..f92bbb762a 100644 --- a/esphome/components/max6956/output/__init__.py +++ b/esphome/components/max6956/output/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PIN +from esphome.types import ConfigType from .. import CONF_MAX6956, MAX6956, max6956_ns @@ -20,7 +21,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_MAX6956]) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index df2423b0d0..54711263dd 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -10,6 +10,9 @@ from esphome.const import ( CONF_NUM_CHIPS, CONF_STATE, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType CODEOWNERS = ["@rspaargaren"] DEPENDENCIES = ["spi"] @@ -84,7 +87,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await spi.register_spi_device(var, config, write_only=True) await display.register_display(var, config) @@ -144,7 +147,12 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( MAX7219_ON_ACTION_SCHEMA, synchronous=True, ) -async def max7219digit_invert_to_code(config, action_id, template_arg, args): +async def max7219digit_invert_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) @@ -164,7 +172,12 @@ async def max7219digit_invert_to_code(config, action_id, template_arg, args): MAX7219_ON_ACTION_SCHEMA, synchronous=True, ) -async def max7219digit_visible_to_code(config, action_id, template_arg, args): +async def max7219digit_visible_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) @@ -184,7 +197,12 @@ async def max7219digit_visible_to_code(config, action_id, template_arg, args): MAX7219_ON_ACTION_SCHEMA, synchronous=True, ) -async def max7219digit_reverse_to_code(config, action_id, template_arg, args): +async def max7219digit_reverse_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) @@ -209,7 +227,12 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( MAX7219_INTENSITY_SCHEMA, synchronous=True, ) -async def max7219digit_intensity_to_code(config, action_id, template_arg, args): +async def max7219digit_intensity_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8) diff --git a/esphome/components/micronova/__init__.py b/esphome/components/micronova/__init__.py index b462352229..ff06d0b913 100644 --- a/esphome/components/micronova/__init__.py +++ b/esphome/components/micronova/__init__.py @@ -7,6 +7,8 @@ import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@jorre05", "@edenhaus"] @@ -63,7 +65,7 @@ def MICRONOVA_ADDRESS_SCHEMA( default_memory_location: int | None = None, default_memory_address: int | None = None, is_polling_component: bool, -): +) -> cv.Schema: location_key = ( cv.Optional(CONF_MEMORY_LOCATION, default=default_memory_location) if default_memory_location is not None @@ -91,7 +93,9 @@ def register_micronova_writer() -> None: _get_data().has_writer = True -async def to_code_micronova_listener(mv, var, config): +async def to_code_micronova_listener( + mv: MockObj, var: MockObj, config: ConfigType +) -> None: _get_data().listener_count += 1 await cg.register_component(var, config) cg.add(var.set_memory_location(config[CONF_MEMORY_LOCATION])) @@ -100,7 +104,7 @@ async def to_code_micronova_listener(mv, var, config): cg.add(mv.register_micronova_listener(var)) -async def to_code(config): +async def to_code(config: ConfigType) -> None: enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN]) var = cg.new_Pvariable(config[CONF_ID], enable_rx_pin) await cg.register_component(var, config) diff --git a/esphome/components/micronova/button/__init__.py b/esphome/components/micronova/button/__init__.py index 63b127e63d..68b5b9aca6 100644 --- a/esphome/components/micronova/button/__init__.py +++ b/esphome/components/micronova/button/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import button import esphome.config_validation as cv +from esphome.types import ConfigType from .. import ( CONF_MEMORY_ADDRESS, @@ -33,7 +34,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if custom_button_config := config.get(CONF_CUSTOM_BUTTON): diff --git a/esphome/components/micronova/number/__init__.py b/esphome/components/micronova/number/__init__.py index bcc972c5a9..d33bb150ce 100644 --- a/esphome/components/micronova/number/__init__.py +++ b/esphome/components/micronova/number/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv from esphome.const import CONF_STEP, DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS +from esphome.types import ConfigType from .. import ( CONF_MICRONOVA_ID, @@ -56,7 +57,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if thermostat_temperature_config := config.get(CONF_THERMOSTAT_TEMPERATURE): diff --git a/esphome/components/micronova/sensor/__init__.py b/esphome/components/micronova/sensor/__init__.py index e53c49aca5..6091718d65 100644 --- a/esphome/components/micronova/sensor/__init__.py +++ b/esphome/components/micronova/sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_REVOLUTIONS_PER_MINUTE, ) +from esphome.types import ConfigType from .. import ( CONF_MICRONOVA_ID, @@ -125,7 +126,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) for key, divisor in { diff --git a/esphome/components/micronova/switch/__init__.py b/esphome/components/micronova/switch/__init__.py index e149ee3ce3..1f57497ad7 100644 --- a/esphome/components/micronova/switch/__init__.py +++ b/esphome/components/micronova/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import ICON_POWER +from esphome.types import ConfigType from .. import ( CONF_MICRONOVA_ID, @@ -49,7 +50,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if stove_config := config.get(CONF_STOVE): diff --git a/esphome/components/micronova/text_sensor/__init__.py b/esphome/components/micronova/text_sensor/__init__.py index 33d0779eae..d6b94c437f 100644 --- a/esphome/components/micronova/text_sensor/__init__.py +++ b/esphome/components/micronova/text_sensor/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv +from esphome.types import ConfigType from .. import ( CONF_MICRONOVA_ID, @@ -33,7 +34,7 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if stove_state_config := config.get(CONF_STOVE_STATE): diff --git a/esphome/components/pipsolar/__init__.py b/esphome/components/pipsolar/__init__.py index e3966aa2cc..b404409145 100644 --- a/esphome/components/pipsolar/__init__.py +++ b/esphome/components/pipsolar/__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 DEPENDENCIES = ["uart"] CODEOWNERS = ["@andreashergert1984"] @@ -26,7 +27,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/pipsolar/binary_sensor/__init__.py b/esphome/components/pipsolar/binary_sensor/__init__.py index 5bcf1f75ee..62c0ed8538 100644 --- a/esphome/components/pipsolar/binary_sensor/__init__.py +++ b/esphome/components/pipsolar/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_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA @@ -132,7 +133,7 @@ CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) for type in TYPES: if type in config: diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py index d1ea981589..62e6d0f113 100644 --- a/esphome/components/pipsolar/output/__init__.py +++ b/esphome/components/pipsolar/output/__init__.py @@ -3,6 +3,9 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_VALUE +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns @@ -75,7 +78,7 @@ CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) for type, (_, command) in TYPES.items(): @@ -100,7 +103,12 @@ async def to_code(config): ), synchronous=True, ) -async def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): +async def output_pipsolar_set_level_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.float_) diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index 88c6566d63..5a697157b7 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -25,6 +25,7 @@ from esphome.const import ( UNIT_VOLT_AMPS, UNIT_WATT, ) +from esphome.types import ConfigType from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA @@ -325,7 +326,7 @@ CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) for type in TYPES: diff --git a/esphome/components/pipsolar/switch/__init__.py b/esphome/components/pipsolar/switch/__init__.py index 11dbc91110..2b493eac95 100644 --- a/esphome/components/pipsolar/switch/__init__.py +++ b/esphome/components/pipsolar/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import ICON_POWER +from esphome.types import ConfigType from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns @@ -36,7 +37,7 @@ CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) for type, (on, off) in TYPES.items(): diff --git a/esphome/components/pipsolar/text_sensor/__init__.py b/esphome/components/pipsolar/text_sensor/__init__.py index 90ce3a7e55..cc7477395b 100644 --- a/esphome/components/pipsolar/text_sensor/__init__.py +++ b/esphome/components/pipsolar/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_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA @@ -31,7 +32,7 @@ CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) for type in TYPES: diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 06b5a10892..e010e2c292 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -13,13 +13,14 @@ from esphome.const import ( CONF_VALUE, 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 CODEOWNERS = ["@mauritskorse"] IS_PLATFORM_COMPONENT = True @@ -90,13 +91,13 @@ def text_schema( @setup_entity("text") async def setup_text_core_( - var, - config, + var: MockObj, + config: ConfigType, *, min_length: int | None, max_length: int | None, pattern: str | None, -): +) -> None: cg.add(var.traits.set_min_length(min_length)) cg.add(var.traits.set_max_length(max_length)) if pattern is not None: @@ -117,13 +118,13 @@ async def setup_text_core_( async def register_text( - var, - config, + var: MockObj, + config: ConfigType, *, min_length: int | None = 0, max_length: int | None = 255, pattern: str | None = None, -): +) -> None: if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) queue_entity_register("text", config) @@ -134,12 +135,12 @@ async def register_text( async def new_text( - config, + config: ConfigType, *, min_length: int | None = 0, max_length: int | None = 255, pattern: str | None = None, -): +) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) await register_text( var, config, min_length=min_length, max_length=max_length, pattern=pattern @@ -148,7 +149,7 @@ async def new_text( @coroutine_with_priority(CoroPriority.CORE) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(text_ns.using) @@ -169,7 +170,12 @@ OPERATION_BASE_SCHEMA = cv.Schema( ), synchronous=True, ) -async def text_set_to_code(config, action_id, template_arg, args): +async def text_set_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_VALUE], args, cg.std_string) diff --git a/esphome/components/text/text_sensor/__init__.py b/esphome/components/text/text_sensor/__init__.py index 5e45f10193..ab0e9bdcdc 100644 --- a/esphome/components/text/text_sensor/__init__.py +++ b/esphome/components/text/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_SOURCE_ID +from esphome.types import ConfigType from .. import Text, text_ns @@ -19,7 +20,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: source = await cg.get_variable(config[CONF_SOURCE_ID]) var = await text_sensor.new_text_sensor(config, source) await cg.register_component(var, config) From 7fe4399b945e242cf07ac8f7aa830976d1c850d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 11:11:53 -0500 Subject: [PATCH 018/155] [esp32] Grow the default IDF component exclusion list (#18536) --- esphome/components/ac_dimmer/output.py | 6 ++ esphome/components/esp32/__init__.py | 25 ++++++++- esphome/components/http_request/__init__.py | 5 +- esphome/components/i2c/__init__.py | 5 ++ esphome/components/ledc/output.py | 4 ++ esphome/components/mqtt/__init__.py | 2 + esphome/components/nextion/display.py | 2 + esphome/components/web_server_idf/__init__.py | 8 ++- .../esp32/config/exclusion_reincludes.yaml | 20 +++++++ .../exclusion_reincludes_http_request.yaml | 14 +++++ .../config/exclusion_reincludes_mqtt.yaml | 14 +++++ .../config/exclusion_reincludes_nextion.yaml | 20 +++++++ .../exclusion_reincludes_web_server.yaml | 14 +++++ tests/component_tests/esp32/test_esp32.py | 56 +++++++++++++++++++ 14 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes.yaml create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes_http_request.yaml create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes_mqtt.yaml create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes_nextion.yaml create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes_web_server.yaml diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index 1f35095e0e..48bef2c317 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -49,6 +49,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + # Re-enable the gptimer driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_gptimer") + if CORE.is_esp8266: # ac_dimmer uses setTimer1Callback which requires the waveform generator from esphome.components.esp8266.const import require_waveform diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d6e0890751..f1f039922a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -204,18 +204,32 @@ COMPILER_OPTIMIZATIONS = { # ESP-IDF components excluded by default to reduce compile time. # Components can be re-enabled by calling include_builtin_idf_component() in to_code(). # -# Cannot be excluded (dependencies of required components): -# - "console": espressif/mdns unconditionally depends on it -# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain +# Note: excluding a component only removes it from the initial build set. +# ESP-IDF's requirement expansion adds an excluded component back when any +# component still in the build REQUIRES it (e.g. espressif/mdns pulls +# "console" back in, esp_http_client pulls "tcp_transport" back in), so +# exclusions here are safe for such components and simply become no-ops in +# builds that need them. DEFAULT_EXCLUDED_IDF_COMPONENTS = ( + "app_trace", # CPU trace/SystemView support - unused by ESPHome "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing + "console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured "driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers + "esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf "esp_adc", # ADC driver - only needed by adc component + "esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back "esp_driver_dac", # DAC driver - only needed by esp32_dac component + "esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs + "esp_driver_i2c", # I2C driver - re-included by i2c; esp32-camera pulls it back itself "esp_driver_i2s", # I2S driver - only needed by i2s_audio component + "esp_driver_ledc", # LEDC PWM driver - re-included by ledc; esp32-camera pulls it back itself "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM "esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus + "esp_driver_sdio", # SDIO device-mode driver - unused by ESPHome + "esp_driver_sdm", # Sigma-delta modulation driver - unused by ESPHome + "esp_driver_sdmmc", # SD/MMC host driver - unused by ESPHome + "esp_driver_sdspi", # SD-over-SPI driver - unused by ESPHome "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component "esp_eth", # Ethernet driver - only needed by ethernet component @@ -227,11 +241,16 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API "espcoredump", # Core dump support - ESPHome has its own debug component "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage + "json", # cJSON library - ESPHome uses ArduinoJson instead "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation "openthread", # Thread protocol - only needed by openthread component "perfmon", # Xtensa performance monitor - ESPHome has its own debug component + "protobuf-c", # Protobuf runtime - only used by provisioning components (also excluded) "protocomm", # Protocol communication for provisioning - unused by ESPHome + "rt", # POSIX realtime extensions - unused by ESPHome + "sdmmc", # SD/MMC protocol layer - only used by SD drivers and fatfs (also excluded) "spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only) + "tcp_transport", # Transport layer - esp_http_client/mqtt pull it back when re-included "ulp", # ULP coprocessor - not currently used by any ESPHome component "unity", # Unit testing framework - ESPHome doesn't use IDF's testing "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index afc39e06a8..8a5aae022a 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -170,8 +170,11 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_watchdog_timeout(timeout_ms)) if CORE.is_esp32: - # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time). + # esp-tls is re-enabled too because http_request includes + # directly and esp_http_client only pulls it in as a private dependency. esp32.include_builtin_idf_component("esp_http_client") + esp32.include_builtin_idf_component("esp-tls") cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 7b163d065e..94aad4d019 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -284,6 +284,11 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): cg.add_global(i2c_ns.using) cg.add_define("USE_I2C") + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + # Re-enable the I2C driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_i2c") if CORE.is_host: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 637e607b6d..e5e7c3dcbe 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -3,6 +3,7 @@ from typing import Any from esphome import automation, pins import esphome.codegen as cg from esphome.components import output +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHANNEL, @@ -62,6 +63,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( async def to_code(config: ConfigType) -> None: + # Re-enable the LEDC driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_ledc") + gpio = await cg.gpio_pin_expression(config[CONF_PIN]) var = cg.new_Pvariable(config[CONF_ID], gpio) await cg.register_component(var, config) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 98ca23b60b..9178bc79e5 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -361,6 +361,8 @@ async def to_code(config): add_idf_component(name="espressif/mqtt", ref="1.0.0") else: include_builtin_idf_component("mqtt") + # mqtt_client.h drags in esp_tls types; esp-tls is excluded by default + include_builtin_idf_component("esp-tls") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 4ab123c354..3f5ba94b40 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -290,7 +290,9 @@ async def to_code(config): if CORE.is_esp32: # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + # and esp-tls, whose sdkconfig options below need the component present esp32.include_builtin_idf_component("esp_http_client") + esp32.include_builtin_idf_component("esp-tls") esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index 74a9d657a6..adf21ddc49 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -1,4 +1,7 @@ -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + include_builtin_idf_component, +) import esphome.config_validation as cv CODEOWNERS = ["@dentra"] @@ -12,3 +15,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024) + # Re-enable esp-tls (excluded by default to save compile time); + # web_server_idf.cpp includes for digest auth + include_builtin_idf_component("esp-tls") diff --git a/tests/component_tests/esp32/config/exclusion_reincludes.yaml b/tests/component_tests/esp32/config/exclusion_reincludes.yaml new file mode 100644 index 0000000000..ba5bf17688 --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes.yaml @@ -0,0 +1,20 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +i2c: + sda: 21 + scl: 22 + +output: + - platform: ledc + id: ledc_out + pin: 25 + - platform: ac_dimmer + id: dimmer_out + gate_pin: 26 + zero_cross_pin: 27 diff --git a/tests/component_tests/esp32/config/exclusion_reincludes_http_request.yaml b/tests/component_tests/esp32/config/exclusion_reincludes_http_request.yaml new file mode 100644 index 0000000000..5adfd66b00 --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes_http_request.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +http_request: + verify_ssl: false diff --git a/tests/component_tests/esp32/config/exclusion_reincludes_mqtt.yaml b/tests/component_tests/esp32/config/exclusion_reincludes_mqtt.yaml new file mode 100644 index 0000000000..c509942635 --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes_mqtt.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +mqtt: + broker: "10.0.0.1" diff --git a/tests/component_tests/esp32/config/exclusion_reincludes_nextion.yaml b/tests/component_tests/esp32/config/exclusion_reincludes_nextion.yaml new file mode 100644 index 0000000000..3c6e527b09 --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes_nextion.yaml @@ -0,0 +1,20 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +uart: + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +display: + - platform: nextion + tft_url: "http://10.0.0.1/display.tft" diff --git a/tests/component_tests/esp32/config/exclusion_reincludes_web_server.yaml b/tests/component_tests/esp32/config/exclusion_reincludes_web_server.yaml new file mode 100644 index 0000000000..6041bffee6 --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes_web_server.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +web_server: + version: 3 diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 1fd835076d..7208318d3a 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -236,6 +236,62 @@ def test_esp32_configuration_errors( FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) +@pytest.mark.parametrize( + ("config_file", "reincluded"), + [ + pytest.param( + "exclusion_reincludes.yaml", + ("esp_driver_i2c", "esp_driver_ledc", "esp_driver_gptimer"), + id="i2c_ledc_ac_dimmer", + ), + # esp-tls has three owners; a per-owner config makes a dropped + # re-include from any single one fail the test. + pytest.param( + "exclusion_reincludes_http_request.yaml", + ("esp-tls", "esp_http_client"), + id="http_request", + ), + pytest.param( + # "mqtt" itself is deliberately not asserted: on IDF >= 6.0 it + # is a managed component and never leaves the exclusion set. + "exclusion_reincludes_mqtt.yaml", + ("esp-tls",), + id="mqtt", + ), + pytest.param( + "exclusion_reincludes_web_server.yaml", + ("esp-tls",), + id="web_server_idf", + ), + pytest.param( + "exclusion_reincludes_nextion.yaml", + ("esp-tls", "esp_http_client"), + id="nextion", + ), + ], +) +def test_default_exclusions_reincluded_by_owning_components( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, + reincluded: tuple[str, ...], +) -> None: + """Components whose IDF driver is excluded by default must re-include it + during codegen; a dropped include_builtin_idf_component() call would only + surface as a missing-header failure in a full compile job.""" + from esphome.components.esp32.const import KEY_EXCLUDE_COMPONENTS + + generate_main(component_config_path(config_file)) + excluded = CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] + + for name in reincluded: + assert name not in excluded, f"{name} should have been re-included" + + # Components no part of this config touches stay excluded. + assert "unity" in excluded + assert "fatfs" in excluded + + def test_execute_from_psram_s3_sdkconfig( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], From a3ea77c2f1206939c0f59aa90870485270998b3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 11:32:15 -0500 Subject: [PATCH 019/155] [core] Keep templated !include filenames as strings so Windows path normalization cannot corrupt them (#18549) --- esphome/components/substitutions/__init__.py | 5 +- esphome/yaml_util.py | 28 ++++++---- tests/unit_tests/test_bundle.py | 56 +++++++++++++++++++- tests/unit_tests/test_substitutions.py | 19 +++++++ tests/unit_tests/test_yaml_util.py | 27 +++++++++- 5 files changed, 120 insertions(+), 15 deletions(-) diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index b4fcf36c9e..5ef7a699eb 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -363,13 +363,12 @@ def resolve_include( an explicit non-goal here. """ original = include.file - original_str = str(original) filename = str( _expand_substitutions( - original_str, path + ["file"], context_vars, strict_undefined, errors + original, path + ["file"], context_vars, strict_undefined, errors ) ) - substituted = filename != original_str + substituted = filename != original if substituted: include = include.with_file(filename) try: diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index d3c6caf60b..c280e550c9 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -231,18 +231,22 @@ class IncludeFile: def __init__( self, parent_file: Path, - file: Path | str, + file: str, vars: dict[str, Any] | None, yaml_loader: Callable[[Path], Any], ) -> None: self.parent_file = parent_file - self.file = Path(file) + # The raw include text may be a substitution/Jinja expression, so it + # must never round-trip through Path(): on Windows, WindowsPath str() + # rewrites "/" to "\", which Jinja then decodes as escapes like + # "\b" -> backspace (issue #18545). + self.file = file self.vars = vars self.yaml_loader = yaml_loader self._content: Any = _UNSET def __repr__(self) -> str: - return f"IncludeFile({self.file.as_posix()})" + return f"IncludeFile({self.file})" def load(self) -> Any: """Load and cache the included file content. @@ -258,15 +262,15 @@ class IncludeFile: raise Invalid( f"Cannot load include with unresolved substitutions: {self.file}" ) - self._content = self.yaml_loader(Path(self.parent_file.parent / self.file)) + self._content = self.yaml_loader(self.parent_file.parent / self.file) self._content = add_context(self._content, self.vars) return self._content def has_unresolved_expressions(self) -> bool: """Check if the filename contains substitution variables or Jinja expressions.""" - return has_substitution_or_expression(str(self.file)) + return has_substitution_or_expression(self.file) - def with_file(self, file: Path | str) -> IncludeFile: + def with_file(self, file: str) -> IncludeFile: """Clone this include with *file* as the filename.""" return IncludeFile(self.parent_file, file, self.vars, self.yaml_loader) @@ -313,7 +317,7 @@ def _candidate_include_paths(include: IncludeFile) -> list[Path]: parent_dir = include.parent_file.parent parent_resolved = include.parent_file.resolve() candidates: list[Path] = [] - for pattern in include_candidate_patterns(str(include.file)): + for pattern in include_candidate_patterns(include.file): if "*" in pattern: matches = sorted(_glob_include_candidates(parent_dir, pattern)) else: @@ -362,7 +366,7 @@ def _load_include_candidates( continue expanded_paths.add(candidate) try: - loaded = include.with_file(candidate).load() + loaded = include.with_file(candidate.as_posix()).load() except (EsphomeError, Invalid) as err: # Unlike an unresolved pattern (expected during the discovery # re-parse), a matched on-disk candidate that fails to load is a @@ -794,6 +798,10 @@ class ESPHomeLoaderMixin: file = fields.get("file") if file is None: raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark) + if not isinstance(file, str): + raise yaml.MarkedYAMLError( + "Include 'file' must be a string", node.start_mark + ) vars = fields.get(CONF_VARS) return file, vars @@ -1333,11 +1341,11 @@ class ESPHomeDumper(yaml.SafeDumper): def represent_include_file(self, value): if value.vars: - mapping = {"file": value.file.as_posix(), "vars": value.vars} + mapping = {"file": value.file, "vars": value.vars} return self.represent_mapping( tag="!include", mapping=mapping, flow_style=False ) - return self.represent_scalar(tag="!include", value=value.file.as_posix()) + return self.represent_scalar(tag="!include", value=value.file) def represent_id(self, value): if is_secret(value.id): diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py index 29e917fe44..1abc7a3ab8 100644 --- a/tests/unit_tests/test_bundle.py +++ b/tests/unit_tests/test_bundle.py @@ -29,8 +29,9 @@ from esphome.bundle import ( read_bundle_manifest, remap_bundle_path, ) +from esphome.components.substitutions import do_substitution_pass from esphome.core import CORE, EsphomeError -from esphome.yaml_util import force_load_include_files +from esphome.yaml_util import force_load_include_files, load_yaml # --------------------------------------------------------------------------- # Helpers @@ -1277,6 +1278,59 @@ def test_discover_files_bundles_all_include_candidates(tmp_path: Path) -> None: assert "includes/empty.yaml" in paths +@pytest.mark.parametrize("enable_proxy", [True, False]) +def test_bundle_roundtrip_templated_include_with_path_separator( + tmp_path: Path, enable_proxy: bool +) -> None: + r"""The issue-18545 flow: a Jinja !include whose branches contain "/" still + resolves after the bundle is extracted on the build server. + + Windows is the leg that regresses: the raw expression text must survive + verbatim, or its separators get rewritten to "\" and Jinja decodes + sequences like "\b" as string escapes. + """ + config_dir = _setup_config_dir( + tmp_path, + files={ + "includes/boards/board.yaml": ( + "packages:\n" + ' - !include ${ "bluetooth/bluetooth_proxy_single_core.yaml"' + ' if enable_bluetooth_proxy else "../empty.yaml" }\n' + ), + "includes/boards/bluetooth/bluetooth_proxy_single_core.yaml": ( + "bluetooth_proxy:\n active: true\n" + ), + "includes/empty.yaml": "{}\n", + }, + ) + (config_dir / "test.yaml").write_text( + "substitutions:\n" + f" enable_bluetooth_proxy: {str(enable_proxy).lower()}\n" + "esphome:\n name: test\n" + "packages:\n - !include includes/boards/board.yaml\n" + ) + + result = ConfigBundleCreator({}).create_bundle() + bundle_path = tmp_path / "device.esphomebundle.tar.gz" + bundle_path.write_bytes(result.data) + + # Both conditional branches must ship in the bundle. + paths = [f.path for f in result.files] + assert "includes/boards/bluetooth/bluetooth_proxy_single_core.yaml" in paths + assert "includes/empty.yaml" in paths + + # Extract to a fresh directory and resolve the config from there, as a + # remote build server would. + extracted_config = extract_bundle(bundle_path, tmp_path / "remote") + config = do_substitution_pass(load_yaml(extracted_config)) + + board_pkg = config["packages"][0]["packages"][0] + if enable_proxy: + assert board_pkg == {"bluetooth_proxy": {"active": True}} + else: + assert board_pkg == {} + + def test_discover_files_candidate_outside_config_dir_skipped( tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index f4063237b1..73c6e496a9 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -744,6 +744,25 @@ def test_include_filename_substitution_undefined_var(tmp_path: Path) -> None: substitutions.do_substitution_pass(config) +def test_include_filename_jinja_expression_with_path_separator( + tmp_path: Path, +) -> None: + """A jinja !include whose string literals contain "/" resolves correctly (issue #18545).""" + main_file = tmp_path / "main.yaml" + main_file.write_text( + "substitutions:\n" + " enable_bluetooth_proxy: true\n" + "result: !include " + '${ "bluetooth/proxy.yaml" if enable_bluetooth_proxy else "../empty.yaml" }\n' + ) + (tmp_path / "bluetooth").mkdir() + (tmp_path / "bluetooth" / "proxy.yaml").write_text("value: 42\n") + + config = yaml_util.load_yaml(main_file) + config = substitutions.do_substitution_pass(config) + assert config["result"] == {"value": 42} + + def test_raise_first_undefined_logs_extras_at_debug( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index e0a81652e3..3bdbd04396 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -701,6 +701,31 @@ def test_include_file_has_unresolved_expressions( assert include.has_unresolved_expressions() == expected +def test_mapping_include_non_string_file_rejected(tmp_path: Path) -> None: + """The mapping !include form rejects a non-string 'file' with a clear error.""" + entry = tmp_path / "entry.yaml" + entry.write_text("wifi: !include\n file: [not, a, string]\n") + with pytest.raises(EsphomeError, match="Include 'file' must be a string"): + yaml_util.load_yaml(entry) + + +def test_include_file_templated_filename_stays_raw_string(tmp_path: Path) -> None: + """A templated filename keeps its verbatim text (issue #18545).""" + parent = tmp_path / "main.yaml" + expr = '${ "bluetooth/proxy.yaml" if enable_bluetooth_proxy else "../empty.yaml" }' + include = yaml_util.IncludeFile(parent, expr, None, lambda _: {}) + assert include.file == expr + assert include.has_unresolved_expressions() + assert repr(include) == f"IncludeFile({expr})" + + +def test_represent_include_file_templated() -> None: + """Dumping a templated IncludeFile emits the raw expression unchanged.""" + expr = '${ "a/b.yaml" if flag else "../c.yaml" }' + include = yaml_util.IncludeFile(Path("/fake/main.yaml"), expr, None, lambda _: {}) + assert yaml_util.dump({"key": include}) == f"key: !include '{expr}'\n" + + def test_include_in_list_context() -> None: """!include of a file returning a list is handled correctly, including when that list itself contains a nested IncludeFile.""" @@ -1051,7 +1076,7 @@ class _StubInclude: ) -> None: # Default parent lives in a nonexistent directory so unresolved # stubs never glob real files during candidate expansion. - self.file = Path(file) + self.file = file self.parent_file = parent_file or Path("/nonexistent/parent.yaml") self._unresolved = unresolved self._load_result = load_result if load_result is not None else {} From 2ab09e1a77227718e1d9318319e8745bcfab01ac Mon Sep 17 00:00:00 2001 From: David van 't Wout Date: Thu, 20 Aug 2026 19:30:33 +0200 Subject: [PATCH 020/155] [core] Add add_cmake_arg (#18498) --- esphome/build_gen/espidf.py | 38 +++++++++------- esphome/build_gen/platformio.py | 11 +++++ esphome/codegen.py | 1 + esphome/components/esp32/__init__.py | 19 ++++---- esphome/core/__init__.py | 35 ++++++++++++++- esphome/cpp_generator.py | 5 +++ tests/unit_tests/build_gen/test_espidf.py | 27 ++++++++++++ tests/unit_tests/build_gen/test_platformio.py | 44 +++++++++++++++++++ tests/unit_tests/test_core.py | 32 ++++++++++++++ 9 files changed, 187 insertions(+), 25 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index b65ce23307..5d4e6b8401 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -72,6 +72,13 @@ def has_discovered_components() -> bool: return get_available_components() is not None +def _cmake_quote(value: str) -> str: + """Quote a cmake arg value for a set() line. add_cmake_arg rejects + whitespace, quotes, and '$', so only backslashes need escaping.""" + escaped = value.replace("\\", "\\\\") + return f'"{escaped}"' + + def get_project_cmakelists(minimal: bool = False) -> str: """Generate the top-level CMakeLists.txt for ESP-IDF project. @@ -114,6 +121,15 @@ def get_project_cmakelists(minimal: bool = False) -> str: else "" ) + # CMake variables registered via cg.add_cmake_arg(). Emitted before + # include(project.cmake) so values like EXCLUDE_COMPONENTS are already + # set when project.cmake seeds the component list, and on minimal + # (discovery) writes too so excluded components never register. + cmake_args = "\n".join( + f"set({name} {_cmake_quote(value)})" + for name, value in sorted(CORE.cmake_args.items()) + ) + # Per-project list exposed as a CMake variable so converted PIO libs # can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking # project-specific names into their cached CMakeLists. @@ -129,18 +145,6 @@ def get_project_cmakelists(minimal: bool = False) -> str: for name in get_managed_component_require_names() ) - # Components excluded from the build (DEFAULT_EXCLUDED_IDF_COMPONENTS - # minus per-component re-includes). project.cmake reads the plain - # EXCLUDE_COMPONENTS variable when seeding the component list, so this - # must be set before project(). Emitted on minimal writes too so the - # discovery reconfigure never registers the excluded components. - excluded_components = get_excluded_builtin_components() - exclude_components_var = ( - f'set(EXCLUDE_COMPONENTS "{";".join(excluded_components)}")' - if excluded_components - else "" - ) - # Built-in IDF components exposed via our own property (not IDF's # __COMPONENT_REQUIRES_COMMON, which would append them to every # component's REQUIRES including real IDF components). Referenced by @@ -150,13 +154,17 @@ def get_project_cmakelists(minimal: bool = False) -> str: # project_description.json from a build without exclusions may still # list them, and requiring an excluded component pulls it back into # the build (IDF requirement expansion overrides EXCLUDE_COMPONENTS). + # Derived from the EXCLUDE_COMPONENTS cmake arg emitted above so the + # two can never disagree within one generated file. builtin_components_property = ( "" if minimal else "\n".join( f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)" for name in sorted( - set(get_available_components() or []).difference(excluded_components) + set(get_available_components() or []).difference( + CORE.cmake_args.get("EXCLUDE_COMPONENTS", "").split(";") + ) ) ) ) @@ -184,9 +192,9 @@ set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1) set(IDF_TARGET {idf_target}) set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) -include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) +{cmake_args} -{exclude_components_var} +include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) {cpp_standard_options} diff --git a/esphome/build_gen/platformio.py b/esphome/build_gen/platformio.py index b63c4b733d..0a12d344a0 100644 --- a/esphome/build_gen/platformio.py +++ b/esphome/build_gen/platformio.py @@ -63,6 +63,17 @@ def get_ini_content(): # Add extra script for C++ flags CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) + # Add CMake args. A user-supplied value (str or list) is deliberately + # replaced; this option was always overwritten at FINAL priority. + if CORE.cmake_args: + CORE.add_platformio_option( + "board_build.cmake_extra_args", + " ".join( + f"-D{name}={value}" for name, value in sorted(CORE.cmake_args.items()) + ), + replace=True, + ) + content = "[platformio]\n" content += f"description = ESPHome {__version__}\n" diff --git a/esphome/codegen.py b/esphome/codegen.py index 2430f17f3a..2aa6a70abd 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -25,6 +25,7 @@ from esphome.cpp_generator import ( # noqa: F401 add, add_build_flag, add_build_unflag, + add_cmake_arg, add_cxx_build_flag, add_define, add_global, diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f1f039922a..d6ed6d9399 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -760,9 +760,10 @@ def include_builtin_idf_component(name: str) -> None: def get_excluded_builtin_components() -> list[str]: """Return the sorted built-in IDF components excluded from the build. - Single accessor for both build writers: the PlatformIO path passes it as - ``-DEXCLUDE_COMPONENTS`` and the native ESP-IDF path emits it into the - generated CMakeLists. + The set reaches both build writers as the ``EXCLUDE_COMPONENTS`` CMake + arg (registered via ``cg.add_cmake_arg`` at FINAL priority); the native + ESP-IDF writer also reads it directly to filter the built-in component + list. """ return sorted(CORE.data.get(KEY_ESP32, {}).get(KEY_EXCLUDE_COMPONENTS, ())) @@ -2148,14 +2149,16 @@ def _configure_lwip_max_sockets(conf: dict) -> None: add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) +def register_exclude_components_cmake_arg() -> None: + """Register the current exclusion set as the EXCLUDE_COMPONENTS cmake arg.""" + if excluded := get_excluded_builtin_components(): + cg.add_cmake_arg("EXCLUDE_COMPONENTS", ";".join(excluded)) + + @coroutine_with_priority(CoroPriority.FINAL) async def _write_exclude_components() -> None: """Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions.""" - if excluded := get_excluded_builtin_components(): - cg.add_platformio_option( - "board_build.cmake_extra_args", - f"-DEXCLUDE_COMPONENTS={';'.join(excluded)}", - ) + register_exclude_components_cmake_arg() @coroutine_with_priority(CoroPriority.FINAL) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 534b740a5d..0f1ac9213e 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -641,6 +641,8 @@ class EsphomeCore: self.platformio_libraries: dict[str, Library] = {} # A set of build flags to set in the platformio project self.build_flags: set[str] = set() + # A map of CMake args to apply to build systems that use CMake. + self.cmake_args: dict[str, str] = {} # A set of build flags that apply to C++ compiles only (CXXFLAGS / # CXX_COMPILE_OPTIONS), for flags GCC rejects or warns about on C self.cxx_build_flags: set[str] = set() @@ -704,6 +706,7 @@ class EsphomeCore: self.global_statements = [] self.platformio_libraries = {} self.build_flags = set() + self.cmake_args = {} self.cxx_build_flags = set() self.build_unflags = set() self.cpp_standard = None @@ -1062,6 +1065,30 @@ class EsphomeCore: _LOGGER.debug("Adding build flag: %s", build_flag) return build_flag + def add_cmake_arg(self, name: str, value: str) -> None: + """Register a CMake variable for CMake-based toolchains. + + The value must not contain whitespace or quotes (the PlatformIO + backend passes all args to CMake as a single space-joined string + of ``-DNAME=VALUE`` pairs) or ``$`` (expanded by CMake on the + ESP-IDF path but interpolated differently or passed through by + PlatformIO). + """ + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name): + raise ValueError(f"Invalid CMake arg name: {name!r}") + if re.search(r"[\s\"'$]", value): + raise ValueError( + f"CMake arg {name} value {value!r} must not contain " + "whitespace, quotes, or '$'" + ) + old = self.cmake_args.get(name) + if old is not None and old != value: + _LOGGER.warning( + "CMake arg %s already set to %s; overwriting with %s", name, old, value + ) + self.cmake_args[name] = value + _LOGGER.debug("Adding CMake arg: %s=%s", name, value) + def add_cxx_build_flag(self, build_flag: str) -> str: self.cxx_build_flags.add(build_flag) _LOGGER.debug("Adding C++ build flag: %s", build_flag) @@ -1091,10 +1118,14 @@ class EsphomeCore: _LOGGER.debug("Adding define: %s", define) return define - def add_platformio_option(self, key: str, value: str | list[str]) -> None: + def add_platformio_option( + self, key: str, value: str | list[str], *, replace: bool = False + ) -> None: + """Set a platformio.ini option; list values append to an existing list + unless ``replace`` is True, which overwrites any existing value.""" new_val = value old_val = self.platformio_options.get(key) - if isinstance(old_val, list): + if not replace and isinstance(old_val, list): assert isinstance(value, list) new_val = old_val + value self.platformio_options[key] = new_val diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 6bcf4eed77..e6b8c0de42 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -699,6 +699,11 @@ def add_build_flag(build_flag: str): CORE.add_build_flag(build_flag) +def add_cmake_arg(name: str, value: str) -> None: + """Add a CMake arg for CMake-based toolchains; see ``EsphomeCore.add_cmake_arg``.""" + CORE.add_cmake_arg(name, value) + + def add_cxx_build_flag(build_flag: str) -> None: """Add a global build flag that applies to C++ compiles only. diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index ec01000920..29010bcf0e 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -16,6 +16,7 @@ from esphome.components.esp32 import ( KEY_PATH, KEY_REF, KEY_REPO, + register_exclude_components_cmake_arg, ) import esphome.config_validation as cv from esphome.const import KEY_CORE @@ -137,6 +138,27 @@ def test_get_project_cmakelists_full_emits_builtin_components_property( assert "JPEGDEC APPEND" not in content +def test_get_project_cmakelists_emits_cmake_args() -> None: + """Args registered via CORE.add_cmake_arg() are emitted as set() lines, + on minimal writes too.""" + CORE.add_cmake_arg("EXECUTABLE_COMPONENT_NAME", "src") + + content = _render(minimal=True) + + assert 'set(EXECUTABLE_COMPONENT_NAME "src")' in content + + +def test_get_project_cmakelists_escapes_backslashes_in_cmake_args() -> None: + """Backslashes (the only character escaping applies to; the rest are + rejected at registration) are doubled so CMake reads the value back + verbatim.""" + CORE.add_cmake_arg("MY_PATH", r"C:\esp\idf") + + content = _render(minimal=True) + + assert r'set(MY_PATH "C:\\esp\\idf")' in content + + def test_get_project_cmakelists_emits_exclude_components(tmp_path: Path) -> None: """Excluded components are passed to IDF via EXCLUDE_COMPONENTS and are dropped from ESPHOME_PROJECT_BUILTIN_COMPONENTS even when a stale @@ -151,6 +173,7 @@ def test_get_project_cmakelists_emits_exclude_components(tmp_path: Path) -> None }, ) CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity", "esp_lcd"} + register_exclude_components_cmake_arg() content = _render() @@ -169,6 +192,7 @@ def test_get_project_cmakelists_minimal_emits_exclude_components() -> None: """The discovery (minimal) write also excludes components so they never register in project_description.json.""" CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity"} + register_exclude_components_cmake_arg() content = _render(minimal=True) @@ -177,6 +201,8 @@ def test_get_project_cmakelists_minimal_emits_exclude_components() -> None: def test_get_project_cmakelists_no_exclude_components_line_when_empty() -> None: """No EXCLUDE_COMPONENTS line at all when nothing is excluded.""" + register_exclude_components_cmake_arg() + content = _render() assert "EXCLUDE_COMPONENTS" not in content @@ -197,6 +223,7 @@ def test_include_builtin_idf_component_removes_exclusion() -> None: assert get_excluded_builtin_components() == ["unity"] + register_exclude_components_cmake_arg() content = _render() assert 'set(EXCLUDE_COMPONENTS "unity")' in content diff --git a/tests/unit_tests/build_gen/test_platformio.py b/tests/unit_tests/build_gen/test_platformio.py index 3df2fb1036..20acbe302c 100644 --- a/tests/unit_tests/build_gen/test_platformio.py +++ b/tests/unit_tests/build_gen/test_platformio.py @@ -169,6 +169,7 @@ def clean_core(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(CORE, "platformio_libraries", {}) monkeypatch.setattr(CORE, "build_flags", set()) monkeypatch.setattr(CORE, "build_unflags", set()) + monkeypatch.setattr(CORE, "cmake_args", {}) def test_get_ini_content_pins_cpp_standard( @@ -202,6 +203,49 @@ def test_get_ini_content_no_cpp_standard( assert "-std=" not in content +def test_get_ini_content_emits_cmake_args( + clean_core: None, monkeypatch: pytest.MonkeyPatch +) -> None: + """Registered args are space-joined into one option, sorted by name.""" + monkeypatch.setattr( + CORE, + "cmake_args", + {"EXECUTABLE_COMPONENT_NAME": "src", "EXCLUDE_COMPONENTS": "unity"}, + ) + + content = platformio.get_ini_content() + + assert ( + "board_build.cmake_extra_args = " + "-DEXCLUDE_COMPONENTS=unity -DEXECUTABLE_COMPONENT_NAME=src" in content + ) + + +def test_get_ini_content_no_cmake_option_when_no_args(clean_core: None) -> None: + """No board_build.cmake_extra_args line at all when nothing registered + (ESP8266/RP2040/LibreTiny builds must not get a blank option).""" + content = platformio.get_ini_content() + + assert "board_build.cmake_extra_args" not in content + + +def test_get_ini_content_overwrites_list_valued_user_cmake_option( + clean_core: None, monkeypatch: pytest.MonkeyPatch +) -> None: + """A user-supplied board_build.cmake_extra_args may be a list; the + registered args must replace it without tripping add_platformio_option's + list-append assert.""" + monkeypatch.setattr( + CORE, "platformio_options", {"board_build.cmake_extra_args": ["-DFOO=1"]} + ) + monkeypatch.setattr(CORE, "cmake_args", {"EXECUTABLE_COMPONENT_NAME": "src"}) + + content = platformio.get_ini_content() + + assert "board_build.cmake_extra_args = -DEXECUTABLE_COMPONENT_NAME=src" in content + assert "-DFOO=1" not in content + + def test_write_cxx_flags_script_emits_registered_flags( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 7f00d00ef7..c373116106 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -990,3 +990,35 @@ class TestEsphomeCore: ) # The unflag is still recorded either way. assert target.build_unflags == {"-fno-rtti", "-fno-exceptions"} + + def test_add_cmake_arg(self, target) -> None: + target.add_cmake_arg("EXCLUDE_COMPONENTS", "unity;esp_lcd") + assert target.cmake_args == {"EXCLUDE_COMPONENTS": "unity;esp_lcd"} + + @pytest.mark.parametrize("name", ["", "BAD NAME", 'A"B', "A(B)", "1ABC"]) + def test_add_cmake_arg__rejects_invalid_name(self, target, name: str) -> None: + with pytest.raises(ValueError, match="Invalid CMake arg name"): + target.add_cmake_arg(name, "value") + + @pytest.mark.parametrize("value", ["a b", "a\tb", 'a"b', "a'b", "a${FOO}b"]) + def test_add_cmake_arg__rejects_invalid_value(self, target, value: str) -> None: + """Whitespace and quotes are rejected (the PlatformIO backend passes + args as one space-joined string, which would split such a value), and + so is '$' (expanded differently by CMake and PlatformIO).""" + with pytest.raises(ValueError, match="must not contain"): + target.add_cmake_arg("MY_ARG", value) + + def test_add_cmake_arg__warns_on_overwrite( + self, target, caplog: pytest.LogCaptureFixture + ) -> None: + """Re-registering with a different value is last-writer-wins; warn so + the silently dropped value is diagnosable.""" + target.add_cmake_arg("MY_ARG", "one") + target.add_cmake_arg("MY_ARG", "one") + assert "overwriting" not in caplog.text + + target.add_cmake_arg("MY_ARG", "two") + assert ( + "CMake arg MY_ARG already set to one; overwriting with two" in caplog.text + ) + assert target.cmake_args == {"MY_ARG": "two"} From 6343c11873fb62b1ed83b841f4e9c46e5fc73697 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:56:02 +1200 Subject: [PATCH 021/155] [core] Add type annotations to component Python (5/11) (#18342) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/bedjet/__init__.py | 6 ++++-- esphome/components/bedjet/climate/__init__.py | 3 ++- esphome/components/bedjet/fan/__init__.py | 3 ++- esphome/components/bedjet/sensor/__init__.py | 3 ++- esphome/components/bme680_bsec/__init__.py | 3 ++- esphome/components/bme680_bsec/sensor.py | 6 ++++-- esphome/components/bme680_bsec/text_sensor.py | 6 ++++-- esphome/components/cs5460a/sensor.py | 14 +++++++++++--- esphome/components/esp32_touch/__init__.py | 13 ++++++++----- .../components/esp32_touch/binary_sensor.py | 3 ++- esphome/components/esp8266_pwm/output.py | 14 +++++++++++--- esphome/components/factory_reset/__init__.py | 7 ++++--- .../factory_reset/button/__init__.py | 3 ++- .../factory_reset/switch/__init__.py | 3 ++- esphome/components/hbridge/fan/__init__.py | 12 ++++++++++-- esphome/components/hbridge/light/__init__.py | 3 ++- esphome/components/hbridge/switch/__init__.py | 3 ++- esphome/components/hmc5883l/sensor.py | 15 +++++++++++---- esphome/components/mhz19/sensor.py | 19 ++++++++++++++++--- esphome/components/mpr121/__init__.py | 14 +++++++++----- .../mpr121/binary_sensor/__init__.py | 3 ++- esphome/components/pcf85063/time.py | 19 ++++++++++++++++--- esphome/components/pcf8563/time.py | 19 ++++++++++++++++--- esphome/components/pcm5122/audio_dac.py | 12 +++++++----- esphome/components/pcm5122/switch/__init__.py | 3 ++- esphome/components/pmwcs3/sensor.py | 19 ++++++++++++++++--- esphome/components/qmc5883l/sensor.py | 13 +++++++++---- .../components/remote_transmitter/__init__.py | 15 +++++++++++---- esphome/components/rotary_encoder/sensor.py | 14 +++++++++++--- .../components/rp2040_pio_led_strip/light.py | 8 ++++---- esphome/components/rx8130/time.py | 19 ++++++++++++++++--- esphome/components/servo/__init__.py | 19 ++++++++++++++++--- esphome/components/sx1509/__init__.py | 10 ++++++---- .../sx1509/binary_sensor/__init__.py | 3 ++- esphome/components/sx1509/output/__init__.py | 3 ++- 35 files changed, 246 insertions(+), 86 deletions(-) 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/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/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_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/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/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/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/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/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/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/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/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/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 521c3daf87..a97b925e06 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) 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/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/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/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) From c006e9804a2e88d5852ca6761800c01c0cde6fa7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:56:48 +1200 Subject: [PATCH 022/155] [core] Add type annotations to component Python (9/11) (#18346) --- esphome/components/as3935/__init__.py | 4 +++- esphome/components/as3935/binary_sensor.py | 3 ++- esphome/components/as3935/sensor.py | 3 ++- esphome/components/bthome_mithermometer/__init__.py | 8 ++++++-- esphome/components/bthome_mithermometer/sensor.py | 3 ++- esphome/components/color/__init__.py | 11 +++++++---- esphome/components/ds248x/__init__.py | 7 ++++--- esphome/components/ds248x/one_wire.py | 5 +++-- esphome/components/emontx/__init__.py | 10 +++++++--- esphome/components/gdk101/__init__.py | 3 ++- esphome/components/gdk101/binary_sensor.py | 3 ++- esphome/components/gdk101/sensor.py | 3 ++- esphome/components/gdk101/text_sensor.py | 3 ++- esphome/components/hc8/sensor.py | 12 ++++++++++-- esphome/components/lcd_base/__init__.py | 10 +++++++--- esphome/components/libretiny_pwm/output.py | 12 ++++++++++-- esphome/components/lightwaverf/__init__.py | 12 ++++++++++-- esphome/components/max17043/sensor.py | 12 ++++++++++-- esphome/components/nau7802/sensor.py | 12 ++++++++++-- esphome/components/ntc/sensor.py | 12 +++++++----- esphome/components/openthread_info/sensor.py | 5 +++-- esphome/components/openthread_info/text_sensor.py | 5 +++-- esphome/components/pmsx003/sensor.py | 12 ++++++++---- esphome/components/pzemac/sensor.py | 11 +++++++++-- esphome/components/pzemdc/sensor.py | 11 +++++++++-- esphome/components/remote_receiver/__init__.py | 9 ++++++--- esphome/components/remote_receiver/binary_sensor.py | 3 ++- esphome/components/scd30/sensor.py | 12 +++++++++--- esphome/components/senseair/sensor.py | 12 ++++++++++-- esphome/components/sml/__init__.py | 6 ++++-- esphome/components/sml/sensor/__init__.py | 3 ++- esphome/components/sml/text_sensor/__init__.py | 3 ++- esphome/components/sn74hc595/__init__.py | 12 ++++++++---- esphome/components/spa06_base/__init__.py | 12 +++++++----- esphome/components/sy6970/__init__.py | 3 ++- esphome/components/sy6970/binary_sensor/__init__.py | 3 ++- esphome/components/sy6970/sensor/__init__.py | 3 ++- esphome/components/sy6970/text_sensor/__init__.py | 3 ++- esphome/components/tm1638/binary_sensor/__init__.py | 3 ++- esphome/components/tm1638/display.py | 3 ++- esphome/components/tm1638/output/__init__.py | 3 ++- esphome/components/tm1638/switch/__init__.py | 3 ++- esphome/components/uponor_smatrix/__init__.py | 6 ++++-- .../components/uponor_smatrix/climate/__init__.py | 3 ++- .../components/uponor_smatrix/sensor/__init__.py | 3 ++- esphome/components/vl53l0x/sensor.py | 10 +++++++--- esphome/components/weikai/__init__.py | 12 +++++++----- esphome/components/zephyr_ble_server/__init__.py | 13 ++++++++++--- 48 files changed, 238 insertions(+), 97 deletions(-) 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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) From 12da2140cf93273875315823ba041eb4b5841ade Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:57:14 +1200 Subject: [PATCH 023/155] [core] Add type annotations to component Python (11/11) (#18348) --- esphome/components/animation/image.py | 9 ++++++++- esphome/components/apds9960/__init__.py | 3 ++- esphome/components/apds9960/binary_sensor.py | 3 ++- esphome/components/apds9960/sensor.py | 3 ++- esphome/components/emc2101/__init__.py | 3 ++- esphome/components/emc2101/output/__init__.py | 3 ++- esphome/components/emc2101/sensor/__init__.py | 3 ++- esphome/components/graph/__init__.py | 9 ++++++--- esphome/components/pylontech/__init__.py | 3 ++- esphome/components/pylontech/sensor/__init__.py | 3 ++- esphome/components/pylontech/text_sensor/__init__.py | 3 ++- esphome/components/rd03d/__init__.py | 3 ++- esphome/components/rd03d/binary_sensor.py | 3 ++- esphome/components/rd03d/sensor.py | 3 ++- esphome/components/sun_gtil2/__init__.py | 3 ++- esphome/components/sun_gtil2/sensor.py | 3 ++- esphome/components/sun_gtil2/text_sensor.py | 3 ++- esphome/components/teleinfo/__init__.py | 3 ++- esphome/components/teleinfo/sensor/__init__.py | 3 ++- esphome/components/teleinfo/text_sensor/__init__.py | 3 ++- esphome/components/ufm01/__init__.py | 3 ++- esphome/components/ufm01/binary_sensor.py | 3 ++- esphome/components/ufm01/sensor.py | 3 ++- esphome/components/xl9535/__init__.py | 10 ++++++---- 24 files changed, 62 insertions(+), 29 deletions(-) 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/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/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/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/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/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/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/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/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]) From 44dcd82d78bf4da00ac00f1738d9b95e9735df9e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:58:11 +1200 Subject: [PATCH 024/155] [mipi_spi] Toggle D/C only while holding the SPI bus (#18529) --- esphome/components/mipi_spi/mipi_spi.h | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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); From edd4a86d14a0d4ac64b1b6efc2bcb00cee0d5111 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:02:15 -0500 Subject: [PATCH 025/155] Bump prek from 0.4.13 to 0.4.14 (#18563) Signed-off-by: dependabot[bot] --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 4cfa4893ef4bde01ab73da64470f83eeef2e7461 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:24:28 -0500 Subject: [PATCH 026/155] Bump docker/setup-buildx-action from 4.2.0 to 4.3.0 in the docker-actions group (#18564) Signed-off-by: dependabot[bot] --- .github/workflows/ci-docker.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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' From ece90ee97b11ecfeff40a3c2da11cf357cf381f7 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:59:30 -0500 Subject: [PATCH 027/155] Bump bundled esphome-device-builder to 1.12.2 (#18573) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 55aa0ac982..2bbe5331e5 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.2 RUN \ platformio settings set enable_telemetry No \ From 5177972c041d75bf00351c7f6d2cb250253c19e5 Mon Sep 17 00:00:00 2001 From: guillempages Date: Fri, 21 Aug 2026 00:27:44 +0200 Subject: [PATCH 028/155] [runtime_image] keep decoder allocated (#18488) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../components/online_image/online_image.cpp | 2 +- esphome/components/runtime_image/__init__.py | 5 + .../components/runtime_image/bmp_decoder.cpp | 21 +- .../components/runtime_image/bmp_decoder.h | 12 +- .../components/runtime_image/image_decoder.h | 34 +- .../components/runtime_image/image_format.h | 19 + .../components/runtime_image/jpeg_decoder.cpp | 6 - .../components/runtime_image/jpeg_decoder.h | 3 +- .../components/runtime_image/png_decoder.cpp | 7 +- .../components/runtime_image/png_decoder.h | 8 + .../runtime_image/runtime_image.cpp | 53 +-- .../components/runtime_image/runtime_image.h | 34 +- .../sendspin/image/sendspin_image.cpp | 4 +- tests/components/runtime_image/__init__.py | 15 + .../runtime_image/test_decoder_reuse.cpp | 336 ++++++++++++++++++ 15 files changed, 487 insertions(+), 72 deletions(-) create mode 100644 esphome/components/runtime_image/image_format.h create mode 100644 tests/components/runtime_image/__init__.py create mode 100644 tests/components/runtime_image/test_decoder_reuse.cpp 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/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/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/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 From 46f90d0c54af2ce42af6aa55dcdcdbaf2717e5ae Mon Sep 17 00:00:00 2001 From: guillempages Date: Fri, 21 Aug 2026 00:28:17 +0200 Subject: [PATCH 029/155] [core] Add portable strcasestr implementation named str_contains_ignore_case (#18497) Co-authored-by: J. Nick Koston --- esphome/components/audio/audio.cpp | 2 +- esphome/core/helpers.cpp | 13 ++++++++ esphome/core/helpers.h | 19 +++++++++++ tests/components/core/helpers_test.cpp | 46 ++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) 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/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/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 From 52bfc0efb1c574324910c5d0c1de628a4bcc1147 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 18:06:23 -0500 Subject: [PATCH 030/155] [espnow] Fix dump_config crash when enable_on_boot is false (#18572) --- esphome/components/espnow/espnow_component.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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, From 7957808f00eec1eac78e40cd59dac8815ae7c55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Metrich?= <45318189+FredM67@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:57:58 +0200 Subject: [PATCH 031/155] [emontx] Fix sensor state_class defaults not being applied correctly (#17610) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- esphome/components/emontx/sensor/__init__.py | 63 ++++++----- tests/component_tests/emontx/__init__.py | 0 .../emontx/test_sensor_defaults.py | 100 ++++++++++++++++++ tests/components/emontx/test.esp32-idf.yaml | 3 +- tests/components/emontx/test.esp8266-ard.yaml | 3 +- tests/components/emontx/test.rp2040-ard.yaml | 3 +- .../components/emontx/validate.esp32-idf.yaml | 73 +++++++++++++ 7 files changed, 213 insertions(+), 32 deletions(-) create mode 100644 tests/component_tests/emontx/__init__.py create mode 100644 tests/component_tests/emontx/test_sensor_defaults.py create mode 100644 tests/components/emontx/validate.esp32-idf.yaml 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/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/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 From 409d74a48da48ea3152c7d8aedb49f622123782f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:53:44 -0400 Subject: [PATCH 032/155] [esp32_hosted] Fire on_update_available trigger when update is detected (#18591) --- .../esp32_hosted/update/esp32_hosted_update.cpp | 9 +++++++++ .../esp32_hosted/test-embedded.esp32-p4-idf.yaml | 3 +++ .../components/esp32_hosted/test-http.esp32-p4-idf.yaml | 3 +++ 3 files changed, 15 insertions(+) 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/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" From aa944456e0ab4531d7b9184d5d97de166d522913 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:11:52 +1200 Subject: [PATCH 033/155] [core] Add type annotations to component Python (6/11) (#18343) --- esphome/components/ags10/sensor.py | 19 ++++++++++--- esphome/components/at581x/__init__.py | 19 ++++++++++--- esphome/components/at581x/switch/__init__.py | 3 ++- esphome/components/canbus/__init__.py | 20 +++++++++----- esphome/components/daly_bms/__init__.py | 3 ++- esphome/components/daly_bms/binary_sensor.py | 6 +++-- esphome/components/daly_bms/sensor.py | 6 +++-- esphome/components/daly_bms/text_sensor.py | 6 +++-- esphome/components/deep_sleep/__init__.py | 21 +++++++++++---- esphome/components/ds1307/time.py | 19 ++++++++++--- .../components/esp32_ble_tracker/__init__.py | 21 ++++++++++----- esphome/components/ethernet/__init__.py | 27 ++++++++++++------- esphome/components/hdc302x/sensor.py | 23 +++++++++++++--- esphome/components/htu21d/sensor.py | 19 ++++++++++--- esphome/components/ld6002b/__init__.py | 2 +- esphome/components/ld6002b/binary_sensor.py | 3 ++- esphome/components/ld6002b/button/__init__.py | 2 +- esphome/components/ld6002b/number/__init__.py | 2 +- esphome/components/ld6002b/select/__init__.py | 3 ++- esphome/components/ld6002b/sensor.py | 3 ++- esphome/components/ld6002b/switch/__init__.py | 3 ++- esphome/components/ld6002b/text_sensor.py | 3 ++- esphome/components/m5stack_8angle/__init__.py | 3 ++- .../m5stack_8angle/binary_sensor/__init__.py | 3 ++- .../m5stack_8angle/light/__init__.py | 3 ++- .../m5stack_8angle/sensor/__init__.py | 3 ++- esphome/components/modbus/__init__.py | 20 ++++++++------ esphome/components/openthread/__init__.py | 25 +++++++++++------ esphome/components/pulse_counter/sensor.py | 21 ++++++++++----- esphome/components/pulse_meter/sensor.py | 21 ++++++++++----- esphome/components/shelly_dimmer/light.py | 11 ++++---- 31 files changed, 246 insertions(+), 97 deletions(-) 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/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/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/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/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 3b70f947d2..91131a3ed7 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 = { @@ -174,7 +175,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 +346,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 +459,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 +493,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/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/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/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/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/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/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/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/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/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/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]) From 00cffa09a2491be8a39ffd1a62d2c6355bedc61c Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Fri, 21 Aug 2026 14:05:07 -0400 Subject: [PATCH 034/155] [sendspin] Convert tests to package-style includes (#18588) --- tests/components/sendspin/common-action.yaml | 2 +- tests/components/sendspin/common-ethernet.yaml | 5 +++++ tests/components/sendspin/common-hub.yaml | 6 ++++++ tests/components/sendspin/common-media_player.yaml | 3 ++- tests/components/sendspin/common-media_source.yaml | 3 ++- tests/components/sendspin/common-sensor.yaml | 3 ++- tests/components/sendspin/common-text_sensor.yaml | 3 ++- tests/components/sendspin/common.yaml | 10 +++------- tests/components/sendspin/test-action.esp32-idf.yaml | 3 ++- .../components/sendspin/test-ethernet.esp32-idf.yaml | 11 ++--------- .../sendspin/test-media_player.esp32-idf.yaml | 3 ++- .../sendspin/test-media_source.esp32-idf.yaml | 3 ++- tests/components/sendspin/test-sensor.esp32-idf.yaml | 3 ++- .../sendspin/test-text_sensor.esp32-idf.yaml | 3 ++- tests/components/sendspin/test.esp32-idf.yaml | 3 ++- 15 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 tests/components/sendspin/common-ethernet.yaml create mode 100644 tests/components/sendspin/common-hub.yaml 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 From abc9098bd833ca2186b4dc0ec59bf32d049d862d Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:05:00 -0500 Subject: [PATCH 035/155] Bump bundled esphome-device-builder to 1.12.3 (#18601) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2bbe5331e5..4cde6505b3 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.2 +RUN uv pip install --no-cache-dir esphome-device-builder==1.12.3 RUN \ platformio settings set enable_telemetry No \ From 11ea819bc7728d72586f34f381de3c57d1584ff5 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:36:47 -0500 Subject: [PATCH 036/155] Bump aioesphomeapi from 45.12.0 to 45.13.1 (#18600) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 740a8c1a79..3362e43239 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 From 8e9fb0f93c9c8da438dd1f301e8ef593d94ca4c2 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 21 Aug 2026 23:10:47 -0500 Subject: [PATCH 037/155] [remote_transmitter] Fix repeat gap timing on LibreTiny Beken (#18585) Co-authored-by: J. Nick Koston --- .../remote_transmitter/remote_transmitter.cpp | 40 ++++++++++++------- .../remote_transmitter/remote_transmitter.h | 2 +- .../remote_transmitter/test.bk72xx-ard.yaml | 7 ++++ 3 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 tests/components/remote_transmitter/test.bk72xx-ard.yaml diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 49c711330b..31e7464314 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -81,25 +81,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..0aa04682ba 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -72,7 +72,7 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa void space_(uint32_t usec); void await_target_time_(); - uint32_t target_time_; + uint32_t target_time_{0}; #endif #if defined(USE_ESP32) && SOC_RMT_SUPPORTED 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 From 5a300e92f14ef6e2f308dd2394bc5f999fcd5b5f Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:14:56 -0500 Subject: [PATCH 038/155] [wifi] Inline the trivial WiFiScanResult accessors (#18613) --- esphome/components/wifi/wifi_component.cpp | 8 -------- esphome/components/wifi/wifi_component.h | 14 +++++++------- 2 files changed, 7 insertions(+), 15 deletions(-) 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..ff90fbe49b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -319,14 +319,14 @@ 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_; } int8_t get_priority() const { return priority_; } void set_priority(int8_t priority) { priority_ = priority; } From a30e82459f2d7fbb97d2c4861f87b2c784938c9f Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:51:19 -0500 Subject: [PATCH 039/155] [deep_sleep] Reject wakeup_pin_mode at both levels on BK72xx (#18615) --- esphome/components/deep_sleep/__init__.py | 5 +++ .../deep_sleep/test_deep_sleep.py | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 91131a3ed7..dc03708645 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -163,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) 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" + ) From 65704e881f868546390770ec1ca75b63c97739de Mon Sep 17 00:00:00 2001 From: Boris Krivonog Date: Sat, 22 Aug 2026 06:52:18 +0200 Subject: [PATCH 040/155] [mitsubishi_cn105] Add Fahrenheit support (#15488) Co-authored-by: J. Nick Koston --- .../components/mitsubishi_cn105/__init__.py | 3 + .../mitsubishi_cn105/mitsubishi_cn105.h | 1 + .../mitsubishi_cn105_climate.cpp | 20 ++++--- .../mitsubishi_cn105_component.cpp | 7 +++ .../mitsubishi_cn105_component.h | 35 ++++++++++- esphome/components/mqtt/mqtt_climate.cpp | 3 +- .../mitsubishi_cn105_climate_tests.cpp | 60 +++++++++++++++++++ tests/components/mitsubishi_cn105/common.h | 1 + tests/components/mitsubishi_cn105/common.yaml | 1 + 9 files changed, 121 insertions(+), 10 deletions(-) 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/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/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: From dccf55eadc6c41eaadeca24d10d7cd470ccf8bd7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 21 Aug 2026 23:53:16 -0500 Subject: [PATCH 041/155] [remote_transmitter] Use hardware PWM on rtl87xx to fix watchdog crash (#18579) --- .../components/remote_transmitter/__init__.py | 4 +- .../remote_transmitter/remote_transmitter.cpp | 3 +- .../remote_transmitter/remote_transmitter.h | 13 +- .../remote_transmitter_rtl87xx.cpp | 137 ++++++++++++++++++ .../remote_transmitter/test.rtl87xx-ard.yaml | 7 + 5 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 esphome/components/remote_transmitter/remote_transmitter_rtl87xx.cpp create mode 100644 tests/components/remote_transmitter/test.rtl87xx-ard.yaml diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index a97b925e06..9d8761ea90 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -185,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 31e7464314..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 { diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 0aa04682ba..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_{0}; +#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/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 From ef1d77885dd5a7f1beef4e3d34e22e26d5661fa1 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:04:08 -0500 Subject: [PATCH 042/155] [captive_portal] Show each network once in the scan list (#17847) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: Bluetooth Devices Bot --- .../captive_portal/captive_portal.cpp | 11 +- esphome/components/captive_portal/scan_list.h | 28 ++++ esphome/components/wifi/wifi_component.h | 1 + tests/components/captive_portal/__init__.py | 10 ++ .../captive_portal/scan_list_test.cpp | 130 ++++++++++++++++++ 5 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 esphome/components/captive_portal/scan_list.h create mode 100644 tests/components/captive_portal/__init__.py create mode 100644 tests/components/captive_portal/scan_list_test.cpp 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/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ff90fbe49b..c54fbc004b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -327,6 +327,7 @@ class WiFiScanResult { 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/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 From ea10f94376d967f2099701abd12902efdc9e7cf8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 23 Aug 2026 03:31:47 +1200 Subject: [PATCH 043/155] [core] Add type annotations to component Python (10/11) (#18347) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/adc/__init__.py | 5 ++- esphome/components/adc/sensor.py | 6 +-- esphome/components/api/__init__.py | 36 ++++++++++++----- esphome/components/button/__init__.py | 20 ++++++---- esphome/components/climate/__init__.py | 29 ++++++++++---- esphome/components/cover/__init__.py | 40 ++++++++++++++----- .../components/dashboard_import/__init__.py | 10 +++-- esphome/components/debug/__init__.py | 3 +- esphome/components/debug/sensor.py | 3 +- esphome/components/debug/text_sensor.py | 3 +- esphome/components/esp8266/__init__.py | 18 +++++---- esphome/components/esp8266/gpio.py | 15 ++++--- esphome/components/file/image.py | 17 ++++---- esphome/components/globals/__init__.py | 12 ++++-- .../components/gpio/binary_sensor/__init__.py | 5 ++- esphome/components/gpio/one_wire/__init__.py | 3 +- esphome/components/gpio/output/__init__.py | 3 +- esphome/components/gpio/switch/__init__.py | 3 +- esphome/components/homeassistant/__init__.py | 12 ++++-- .../homeassistant/binary_sensor/__init__.py | 3 +- .../homeassistant/number/__init__.py | 3 +- .../homeassistant/sensor/__init__.py | 3 +- .../homeassistant/switch/__init__.py | 3 +- .../homeassistant/text_sensor/__init__.py | 3 +- .../components/homeassistant/time/__init__.py | 3 +- esphome/components/host/__init__.py | 5 ++- esphome/components/host/gpio.py | 9 +++-- esphome/components/host/time/__init__.py | 3 +- esphome/components/i2c/__init__.py | 32 ++++++++------- esphome/components/lock/__init__.py | 34 +++++++++++----- 30 files changed, 230 insertions(+), 114 deletions(-) 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/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/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/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/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/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/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 2161a902cb..3dd9750c6f 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -3,6 +3,7 @@ from pathlib import Path import platform import re import subprocess +from typing import Any import esphome.codegen as cg import esphome.config_validation as cv @@ -31,6 +32,7 @@ from esphome.core import ( from esphome.core.config import BOARD_MAX_LENGTH from esphome.helpers import IS_MACOS, copy_file_if_changed from esphome.platformio.toolchain import copy_ccache_script +from esphome.storage_json import StorageJSON from esphome.types import ConfigType from .boards import BOARDS, ESP8266_LD_SCRIPTS @@ -88,7 +90,7 @@ def lambdas_use_scanf_float(config: ConfigType) -> bool: return False -def set_core_data(config): +def set_core_data(config: ConfigType) -> ConfigType: CORE.data[KEY_ESP8266] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266 CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" @@ -102,7 +104,7 @@ def set_core_data(config): return config -def get_download_types(storage_json): +def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: """Binary-download entries for a built ESP8266 firmware. Used by device-builder (esphome/device-builder), via @@ -157,7 +159,7 @@ ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1) -def _arduino_check_versions(value): +def _arduino_check_versions(value: ConfigType) -> ConfigType: value = value.copy() lookups = { "dev": (cv.Version(3, 1, 2), "https://github.com/esp8266/Arduino.git"), @@ -200,7 +202,7 @@ def _arduino_check_versions(value): return value -def _parse_platform_version(value): +def _parse_platform_version(value: Any) -> str: try: # if platform version is a valid version constraint, prefix the default package cv.platformio_version_constraint(value) @@ -275,7 +277,7 @@ def check_rosetta() -> None: @coroutine_with_priority(CoroPriority.PLATFORM) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add(esp8266_ns.setup_preferences()) cg.add_platformio_option("lib_ldf_mode", "off") @@ -504,7 +506,7 @@ ESP8266_EXCEPTION_CODES = { } -def _decode_pc(config, addr): +def _decode_pc(config: ConfigType, addr: str) -> None: from esphome.platformio import toolchain idedata = toolchain.get_idedata(config) @@ -525,7 +527,7 @@ def _decode_pc(config, addr): _LOGGER.warning("Decoded %s", translation) -def _parse_register(config, regex, line): +def _parse_register(config: ConfigType, regex: re.Pattern[str], line: str) -> None: match = regex.match(line) if match is not None: _decode_pc(config, match.group(1)) @@ -549,7 +551,7 @@ STACKTRACE_BAD_ALLOC_RE = re.compile( STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") -def process_stacktrace(config, line, backtrace_state): +def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool: line = line.strip() # ESP8266 Exception type match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line) diff --git a/esphome/components/esp8266/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/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/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/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 7b23775b47..1b66842f1e 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -1,13 +1,19 @@ +from collections.abc import Callable, Iterable + import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter", "@esphome/core"] homeassistant_ns = cg.esphome_ns.namespace("homeassistant") -def validate_entity_domain(platform, supported_domains): - def validator(config): +def validate_entity_domain( + platform: str, supported_domains: Iterable[str] +) -> Callable[[ConfigType], ConfigType]: + def validator(config: ConfigType) -> ConfigType: domain = config[CONF_ENTITY_ID].split(".", 1)[0] if domain not in supported_domains: raise cv.Invalid( @@ -34,7 +40,7 @@ HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema( ) -def setup_home_assistant_entity(var, config): +def setup_home_assistant_entity(var: MockObj, config: ConfigType) -> None: cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) if CONF_ATTRIBUTE in config: cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) diff --git a/esphome/components/homeassistant/binary_sensor/__init__.py b/esphome/components/homeassistant/binary_sensor/__init__.py index a943368dd7..6ea17b6831 100644 --- a/esphome/components/homeassistant/binary_sensor/__init__.py +++ b/esphome/components/homeassistant/binary_sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import binary_sensor +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_SCHEMA, @@ -18,7 +19,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).ex ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/number/__init__.py b/esphome/components/homeassistant/number/__init__.py index 8f760772c3..ab1389e13a 100644 --- a/esphome/components/homeassistant/number/__init__.py +++ b/esphome/components/homeassistant/number/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, @@ -22,7 +23,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") var = await number.new_number( config, diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index 6437476827..abee957fda 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_SCHEMA, @@ -18,7 +19,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).e ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/switch/__init__.py b/esphome/components/homeassistant/switch/__init__.py index c299a731f2..55854cd659 100644 --- a/esphome/components/homeassistant/switch/__init__.py +++ b/esphome/components/homeassistant/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, @@ -36,7 +37,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/homeassistant/text_sensor/__init__.py b/esphome/components/homeassistant/text_sensor/__init__.py index b59f9d23df..265250c695 100644 --- a/esphome/components/homeassistant/text_sensor/__init__.py +++ b/esphome/components/homeassistant/text_sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import text_sensor +from esphome.types import ConfigType from .. import ( HOME_ASSISTANT_IMPORT_SCHEMA, @@ -18,7 +19,7 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/time/__init__.py b/esphome/components/homeassistant/time/__init__.py index 05ca86a26e..146b8278ea 100644 --- a/esphome/components/homeassistant/time/__init__.py +++ b/esphome/components/homeassistant/time/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import time as time_ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TIMEZONE +from esphome.types import ConfigType from .. import homeassistant_ns @@ -16,7 +17,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await time_.register_time(var, config) await cg.register_component(var, config) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index b6a3b8b615..c5846f5406 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.platformio.toolchain import copy_ccache_script +from esphome.types import ConfigType from .const import KEY_HOST @@ -22,7 +23,7 @@ AUTO_LOAD = ["network", "preferences"] IS_TARGET_PLATFORM = True -def set_core_data(config): +def set_core_data(config: ConfigType) -> ConfigType: CORE.data[KEY_HOST] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_HOST CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host" @@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_NATIVE_64BIT_TIME") # The prefs file finds stored preferences by key, so key migration is possible diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index fcfb0b6c54..e39d35d077 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -1,4 +1,5 @@ import logging +from typing import Any from esphome import pins import esphome.codegen as cg @@ -14,6 +15,8 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, ) +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType from .const import host_ns @@ -22,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin) -def _translate_pin(value): +def _translate_pin(value: Any) -> int | str: if isinstance(value, dict) or value is None: raise cv.Invalid( "This variable only supports pin numbers, not full pin schemas " @@ -41,7 +44,7 @@ def _translate_pin(value): return value -def validate_gpio_pin(value): +def validate_gpio_pin(value: Any) -> int | str: return _translate_pin(value) @@ -53,7 +56,7 @@ HOST_PIN_SCHEMA = pins.gpio_base_schema( @pins.PIN_SCHEMA_REGISTRY.register("host", HOST_PIN_SCHEMA) -async def host_pin_to_code(config): +async def host_pin_to_code(config: ConfigType) -> MockObj: var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) diff --git a/esphome/components/host/time/__init__.py b/esphome/components/host/time/__init__.py index d9a2f1207c..6eb0cf954d 100644 --- a/esphome/components/host/time/__init__.py +++ b/esphome/components/host/time/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import time as time_ import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] @@ -14,7 +15,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await time_.register_time(var, config) diff --git a/esphome/components/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/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) From 74fc2e367abb874d74c436d9961d7ff3d9981a11 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:42:53 +0000 Subject: [PATCH 044/155] Bump bundled esphome-device-builder to 1.12.4 (#18651) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 4cde6505b3..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.3 +RUN uv pip install --no-cache-dir esphome-device-builder==1.12.4 RUN \ platformio settings set enable_telemetry No \ From d119ad6c6078fd8fa3d2191c4d5e2b91fbc7a38e Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke Date: Sat, 22 Aug 2026 17:47:46 +0200 Subject: [PATCH 045/155] [usb_uart] Extract non-final USBUartChannelBase from USBUartChannel (#17472) Co-authored-by: p1ngb4ck --- esphome/components/usb_uart/ch34x.cpp | 2 +- esphome/components/usb_uart/cp210x.cpp | 2 +- esphome/components/usb_uart/ft23xx.cpp | 6 +-- esphome/components/usb_uart/pl2303.cpp | 2 +- esphome/components/usb_uart/usb_uart.cpp | 20 ++++---- esphome/components/usb_uart/usb_uart.h | 65 ++++++++++++++---------- 6 files changed, 55 insertions(+), 42 deletions(-) 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}; }; From dcabaedff1b5adfb7d2070b7d1557e67d0eca8c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 18:59:24 -0500 Subject: [PATCH 046/155] [bk72xx_ble] Block BK7238 until the LibreTiny bonding partition fix lands (#18649) --- esphome/components/bk72xx_ble/__init__.py | 30 +++++++++---------- .../bk72xx_ble/config/test_bk7238.yaml | 7 +++++ .../bk72xx_ble/test_family_gate.py | 1 + 3 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 tests/component_tests/bk72xx_ble/config/test_bk7238.yaml diff --git a/esphome/components/bk72xx_ble/__init__.py b/esphome/components/bk72xx_ble/__init__.py index 81073c9b02..74b9cb5954 100644 --- a/esphome/components/bk72xx_ble/__init__.py +++ b/esphome/components/bk72xx_ble/__init__.py @@ -4,9 +4,12 @@ The platform analog of esp32_ble / rp2040_ble: owns the Beken BDK BLE stack bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build on this component and contain no SDK calls of their own. -Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253 -(BLE 5.2), and any future BLE-5.x SoC. Known non-5.x families are rejected in -to_code; unknown families are capability-checked at compile time via +Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7252N/BK7253 (BLE 5.2), +and any future BLE-5.x SoC. BK7238 (BLE 5.2) is blocked for now: with BLE +compiled in, the Beken SDK erases the bootloader flash sector at boot because +LibreTiny's partition table has no BLE bonding entry (esphome#18646, +libretiny-eu/libretiny#408). Known non-5.x families and BK7238 are rejected in +to_code. Unknown families are capability-checked at compile time via `__has_include("app_ble.h")`, a header only on the BLE 5.x include path (ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build fails with a clear #error. @@ -65,6 +68,14 @@ def _unsupported_family_message(family: str) -> str | None: ) if family == FAMILY_BK7231Q: return "bk72xx_ble does not support BK7231Q: this SoC has no BLE" + if family == FAMILY_BK7238: + return ( + "bk72xx_ble is disabled on BK7238: with BLE compiled in, the Beken SDK " + "erases the bootloader flash sector at boot and the device can no longer " + "start (see https://github.com/esphome/esphome/issues/18646); support " + "returns once the LibreTiny partition table fix " + "(libretiny-eu/libretiny#408) is released" + ) return None @@ -113,18 +124,7 @@ async def to_code(config: ConfigType) -> None: # BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is # derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++ # which path is available so it doesn't reference a missing symbol. - family = libretiny.get_libretiny_family() - if family == FAMILY_BK7231N: + if libretiny.get_libretiny_family() == FAMILY_BK7231N: cg.add_define("BK72XX_BLE_HAS_COMMON_BDADDR") - elif family == FAMILY_BK7238: - # ESPHome's LibreTiny disables BLE on BK7238 because the SDK can hang at - # WiFi STA startup when BLE init runs. This component re-enables BLE, so - # warn loudly: BK7238 is accepted but not hardware-verified and may be - # WiFi-unstable with BLE on. - _LOGGER.warning( - "bk72xx_ble on BK7238: enabling BLE is known to risk a WiFi STA startup " - "hang on this family and is not yet hardware-verified. Expect possible " - "instability." - ) cg.add_define("USE_BK72XX_BLE") diff --git a/tests/component_tests/bk72xx_ble/config/test_bk7238.yaml b/tests/component_tests/bk72xx_ble/config/test_bk7238.yaml new file mode 100644 index 0000000000..0880cf69f5 --- /dev/null +++ b/tests/component_tests/bk72xx_ble/config/test_bk7238.yaml @@ -0,0 +1,7 @@ +esphome: + name: bk-family-gate-7238 + +bk72xx: + board: generic-bk7238 + +bk72xx_ble: diff --git a/tests/component_tests/bk72xx_ble/test_family_gate.py b/tests/component_tests/bk72xx_ble/test_family_gate.py index da67749bb3..86f3ef0039 100644 --- a/tests/component_tests/bk72xx_ble/test_family_gate.py +++ b/tests/component_tests/bk72xx_ble/test_family_gate.py @@ -16,6 +16,7 @@ from esphome.core import EsphomeError ("test_bk7231t.yaml", "BK7231T.*BLE 4.2"), ("test_bk7252.yaml", "BK7251.*BLE 4.2"), ("test_bk7231q.yaml", "BK7231Q.*no BLE"), + ("test_bk7238.yaml", "BK7238.*bootloader"), ], ) def test_unsupported_family_rejected( From c062d0c7171a1e576fdb0bd8864e374be51a707c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 20:57:13 -0500 Subject: [PATCH 047/155] [ota] Log prepare, upload, and total OTA timing in espota2 (#18582) --- esphome/espota2.py | 18 ++++++++++++++++++ tests/unit_tests/test_espota2.py | 28 ++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/esphome/espota2.py b/esphome/espota2.py index 61e897f601..ca833f1816 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -460,8 +460,14 @@ def perform_ota( (upload_size >> 8) & 0xFF, (upload_size >> 0) & 0xFF, ] + # The device erases flash between receiving the size and acking the + # prepare, so this window shows the erase cost (near zero when the + # device erases lazily during the upload) + prepare_start = time.perf_counter() send_check(sock, upload_size_encoded, "binary size") receive_exactly(sock, 1, "update prepare result", RESPONSE_UPDATE_PREPARE_OK) + prepare_duration = time.perf_counter() - prepare_start + _LOGGER.info("Preparing for upload took %.2f seconds", prepare_duration) upload_md5 = hashlib.md5(upload_contents).hexdigest() _LOGGER.debug("MD5 of upload is %s", upload_md5) @@ -528,11 +534,23 @@ def perform_ota( # reboots on its own; the exact commit point is not observable from # here, so treat everything past the data phase as non-retryable. A # re-upload could flash a device that already updated successfully. + commit_start = time.perf_counter() try: receive_exactly(sock, 1, "update receive result", RESPONSE_RECEIVE_OK) receive_exactly(sock, 1, "update end result", RESPONSE_UPDATE_END_OK) except OTANetworkError as err: raise _committed_error(err) from err + commit_duration = time.perf_counter() - commit_start + + # Sum of the named windows so the breakdown is self consistent; connect, + # handshake, auth, and the one MD5 round trip are not included + _LOGGER.info( + "Update took %.2f seconds (prepare %.2f, upload %.2f, commit %.2f)", + prepare_duration + duration + commit_duration, + prepare_duration, + duration, + commit_duration, + ) try: send_check(sock, RESPONSE_OK, "end acknowledgement") diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index db4a4b1117..e0e9185e1c 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -6,6 +6,8 @@ from collections.abc import Generator import gzip import hashlib import io +import itertools +import logging from pathlib import Path import socket import struct @@ -53,8 +55,9 @@ def mock_sleep() -> Generator[Mock]: @pytest.fixture def mock_time(mock_sleep: Mock) -> Generator[None]: """Mock time-related functions for consistent testing.""" - # Provide enough values for multiple calls (tests may call perform_ota multiple times) - with patch("time.perf_counter", side_effect=[0, 1, 0, 1, 0, 1]): + # Monotonically increasing, never exhausted regardless of how many timing + # windows perform_ota measures or how many times a test calls it + with patch("time.perf_counter", side_effect=itertools.count()): yield @@ -372,7 +375,9 @@ def test_perform_ota_successful_md5_auth( @pytest.mark.usefixtures("mock_time") -def test_perform_ota_no_auth(mock_socket: Mock, mock_file: io.BytesIO) -> None: +def test_perform_ota_no_auth( + mock_socket: Mock, mock_file: io.BytesIO, caplog: pytest.LogCaptureFixture +) -> None: """Test OTA without authentication.""" recv_responses = [ bytes([espota2.RESPONSE_OK]), # First byte of version response @@ -387,7 +392,14 @@ def test_perform_ota_no_auth(mock_socket: Mock, mock_file: io.BytesIO) -> None: mock_socket.recv.side_effect = recv_responses - espota2.perform_ota(mock_socket, None, mock_file, "test.bin") + # Distinct window lengths pin each duration to its label; exactly the 6 + # expected perf_counter calls, so an unaccounted timing window raises + timings = [0.0, 2.0, 10.0, 15.0, 20.0, 27.0] + with ( + patch("time.perf_counter", side_effect=timings), + caplog.at_level(logging.INFO), + ): + espota2.perform_ota(mock_socket, None, mock_file, "test.bin") # Should not send any auth-related data auth_calls = [ @@ -397,6 +409,14 @@ def test_perform_ota_no_auth(mock_socket: Mock, mock_file: io.BytesIO) -> None: ] assert len(auth_calls) == 0 + # The timing summary is the observable output of the upload; exact strings + # pin each duration to its label + assert "Preparing for upload took 2.00 seconds" in caplog.text + assert ( + "Update took 14.00 seconds (prepare 2.00, upload 5.00, commit 7.00)" + in caplog.text + ) + @pytest.mark.usefixtures("mock_time") def test_perform_ota_with_compression(mock_socket: Mock) -> None: From 1f31e51446af7bf7d6a34c8dfc40861a9a7ce783 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 20:57:37 -0500 Subject: [PATCH 048/155] [esphome] Inline the trivial OTA port accessors (#18625) --- esphome/components/esphome/ota/ota_esphome.cpp | 2 -- esphome/components/esphome/ota/ota_esphome.h | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index cab725f704..9cbb25b373 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -588,8 +588,6 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { } float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } -uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } -void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 0053ca6969..979e3f2d7d 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -39,14 +39,14 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { #endif // USE_OTA_PASSWORD /// Manually set the port OTA should listen on - void set_port(uint16_t port); + void set_port(uint16_t port) { this->port_ = port; } void setup() override; void dump_config() override; float get_setup_priority() const override; void loop() override; - uint16_t get_port() const; + uint16_t get_port() const { return this->port_; } protected: void handle_handshake_(); From 6aab523dd9e6c716d1f2f246598bbc786954ef0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 20:57:58 -0500 Subject: [PATCH 049/155] [esp32_ble] Log connection parameter update results (#18607) --- esphome/components/esp32_ble/ble.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index e2d79173ff..6e6fb0e30d 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -643,8 +643,28 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa App.wake_loop_threadsafe(); return; + // Log the result of connection parameter updates: a peer can reject or + // never answer an update, and without this the link silently stays on the + // old parameters (visible only as unexplained supervision timeouts). + case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: { + if (param->update_conn_params.status != ESP_BT_STATUS_SUCCESS) { + char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(param->update_conn_params.bda, mac_s); + ESP_LOGW(TAG, "[%s] Conn param update failed, status=%d", mac_s, param->update_conn_params.status); + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + else { + char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(param->update_conn_params.bda, mac_s); + ESP_LOGV(TAG, "[%s] Conn params updated: interval=%u (x1.25ms) latency=%u timeout=%u (x10ms)", mac_s, + param->update_conn_params.conn_int, param->update_conn_params.latency, + param->update_conn_params.timeout); + } +#endif + return; + } + // Ignore these GAP events as they are not relevant for our use case - case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm From 14499223fd1e1560faf034747f39bd2b2c28f8a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 20:58:13 -0500 Subject: [PATCH 050/155] [esp8266] Don't report stale crash state after hardware WDT resets (#18597) --- esphome/components/esp8266/crash_handler.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 91b0cf9082..dc79043f21 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -118,8 +118,6 @@ static const LogString *get_exception_cause(uint32_t cause) { } static const LogString *get_reset_reason(uint32_t reason) { - if (reason == REASON_WDT_RST) - return LOG_STR("Hardware WDT"); if (reason == REASON_EXCEPTION_RST) return LOG_STR("Exception"); if (reason == REASON_SOFT_WDT_RST) @@ -162,13 +160,20 @@ void crash_handler_log() { if (!is_crash_reason(resetInfo.reason)) return; + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + if (resetInfo.reason == REASON_WDT_RST) { + // A hardware WDT reset happens entirely in hardware: the postmortem hook + // never runs, so rst_info epc1/exccause and the RTC backtrace are + // leftovers from an earlier crash. Don't misattribute them (#18596). + ESP_LOGE(TAG, " Reason: Hardware WDT (no crash state is recorded for hardware WDT resets)"); + return; + } + // Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost). // Both resetInfo and RTC data survive until the next reset, so this can be // called multiple times (logger init + API subscribe) with the same result. uint32_t backtrace[MAX_BACKTRACE]; uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE); - - ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); // GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific // ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match // the Arduino core's postmortem handler behavior. From 74bdf275d20ab138cd9950e4ee281a11c21b39cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 20:58:28 -0500 Subject: [PATCH 051/155] [core] Dump the main.cpp config comment with sorted keys (#18653) --- esphome/__main__.py | 6 ++++-- tests/unit_tests/test_main.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c1e05d2ea7..769b66ecc8 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -762,9 +762,11 @@ def _wrap_to_code(name, comp, yaml_util): async def wrapped(conf): cg.add(cg.LineComment(f"{name}:")) if comp.config_schema is not None: - conf_str = yaml_util.dump(conf) + # sort_keys: voluptuous fills defaults in set order, so an + # unsorted dump would churn main.cpp and relink every run + conf_str = yaml_util.dump(conf, sort_keys=True) conf_str = conf_str.replace("//", "") - # remove tailing \ to avoid multi-line comment warning + # remove trailing \ to avoid multi-line comment warning conf_str = conf_str.replace("\\\n", "\n") cg.add(cg.LineComment(indent(conf_str))) await coro(conf) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index a40341e194..1cb710ca58 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -11,6 +11,7 @@ from pathlib import Path import re import sys import time +from types import SimpleNamespace from typing import Any, Self from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -18,7 +19,7 @@ import pytest from pytest import CaptureFixture from zeroconf import ServiceStateChange -from esphome import __main__ as main +from esphome import __main__ as main, yaml_util from esphome.__main__ import ( Purpose, _get_configured_xtal_freq, @@ -29,6 +30,7 @@ from esphome.__main__ import ( _unresolved_default_error, _validate_bootloader_binary, _validate_partition_table_binary, + _wrap_to_code, check_permissions, choose_upload_log_host, command_analyze_memory, @@ -116,6 +118,7 @@ from esphome.espota2 import ( OTA_TYPE_UPDATE_PARTITION_TABLE, ) from esphome.platformio import toolchain +from esphome.types import ConfigType from esphome.util import BootselResult, FlashImage from esphome.zeroconf import _await_discovery, discover_mdns_devices @@ -7130,3 +7133,28 @@ def test_warn_source_tree_mismatch_falls_back_when_stat_fails( # Same tree, so the path comparison still finds them equal and stays silent assert not caplog.text + + +@pytest.mark.asyncio +async def test_wrap_to_code_comment_is_insertion_order_independent() -> None: + """The config comment dumps with sorted keys: voluptuous fills schema + defaults in set-iteration order, so an unsorted dump would churn + main.cpp and relink the firmware on every run.""" + comments: list[str] = [] + + async def to_code(conf: ConfigType) -> None: + """Accept any config; only the wrapper's comment output matters.""" + + comp = SimpleNamespace(to_code=to_code, config_schema=object()) + wrapped = _wrap_to_code("demo", comp, yaml_util) + with patch("esphome.codegen.add", side_effect=lambda st: comments.append(str(st))): + # Nested on purpose: the real churn lives in nested action configs, + # so sorting must apply at every mapping level + await wrapped({"beta": 1, "alpha": {"z": 1, "a": 2}}) + first = "\n".join(comments) + comments.clear() + await wrapped({"alpha": {"a": 2, "z": 1}, "beta": 1}) + second = "\n".join(comments) + assert first == second + assert second.index("alpha") < second.index("beta") + assert second.index("a: 2") < second.index("z: 1") From 259e7182a350e16b1f70fe88a5bb0dff7fbfc546 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 20:59:55 -0500 Subject: [PATCH 052/155] [esp32] Exclude esp_gdbstub from the build by default (#18604) --- esphome/components/esp32/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d6ed6d9399..501c2e525f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -233,6 +233,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component "esp_eth", # Ethernet driver - only needed by ethernet component + "esp_gdbstub", # GDB stub panic handler - unused by ESPHome; bt pulls it back "esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality "esp_http_client", # HTTP client - only needed by http_request component "esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation From a282cb095ec2dc4bb08e4109714b4d71768c4629 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:00:39 -0500 Subject: [PATCH 053/155] [ethernet] Inline the trivial EthernetComponent setters (#18618) --- .../ethernet/ethernet_component.cpp | 8 ---- .../components/ethernet/ethernet_component.h | 48 +++++++++---------- .../ethernet/ethernet_component_esp32.cpp | 20 +------- .../ethernet/ethernet_component_rp2.cpp | 7 --- 4 files changed, 25 insertions(+), 58 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 42cb0b3cfc..14a4fd660b 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -10,14 +10,6 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non- EthernetComponent::EthernetComponent() { global_eth_component = this; } -float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } - -void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } - -#ifdef USE_ETHERNET_MANUAL_IP -void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } -#endif - #ifdef USE_ETHERNET_IP_STATE_LISTENERS void EthernetComponent::notify_ip_state_listeners_() { auto ips = this->get_ip_addresses(); diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 646e0af8e6..2da070b5e0 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -125,7 +125,7 @@ class EthernetComponent final : public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::ETHERNET; } void on_powerdown() override { powerdown(); } bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } @@ -146,9 +146,9 @@ class EthernetComponent final : public Component { esp_netif_t *get_esp_netif() { return this->eth_netif_; } #endif - void set_type(EthernetType type); + void set_type(EthernetType type) { this->type_ = type; } #ifdef USE_ETHERNET_MANUAL_IP - void set_manual_ip(const ManualIP &manual_ip); + void set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } #endif void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } @@ -171,35 +171,35 @@ class EthernetComponent final : public Component { esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; } #ifdef USE_ETHERNET_SPI - void set_clk_pin(uint8_t clk_pin); - void set_miso_pin(uint8_t miso_pin); - void set_mosi_pin(uint8_t mosi_pin); - void set_cs_pin(uint8_t cs_pin); - void set_interrupt_pin(uint8_t interrupt_pin); - void set_reset_pin(uint8_t reset_pin); - void set_clock_speed(int clock_speed); - void set_interface(spi_host_device_t interface); + void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } + void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } + void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } + void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } + void set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } + void set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } + void set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } + void set_interface(spi_host_device_t interface) { this->interface_ = interface; } #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT - void set_polling_interval(uint32_t polling_interval); + void set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } #endif #else - void set_phy_addr(uint8_t phy_addr); - void set_power_pin(int power_pin); - void set_mdc_pin(uint8_t mdc_pin); - void set_mdio_pin(uint8_t mdio_pin); - void set_clk_pin(uint8_t clk_pin); - void set_clk_mode(emac_rmii_clock_mode_t clk_mode); + void set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } + void set_power_pin(int power_pin) { this->power_pin_ = power_pin; } + void set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } + void set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } + void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } + void set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } void add_phy_register(PHYRegister register_value); #endif // USE_ETHERNET_SPI #endif // USE_ESP32 #ifdef USE_RP2 - void set_clk_pin(uint8_t clk_pin); - void set_miso_pin(uint8_t miso_pin); - void set_mosi_pin(uint8_t mosi_pin); - void set_cs_pin(uint8_t cs_pin); - void set_interrupt_pin(int8_t interrupt_pin); - void set_reset_pin(int8_t reset_pin); + void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } + void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } + void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } + void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } + void set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } + void set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; } #endif // USE_RP2 #ifdef USE_ETHERNET_IP_STATE_LISTENERS diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index 0220d6a19b..4af2d5f93c 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -908,25 +908,7 @@ void EthernetComponent::dump_connect_params_() { #endif /* USE_NETWORK_IPV6 */ } -#ifdef USE_ETHERNET_SPI -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } -void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } -void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } -void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } -void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } -void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } -void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; } -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT -void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } -#endif -#else -void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } -void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } -void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } -void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } +#ifndef USE_ETHERNET_SPI void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif diff --git a/esphome/components/ethernet/ethernet_component_rp2.cpp b/esphome/components/ethernet/ethernet_component_rp2.cpp index 119e447689..7f4db4fab7 100644 --- a/esphome/components/ethernet/ethernet_component_rp2.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2.cpp @@ -355,13 +355,6 @@ void EthernetComponent::dump_connect_params_() { this->get_eth_mac_address_pretty_into_buffer(mac_buf)); } -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } -void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } -void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } -void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } -void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; } - void EthernetComponent::enable() { // RP2040 uses arduino-pico's LwipIntfDev which manages link state internally; // there is no clean enable/disable hook today. The YAML option is accepted on From 0dc69aab1e3f6927cd6ee33804c69e2404a0728b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:00:49 -0500 Subject: [PATCH 054/155] [logger] Inline the trivial Logger accessors (#18619) --- esphome/components/logger/logger.cpp | 7 ------- esphome/components/logger/logger.h | 6 +++--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 6527b6aa8c..bfc005070e 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -201,17 +201,10 @@ void Logger::process_messages_() { #endif // USE_ESPHOME_TASK_LOG_BUFFER } -void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } #endif -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) -UARTSelection Logger::get_uart() const { return this->uart_; } -#endif - -float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } - // Log level strings - packed into flash on ESP8266, indexed by log level (0-7) PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 69d8e6d32a..9c26814f7e 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -148,7 +148,7 @@ class Logger final : public Component { void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. - void set_baud_rate(uint32_t baud_rate); + void set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } uint32_t get_baud_rate() const { return baud_rate_; } #if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *get_hw_serial() const { return hw_serial_; } @@ -163,7 +163,7 @@ class Logger final : public Component { #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. - UARTSelection get_uart() const; + UARTSelection get_uart() const { return this->uart_; } #endif /// Set the default log level for this logger. @@ -197,7 +197,7 @@ class Logger final : public Component { void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); } #endif - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::BUS + 500.0f; } void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH From 4db16660242dc4db15bef9fd3ab2bc3b96ce8ed7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:00:59 -0500 Subject: [PATCH 055/155] [light] Inline the trivial LightState accessors (#18620) --- esphome/components/light/esp_range_view.cpp | 2 -- esphome/components/light/esp_range_view.h | 3 +++ esphome/components/light/light_state.cpp | 18 ------------- esphome/components/light/light_state.h | 28 ++++++++++++--------- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/esphome/components/light/esp_range_view.cpp b/esphome/components/light/esp_range_view.cpp index 58d552031a..5d372983d9 100644 --- a/esphome/components/light/esp_range_view.cpp +++ b/esphome/components/light/esp_range_view.cpp @@ -13,8 +13,6 @@ ESPColorView ESPRangeView::operator[](int32_t index) const { index = interpret_index(index, this->size()) + this->begin_; return (*this->parent_)[index]; } -ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; } -ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; } void ESPRangeView::set(const Color &color) { for (int32_t i = this->begin_; i < this->end_; i++) { diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h index f5e4ebb83f..ec129bdf70 100644 --- a/esphome/components/light/esp_range_view.h +++ b/esphome/components/light/esp_range_view.h @@ -75,4 +75,7 @@ class ESPRangeIterator { int32_t i_; }; +inline ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; } +inline ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; } + } // namespace esphome::light diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 9d0181a05c..82c00e2382 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -157,8 +157,6 @@ void LightState::loop() { } } -float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } - void LightState::publish_state() { if (this->remote_values_listeners_) { for (auto *listener : *this->remote_values_listeners_) { @@ -194,25 +192,11 @@ void LightState::add_target_state_reached_listener(LightTargetStateReachedListen this->target_state_reached_listeners_->push_back(listener); } -void LightState::set_default_transition_length(uint32_t default_transition_length) { - this->default_transition_length_ = default_transition_length; -} -uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; } -void LightState::set_flash_transition_length(uint32_t flash_transition_length) { - this->flash_transition_length_ = flash_transition_length; -} -uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } -void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } -void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } -void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; } -bool LightState::supports_effects() { return !this->effects_.empty(); } -const FixedVector &LightState::get_effects() const { return this->effects_; } void LightState::add_effects(const std::initializer_list &effects) { // Called once from Python codegen during setup with all effects from YAML config this->effects_ = effects; } -void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void LightState::current_values_as_brightness(float *brightness) { this->current_values.as_brightness(brightness); *brightness = this->gamma_correct_lut(*brightness); @@ -333,8 +317,6 @@ float LightState::gamma_uncorrect_lut(float value) const { } #endif // USE_LIGHT_GAMMA_LUT -bool LightState::is_transformer_active() { return this->is_transformer_active_; } - void LightState::start_effect_(uint32_t effect_index) { this->stop_effect_(); if (effect_index == 0) diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 5efc05358b..3a3f8fc368 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -109,7 +109,7 @@ class LightState : public EntityBase, public Component { void dump_config() override; void loop() override; /// Shortly after HARDWARE. - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; } /** The current values of the light as outputted to the light. * @@ -157,15 +157,19 @@ class LightState : public EntityBase, public Component { void add_target_state_reached_listener(LightTargetStateReachedListener *listener); /// Set the default transition length, i.e. the transition length when no transition is provided. - void set_default_transition_length(uint32_t default_transition_length); - uint32_t get_default_transition_length() const; + void set_default_transition_length(uint32_t default_transition_length) { + this->default_transition_length_ = default_transition_length; + } + uint32_t get_default_transition_length() const { return this->default_transition_length_; } /// Set the flash transition length - void set_flash_transition_length(uint32_t flash_transition_length); - uint32_t get_flash_transition_length() const; + void set_flash_transition_length(uint32_t flash_transition_length) { + this->flash_transition_length_ = flash_transition_length; + } + uint32_t get_flash_transition_length() const { return this->flash_transition_length_; } /// Set the gamma correction factor - void set_gamma_correct(float gamma_correct); + void set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } float get_gamma_correct() const { return this->gamma_correct_; } #ifdef USE_LIGHT_GAMMA_LUT @@ -186,17 +190,17 @@ class LightState : public EntityBase, public Component { #endif // USE_LIGHT_GAMMA_LUT /// Set the restore mode of this light - void set_restore_mode(LightRestoreMode restore_mode); + void set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } /// Set a callback to populate the initial state defaults during setup. /// The callback is called once, then cleared. Values live in flash as code. - void set_initial_state(void (*callback)(LightStateRTCState &)); + void set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; } /// Return whether the light has any effects that meet the trait requirements. - bool supports_effects(); + bool supports_effects() const { return !this->effects_.empty(); } /// Get all effects for this light state. - const FixedVector &get_effects() const; + const FixedVector &get_effects() const { return this->effects_; } /// Add effects for this light state. void add_effects(const std::initializer_list &effects); @@ -254,7 +258,7 @@ class LightState : public EntityBase, public Component { } /// The result of all the current_values_as_* methods have gamma correction applied. - void current_values_as_binary(bool *binary); + void current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void current_values_as_brightness(float *brightness); @@ -281,7 +285,7 @@ class LightState : public EntityBase, public Component { * return; * } */ - bool is_transformer_active(); + bool is_transformer_active() const { return this->is_transformer_active_; } protected: friend LightOutput; From 763a1d9371690543487037fa97a53d2bb2ea03a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:01:16 -0500 Subject: [PATCH 056/155] [select] Inline the trivial Select accessors (#18621) --- esphome/components/select/select.cpp | 17 ----------------- esphome/components/select/select.h | 14 ++++++++------ esphome/components/select/select_traits.cpp | 2 -- esphome/components/select/select_traits.h | 2 +- 4 files changed, 9 insertions(+), 26 deletions(-) diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 17c6c811dd..05a0ee1ed9 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -8,8 +8,6 @@ namespace esphome::select { static const char *const TAG = "select"; -void Select::publish_state(const std::string &state) { this->publish_state(state.c_str()); } - void Select::publish_state(const char *state) { auto index = this->index_of(state); if (index.has_value()) { @@ -34,21 +32,6 @@ void Select::publish_state(size_t index) { #endif } -StringRef Select::current_option() const { - return this->has_state() ? StringRef(this->option_at(this->active_index_)) : StringRef(); -} - -bool Select::has_option(const std::string &option) const { return this->index_of(option.c_str()).has_value(); } - -bool Select::has_option(const char *option) const { return this->index_of(option).has_value(); } - -bool Select::has_index(size_t index) const { return index < this->size(); } - -size_t Select::size() const { - const auto &options = traits.get_options(); - return options.size(); -} - optional Select::index_of(const char *option, size_t len) const { const auto &options = traits.get_options(); for (size_t i = 0; i < options.size(); i++) { diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 34d9248523..2294f34e62 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -33,27 +33,29 @@ class Select : public EntityBase { Select() = default; ~Select() = default; - void publish_state(const std::string &state); + void publish_state(const std::string &state) { this->publish_state(state.c_str()); } void publish_state(const char *state); void publish_state(size_t index); /// Return the currently selected option, or empty StringRef if no state. /// The returned StringRef points to string literals from codegen (static storage). /// Traits are set once at startup and valid for the lifetime of the program. - StringRef current_option() const; + StringRef current_option() const { + return this->has_state() ? StringRef(this->option_at(this->active_index_)) : StringRef(); + } /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } /// Return whether this select component contains the provided option. - bool has_option(const std::string &option) const; - bool has_option(const char *option) const; + bool has_option(const std::string &option) const { return this->index_of(option).has_value(); } + bool has_option(const char *option) const { return this->index_of(option).has_value(); } /// Return whether this select component contains the provided index offset. - bool has_index(size_t index) const; + bool has_index(size_t index) const { return index < this->size(); } /// Return the number of options in this select component. - size_t size() const; + size_t size() const { return this->traits.get_options().size(); } /// Find the (optional) index offset of the provided option value. optional index_of(const char *option, size_t len) const; diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index ff52c0d85b..67a5118646 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -11,6 +11,4 @@ void SelectTraits::set_options(const FixedVector &options) { } } -const FixedVector &SelectTraits::get_options() const { return this->options_; } - } // namespace esphome::select diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index 78a83e5944..e1b261bc96 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -9,7 +9,7 @@ class SelectTraits { public: void set_options(const std::initializer_list &options); void set_options(const FixedVector &options); - const FixedVector &get_options() const; + const FixedVector &get_options() const { return this->options_; } protected: FixedVector options_; From c60062c418b3a2e2fb841557d611bd0f341ae551 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:01:28 -0500 Subject: [PATCH 057/155] [sensor] Inline the trivial ExponentialMovingAverageFilter setters (#18622) --- esphome/components/sensor/filter.cpp | 2 -- esphome/components/sensor/filter.h | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 0105580d26..dbd6f4d34b 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -164,8 +164,6 @@ optional ExponentialMovingAverageFilter::new_value(float value) { } return {}; } -void ExponentialMovingAverageFilter::set_send_every(uint16_t send_every) { this->send_every_ = send_every; } -void ExponentialMovingAverageFilter::set_alpha(float alpha) { this->alpha_ = alpha; } // ThrottleAverageFilter ThrottleAverageFilter::ThrottleAverageFilter(uint32_t time_period) : time_period_(time_period) {} diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index b79bfa17d6..bc086e3805 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -239,8 +239,8 @@ class ExponentialMovingAverageFilter : public Filter { optional new_value(float value) override; - void set_send_every(uint16_t send_every); - void set_alpha(float alpha); + void set_send_every(uint16_t send_every) { this->send_every_ = send_every; } + void set_alpha(float alpha) { this->alpha_ = alpha; } protected: float accumulator_{NAN}; From efc0a94112f93d25b9806a332310a877faebe6b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:01:42 -0500 Subject: [PATCH 058/155] [text_sensor] Inline the trivial TextSensor forwarding overloads (#18623) --- esphome/components/text_sensor/text_sensor.cpp | 8 -------- esphome/components/text_sensor/text_sensor.h | 8 +++++--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index d2483619a6..17c606d253 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -18,10 +18,6 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text LOG_ENTITY_ICON(tag, prefix, *obj); } -void TextSensor::publish_state(const std::string &state) { this->publish_state(state.data(), state.size()); } - -void TextSensor::publish_state(const char *state) { this->publish_state(state, strlen(state)); } - void TextSensor::publish_state(const char *state, size_t len) { #ifdef USE_TEXT_SENSOR_FILTER if (this->filter_list_ == nullptr) { @@ -91,10 +87,6 @@ const std::string &TextSensor::get_raw_state() const { #endif return this->state; // No filters, raw == filtered } -void TextSensor::internal_send_state_to_frontend(const std::string &state) { - this->internal_send_state_to_frontend(state.data(), state.size()); -} - void TextSensor::internal_send_state_to_frontend(const char *state, size_t len) { // Only assign if changed to avoid heap allocation if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index aa48781f41..0e7364bf98 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -37,8 +37,8 @@ class TextSensor : public EntityBase { /// Returns the raw (pre-filter) state. const std::string &get_raw_state() const; - void publish_state(const std::string &state); - void publish_state(const char *state); + void publish_state(const std::string &state) { this->publish_state(state.data(), state.size()); } + void publish_state(const char *state) { this->publish_state(state, strlen(state)); } void publish_state(const char *state, size_t len); #ifdef USE_TEXT_SENSOR_FILTER @@ -70,7 +70,9 @@ class TextSensor : public EntityBase { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void internal_send_state_to_frontend(const std::string &state); + void internal_send_state_to_frontend(const std::string &state) { + this->internal_send_state_to_frontend(state.data(), state.size()); + } void internal_send_state_to_frontend(const char *state, size_t len); protected: From 5c2286cc4a1ea1cd3392df32ec1dc27f4ca4a27b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:01:55 -0500 Subject: [PATCH 059/155] [climate] Inline the trivial visual override setters (#18624) --- esphome/components/climate/climate.cpp | 23 ----------------------- esphome/components/climate/climate.h | 21 ++++++++++++++++----- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index b41ca4a540..0f01443bd0 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -511,29 +511,6 @@ ClimateTraits Climate::get_traits() { return traits; } -#ifdef USE_CLIMATE_VISUAL_OVERRIDES -void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) { - this->visual_min_temperature_override_ = visual_min_temperature_override; -} - -void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) { - this->visual_max_temperature_override_ = visual_max_temperature_override; -} - -void Climate::set_visual_temperature_step_override(float target, float current) { - this->visual_target_temperature_step_override_ = target; - this->visual_current_temperature_step_override_ = current; -} - -void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { - this->visual_min_humidity_override_ = visual_min_humidity_override; -} - -void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { - this->visual_max_humidity_override_ = visual_max_humidity_override; -} -#endif - ClimateCall Climate::make_call() { return ClimateCall(this); } ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 04f653a2b0..a906897235 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -228,11 +228,22 @@ class Climate : public EntityBase { ClimateTraits get_traits(); #ifdef USE_CLIMATE_VISUAL_OVERRIDES - void set_visual_min_temperature_override(float visual_min_temperature_override); - void set_visual_max_temperature_override(float visual_max_temperature_override); - void set_visual_temperature_step_override(float target, float current); - void set_visual_min_humidity_override(float visual_min_humidity_override); - void set_visual_max_humidity_override(float visual_max_humidity_override); + void set_visual_min_temperature_override(float visual_min_temperature_override) { + this->visual_min_temperature_override_ = visual_min_temperature_override; + } + void set_visual_max_temperature_override(float visual_max_temperature_override) { + this->visual_max_temperature_override_ = visual_max_temperature_override; + } + void set_visual_temperature_step_override(float target, float current) { + this->visual_target_temperature_step_override_ = target; + this->visual_current_temperature_step_override_ = current; + } + void set_visual_min_humidity_override(float visual_min_humidity_override) { + this->visual_min_humidity_override_ = visual_min_humidity_override; + } + void set_visual_max_humidity_override(float visual_max_humidity_override) { + this->visual_max_humidity_override_ = visual_max_humidity_override; + } #endif /// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits). From ce019f508d3a78a31e17bf71f98a70ef0d63419e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:02:04 -0500 Subject: [PATCH 060/155] [safe_mode] Inline the trivial set_safe_mode setters (#18626) --- esphome/components/safe_mode/button/safe_mode_button.cpp | 4 ---- esphome/components/safe_mode/button/safe_mode_button.h | 2 +- esphome/components/safe_mode/switch/safe_mode_switch.cpp | 4 ---- esphome/components/safe_mode/switch/safe_mode_switch.h | 2 +- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/esphome/components/safe_mode/button/safe_mode_button.cpp b/esphome/components/safe_mode/button/safe_mode_button.cpp index 04203854fb..982ecf8402 100644 --- a/esphome/components/safe_mode/button/safe_mode_button.cpp +++ b/esphome/components/safe_mode/button/safe_mode_button.cpp @@ -7,10 +7,6 @@ namespace esphome::safe_mode { static const char *const TAG = "safe_mode.button"; -void SafeModeButton::set_safe_mode(SafeModeComponent *safe_mode_component) { - this->safe_mode_component_ = safe_mode_component; -} - void SafeModeButton::press_action() { ESP_LOGI(TAG, "Restarting in safe mode"); this->safe_mode_component_->set_safe_mode_pending(true); diff --git a/esphome/components/safe_mode/button/safe_mode_button.h b/esphome/components/safe_mode/button/safe_mode_button.h index 6012bb2aeb..035bd77802 100644 --- a/esphome/components/safe_mode/button/safe_mode_button.h +++ b/esphome/components/safe_mode/button/safe_mode_button.h @@ -9,7 +9,7 @@ namespace esphome::safe_mode { class SafeModeButton final : public button::Button, public Component { public: void dump_config() override; - void set_safe_mode(SafeModeComponent *safe_mode_component); + void set_safe_mode(SafeModeComponent *safe_mode_component) { this->safe_mode_component_ = safe_mode_component; } protected: SafeModeComponent *safe_mode_component_; diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.cpp b/esphome/components/safe_mode/switch/safe_mode_switch.cpp index f513465db0..b4b9735757 100644 --- a/esphome/components/safe_mode/switch/safe_mode_switch.cpp +++ b/esphome/components/safe_mode/switch/safe_mode_switch.cpp @@ -7,10 +7,6 @@ namespace esphome::safe_mode { static const char *const TAG = "safe_mode.switch"; -void SafeModeSwitch::set_safe_mode(SafeModeComponent *safe_mode_component) { - this->safe_mode_component_ = safe_mode_component; -} - void SafeModeSwitch::write_state(bool state) { // Acknowledge this->publish_state(false); diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.h b/esphome/components/safe_mode/switch/safe_mode_switch.h index cbd79cd520..cb48023f63 100644 --- a/esphome/components/safe_mode/switch/safe_mode_switch.h +++ b/esphome/components/safe_mode/switch/safe_mode_switch.h @@ -9,7 +9,7 @@ namespace esphome::safe_mode { class SafeModeSwitch final : public switch_::Switch, public Component { public: void dump_config() override; - void set_safe_mode(SafeModeComponent *safe_mode_component); + void set_safe_mode(SafeModeComponent *safe_mode_component) { this->safe_mode_component_ = safe_mode_component; } protected: SafeModeComponent *safe_mode_component_; From dba3b287dd817b9ad68941b7fd1a8294bdc4a616 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:02:14 -0500 Subject: [PATCH 061/155] [api] Inline the trivial APIServer accessors (#18627) --- esphome/components/api/api_server.cpp | 10 ---------- esphome/components/api/api_server.h | 10 +++++----- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ef5b43d7b1..2d5f9e4155 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -423,12 +423,6 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_ API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif -float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } - -void APIServer::set_port(uint16_t port) { this->port_ = port; } - -void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } - #ifdef USE_API_HOMEASSISTANT_SERVICES void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) { bool has_subscriber = false; @@ -553,10 +547,6 @@ const std::vector &APIServer::get_sta } #endif -uint16_t APIServer::get_port() const { return this->port_; } - -void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } - #ifdef USE_API_NOISE bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool make_active) { diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 248b83a0ff..a58e42534b 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -51,8 +51,8 @@ class APIServer final : public Component, public: APIServer(); void setup() override; - uint16_t get_port() const; - float get_setup_priority() const override; + uint16_t get_port() const { return this->port_; } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void loop() override; void dump_config() override; void on_shutdown() override; @@ -63,9 +63,9 @@ class APIServer final : public Component, #ifdef USE_CAMERA void on_camera_image(const std::shared_ptr &image) override; #endif - void set_port(uint16_t port); - void set_reboot_timeout(uint32_t reboot_timeout); - void set_batch_delay(uint16_t batch_delay); + void set_port(uint16_t port) { this->port_ = port; } + void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } + void set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } uint16_t get_batch_delay() const { return batch_delay_; } void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; } From 0e915e9b8bf709acd8b63258cfb6d7bd27b2a85b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:02:58 -0500 Subject: [PATCH 062/155] [core] Inline the ESPTime::strftime std::string overload (#18628) --- esphome/core/time.cpp | 2 -- esphome/core/time.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index b6fc9b90ad..d1ba981e95 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -114,8 +114,6 @@ std::string ESPTime::strftime(const char *format) { return std::string(buf, len); } -std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); } - // Helper to parse exactly N digits, returns false if not enough digits static bool parse_digits(const char *&p, const char *end, int count, uint16_t &value) { value = 0; diff --git a/esphome/core/time.h b/esphome/core/time.h index 0b67b7b3fc..f58cf20b4e 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -71,7 +71,7 @@ struct ESPTime { * @warning This method can return "ERROR" when the underlying strftime() call fails or when the * output exceeds STRFTIME_BUFFER_SIZE bytes. */ - std::string strftime(const std::string &format); + std::string strftime(const std::string &format) { return this->strftime(format.c_str()); } /// @copydoc strftime(const std::string &format) std::string strftime(const char *format); From 3ef5a8e6a4cca3f061712c39a8757c8e1a001eb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:03:07 -0500 Subject: [PATCH 063/155] [water_heater] Inline the trivial visual override setters (#18629) --- esphome/components/water_heater/water_heater.cpp | 12 ------------ esphome/components/water_heater/water_heater.h | 12 +++++++++--- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 9ee8faadee..9862253ad9 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -233,18 +233,6 @@ WaterHeaterTraits WaterHeater::get_traits() { return traits; } -#ifdef USE_WATER_HEATER_VISUAL_OVERRIDES -void WaterHeater::set_visual_min_temperature_override(float min_temperature_override) { - this->visual_min_temperature_override_ = min_temperature_override; -} -void WaterHeater::set_visual_max_temperature_override(float max_temperature_override) { - this->visual_max_temperature_override_ = max_temperature_override; -} -void WaterHeater::set_visual_target_temperature_step_override(float visual_target_temperature_step_override) { - this->visual_target_temperature_step_override_ = visual_target_temperature_step_override; -} -#endif - // Water heater mode strings indexed by WaterHeaterMode enum (0-6): OFF, ECO, ELECTRIC, PERFORMANCE, HIGH_DEMAND, // HEAT_PUMP, GAS PROGMEM_STRING_TABLE(WaterHeaterModeStrings, "OFF", "ECO", "ELECTRIC", "PERFORMANCE", "HIGH_DEMAND", "HEAT_PUMP", "GAS", diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index 995b815440..1255a68595 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -217,9 +217,15 @@ class WaterHeater : public EntityBase { virtual WaterHeaterCallInternal make_call() = 0; #ifdef USE_WATER_HEATER_VISUAL_OVERRIDES - void set_visual_min_temperature_override(float min_temperature_override); - void set_visual_max_temperature_override(float max_temperature_override); - void set_visual_target_temperature_step_override(float visual_target_temperature_step_override); + void set_visual_min_temperature_override(float min_temperature_override) { + this->visual_min_temperature_override_ = min_temperature_override; + } + void set_visual_max_temperature_override(float max_temperature_override) { + this->visual_max_temperature_override_ = max_temperature_override; + } + void set_visual_target_temperature_step_override(float visual_target_temperature_step_override) { + this->visual_target_temperature_step_override_ = visual_target_temperature_step_override; + } #endif virtual void control(const WaterHeaterCall &call) = 0; From cb4e55e4449b08dac696d38dc4314216a8c9b332 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:03:15 -0500 Subject: [PATCH 064/155] [cover] Inline the trivial Cover and CoverCall accessors (#18630) --- esphome/components/cover/cover.cpp | 7 ------- esphome/components/cover/cover.h | 8 ++++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index e98a555fe5..dc2db3bf32 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -135,10 +135,6 @@ CoverCall &CoverCall::set_stop(bool stop) { this->stop_ = stop; return *this; } -bool CoverCall::get_stop() const { return this->stop_; } - -CoverCall Cover::make_call() { return {this}; } - void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f); @@ -184,9 +180,6 @@ optional Cover::restore_state_() { return recovered; } -bool Cover::is_fully_open() const { return this->position == COVER_OPEN; } -bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; } - CoverCall CoverRestoreState::to_call(Cover *cover) { auto call = cover->make_call(); auto traits = cover->get_traits(); diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 9a75e68487..8bf45cfb57 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -50,7 +50,7 @@ class CoverCall { void perform(); const optional &get_position() const; - bool get_stop() const; + bool get_stop() const { return this->stop_; } const optional &get_tilt() const; const optional &get_toggle() const; @@ -123,7 +123,7 @@ class Cover : public EntityBase { float tilt{COVER_OPEN}; /// Construct a new cover call used to control the cover. - CoverCall make_call(); + CoverCall make_call() { return {this}; } template void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward(f)); } @@ -139,9 +139,9 @@ class Cover : public EntityBase { virtual CoverTraits get_traits() = 0; /// Helper method to check if the cover is fully open. Equivalent to comparing .position against 1.0 - bool is_fully_open() const; + bool is_fully_open() const { return this->position == COVER_OPEN; } /// Helper method to check if the cover is fully closed. Equivalent to comparing .position against 0.0 - bool is_fully_closed() const; + bool is_fully_closed() const { return this->position == COVER_CLOSED; } protected: friend CoverCall; From fedb3ac5c1999f03eb4f47f1b63c2d23b37ed47f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:03:38 -0500 Subject: [PATCH 065/155] [fan] Inline the trivial Fan call helpers (#18631) --- esphome/components/fan/fan.cpp | 5 ----- esphome/components/fan/fan.h | 8 ++++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 853bf94ffe..7dc0b5c6fe 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -153,11 +153,6 @@ void FanRestoreState::apply(Fan &fan) { fan.publish_state(); } -FanCall Fan::turn_on() { return this->make_call().set_state(true); } -FanCall Fan::turn_off() { return this->make_call().set_state(false); } -FanCall Fan::toggle() { return this->make_call().set_state(!this->state); } -FanCall Fan::make_call() { return FanCall(*this); } - const char *Fan::find_preset_mode_(const char *preset_mode) { return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0); } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 3d731e6eb0..106e6e74cd 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -115,10 +115,10 @@ class Fan : public EntityBase { /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; - FanCall turn_on(); - FanCall turn_off(); - FanCall toggle(); - FanCall make_call(); + FanCall turn_on() { return this->make_call().set_state(true); } + FanCall turn_off() { return this->make_call().set_state(false); } + FanCall toggle() { return this->make_call().set_state(!this->state); } + FanCall make_call() { return FanCall(*this); } /// Register a callback that will be called each time the state changes. template void add_on_state_callback(F &&callback) { From 832a738588e2d308e981c71a6620e894a2ac356d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:03:51 -0500 Subject: [PATCH 066/155] [switch] Inline the trivial inverted accessors (#18632) --- esphome/components/switch/switch.cpp | 3 --- esphome/components/switch/switch.h | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index abc7338a62..101a0b9ffa 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -69,9 +69,6 @@ void Switch::publish_state(bool state) { } bool Switch::assumed_state() { return false; } -void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; } -bool Switch::is_inverted() const { return this->inverted_; } - void log_switch(const char *tag, const char *prefix, const char *type, Switch *obj) { if (obj != nullptr) { // Prepare restore mode string diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index b7761cba0a..0564c3efd2 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -87,7 +87,7 @@ class Switch : public EntityBase { * * @param inverted Whether to invert this switch. */ - void set_inverted(bool inverted); + void set_inverted(bool inverted) { this->inverted_ = inverted; } /** Set callback for state changes. * @@ -117,7 +117,7 @@ class Switch : public EntityBase { */ virtual bool assumed_state(); - bool is_inverted() const; + bool is_inverted() const { return this->inverted_; } void set_restore_mode(SwitchRestoreMode restore_mode) { this->restore_mode = restore_mode; } From ad1a4fca3653f4cc98da63abe7b66f434f7ee66c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:04:08 -0500 Subject: [PATCH 067/155] [version] Inline the trivial VersionTextSensor setters (#18633) --- esphome/components/version/version_text_sensor.cpp | 2 -- esphome/components/version/version_text_sensor.h | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 34c7aae6bc..15e6b0d088 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -48,8 +48,6 @@ void VersionTextSensor::setup() { version_str[sizeof(version_str) - 1] = '\0'; this->publish_state(version_str); } -void VersionTextSensor::set_hide_hash(bool hide_hash) { this->hide_hash_ = hide_hash; } -void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } } // namespace esphome::version diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index d2ca0ba6f6..96f72ad035 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -7,8 +7,8 @@ namespace esphome::version { class VersionTextSensor final : public text_sensor::TextSensor, public Component { public: - void set_hide_hash(bool hide_hash); - void set_hide_timestamp(bool hide_timestamp); + void set_hide_hash(bool hide_hash) { this->hide_hash_ = hide_hash; } + void set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void setup() override; void dump_config() override; From ecb007da70a94f6605d01f8d775c27a639ae5e02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:04:23 -0500 Subject: [PATCH 068/155] [deep_sleep] Inline the trivial DeepSleepComponent setters (#18634) --- esphome/components/deep_sleep/deep_sleep_component.cpp | 8 -------- esphome/components/deep_sleep/deep_sleep_component.h | 10 +++++----- esphome/components/deep_sleep/deep_sleep_esp32.cpp | 6 ------ 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index e7ce70b60c..9a3e537e05 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -43,10 +43,6 @@ void DeepSleepComponent::loop() { this->begin_sleep(); } -void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } - -void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } - void DeepSleepComponent::begin_sleep(bool manual) { if (this->prevent_ && !manual) { this->next_enter_deep_sleep_ = true; @@ -76,8 +72,4 @@ void DeepSleepComponent::begin_sleep(bool manual) { float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } -void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } - -void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; } - } // namespace esphome::deep_sleep diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index a620d52a02..208f88d707 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -132,7 +132,7 @@ template class PreventDeepSleepAction; class DeepSleepComponent final : public Component { public: /// Set the duration in ms the component should sleep once it's in deep sleep mode. - void set_sleep_duration(uint32_t time_ms); + void set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } #if defined(USE_ESP32) /** Set the pin to wake up to on the ESP32 once it's in deep sleep mode. * Use the inverted property to set the wakeup level. @@ -157,7 +157,7 @@ class DeepSleepComponent final : public Component { #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ !defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2) - void set_touch_wakeup(bool touch_wakeup); + void set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } #endif // Set the duration in ms for how long the code should run before entering @@ -166,7 +166,7 @@ class DeepSleepComponent final : public Component { #endif // USE_ESP32 /// Set a duration in ms for how long the code should run before entering deep sleep mode. - void set_run_duration(uint32_t time_ms); + void set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } void setup() override; void dump_config() override; @@ -176,8 +176,8 @@ class DeepSleepComponent final : public Component { /// Helper to enter deep sleep mode void begin_sleep(bool manual = false); - void prevent_deep_sleep(); - void allow_deep_sleep(); + void prevent_deep_sleep() { this->prevent_ = true; } + void allow_deep_sleep() { this->prevent_ = false; } protected: // Returns nullopt if no run duration is set. Otherwise, returns the run diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index f64e1f37e1..3fa1a1f1ed 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -74,12 +74,6 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } #endif -#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ - !defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ - !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2) -void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } -#endif - void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; } From 5a9f06e584ac8e771f9aaca53ae85574f5d7776b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:04:41 -0500 Subject: [PATCH 069/155] [thermostat] Inline the trivial ThermostatClimate setters and getters (#18635) --- .../thermostat/thermostat_climate.cpp | 95 -------------- .../thermostat/thermostat_climate.h | 116 +++++++++++------- 2 files changed, 74 insertions(+), 137 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 2390a96337..c10eb5b9f5 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -76,11 +76,6 @@ void ThermostatClimate::loop() { } } -float ThermostatClimate::cool_deadband() { return this->cooling_deadband_; } -float ThermostatClimate::cool_overrun() { return this->cooling_overrun_; } -float ThermostatClimate::heat_deadband() { return this->heating_deadband_; } -float ThermostatClimate::heat_overrun() { return this->heating_overrun_; } - void ThermostatClimate::refresh() { this->switch_to_mode_(this->mode, false); this->switch_to_action_(this->compute_action_(), false); @@ -121,8 +116,6 @@ bool ThermostatClimate::fan_mode_change_delayed() { climate::ClimateAction ThermostatClimate::delayed_climate_action() { return this->compute_action_(true); } -climate::ClimateFanMode ThermostatClimate::locked_fan_mode() { return this->prev_fan_mode_; } - bool ThermostatClimate::hysteresis_valid() { if ((this->supports_cool_ || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) && (std::isnan(this->cooling_deadband_) || std::isnan(this->cooling_overrun_))) @@ -1286,10 +1279,6 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem return something_changed; } -void ThermostatClimate::set_preset_config(std::initializer_list presets) { - this->preset_config_ = presets; -} - void ThermostatClimate::set_custom_preset_config(std::initializer_list presets) { this->custom_preset_config_ = presets; // Populate Climate base class custom presets vector @@ -1317,19 +1306,6 @@ void ThermostatClimate::set_default_preset(const char *custom_preset) { void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } -void ThermostatClimate::set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from) { - this->on_boot_restore_from_ = on_boot_restore_from; -} -void ThermostatClimate::set_set_point_minimum_differential(float differential) { - this->set_point_minimum_differential_ = differential; -} -void ThermostatClimate::set_cool_deadband(float deadband) { this->cooling_deadband_ = deadband; } -void ThermostatClimate::set_cool_overrun(float overrun) { this->cooling_overrun_ = overrun; } -void ThermostatClimate::set_heat_deadband(float deadband) { this->heating_deadband_ = deadband; } -void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; } -void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } -void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } - void ThermostatClimate::set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time) { uint32_t new_duration_ms = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); @@ -1389,80 +1365,9 @@ void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_IDLE_ON, time); } -void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } -void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { - this->humidity_sensor_ = humidity_sensor; -} void ThermostatClimate::set_humidity_hysteresis(float humidity_hysteresis) { this->humidity_hysteresis_ = std::clamp(humidity_hysteresis, 0.0f, 100.0f); } -void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } -void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { - this->supports_heat_cool_ = supports_heat_cool; -} -void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } -void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } -void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } -void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } -void ThermostatClimate::set_supports_fan_only_action_uses_fan_mode_timer( - bool supports_fan_only_action_uses_fan_mode_timer) { - this->supports_fan_only_action_uses_fan_mode_timer_ = supports_fan_only_action_uses_fan_mode_timer; -} -void ThermostatClimate::set_supports_fan_only_cooling(bool supports_fan_only_cooling) { - this->supports_fan_only_cooling_ = supports_fan_only_cooling; -} -void ThermostatClimate::set_supports_fan_with_cooling(bool supports_fan_with_cooling) { - this->supports_fan_with_cooling_ = supports_fan_with_cooling; -} -void ThermostatClimate::set_supports_fan_with_heating(bool supports_fan_with_heating) { - this->supports_fan_with_heating_ = supports_fan_with_heating; -} -void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } -void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { - this->supports_fan_mode_on_ = supports_fan_mode_on; -} -void ThermostatClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) { - this->supports_fan_mode_off_ = supports_fan_mode_off; -} -void ThermostatClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { - this->supports_fan_mode_auto_ = supports_fan_mode_auto; -} -void ThermostatClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) { - this->supports_fan_mode_low_ = supports_fan_mode_low; -} -void ThermostatClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { - this->supports_fan_mode_medium_ = supports_fan_mode_medium; -} -void ThermostatClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) { - this->supports_fan_mode_high_ = supports_fan_mode_high; -} -void ThermostatClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { - this->supports_fan_mode_middle_ = supports_fan_mode_middle; -} -void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { - this->supports_fan_mode_focus_ = supports_fan_mode_focus; -} -void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { - this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; -} -void ThermostatClimate::set_supports_fan_mode_quiet(bool supports_fan_mode_quiet) { - this->supports_fan_mode_quiet_ = supports_fan_mode_quiet; -} -void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { - this->supports_swing_mode_both_ = supports_swing_mode_both; -} -void ThermostatClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) { - this->supports_swing_mode_off_ = supports_swing_mode_off; -} -void ThermostatClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { - this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; -} -void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { - this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; -} -void ThermostatClimate::set_supports_two_points(bool supports_two_points) { - this->supports_two_points_ = supports_two_points; -} void ThermostatClimate::set_supports_dehumidification(bool supports_dehumidification) { this->supports_dehumidification_ = supports_dehumidification; if (supports_dehumidification) { diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index f30659a8a6..4dc2a74d8e 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -93,14 +93,16 @@ class ThermostatClimate final : public climate::Climate, public Component { void set_default_preset(const char *custom_preset); void set_default_preset(climate::ClimatePreset preset); - void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); - void set_set_point_minimum_differential(float differential); - void set_cool_deadband(float deadband); - void set_cool_overrun(float overrun); - void set_heat_deadband(float deadband); - void set_heat_overrun(float overrun); - void set_supplemental_cool_delta(float delta); - void set_supplemental_heat_delta(float delta); + void set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from) { + this->on_boot_restore_from_ = on_boot_restore_from; + } + void set_set_point_minimum_differential(float differential) { this->set_point_minimum_differential_ = differential; } + void set_cool_deadband(float deadband) { this->cooling_deadband_ = deadband; } + void set_cool_overrun(float overrun) { this->cooling_overrun_ = overrun; } + void set_heat_deadband(float deadband) { this->heating_deadband_ = deadband; } + void set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; } + void set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } + void set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } void set_cooling_maximum_run_time_in_sec(uint32_t time); void set_heating_maximum_run_time_in_sec(uint32_t time); void set_cooling_minimum_off_time_in_sec(uint32_t time); @@ -111,39 +113,69 @@ class ThermostatClimate final : public climate::Climate, public Component { void set_heating_minimum_off_time_in_sec(uint32_t time); void set_heating_minimum_run_time_in_sec(uint32_t time); void set_idle_minimum_time_in_sec(uint32_t time); - void set_sensor(sensor::Sensor *sensor); - void set_humidity_sensor(sensor::Sensor *humidity_sensor); + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } void set_humidity_hysteresis(float humidity_hysteresis); - void set_use_startup_delay(bool use_startup_delay); - void set_supports_auto(bool supports_auto); - void set_supports_heat_cool(bool supports_heat_cool); - void set_supports_cool(bool supports_cool); - void set_supports_dry(bool supports_dry); - void set_supports_fan_only(bool supports_fan_only); - void set_supports_fan_only_action_uses_fan_mode_timer(bool fan_only_action_uses_fan_mode_timer); - void set_supports_fan_only_cooling(bool supports_fan_only_cooling); - void set_supports_fan_with_cooling(bool supports_fan_with_cooling); - void set_supports_fan_with_heating(bool supports_fan_with_heating); - void set_supports_heat(bool supports_heat); - void set_supports_fan_mode_on(bool supports_fan_mode_on); - void set_supports_fan_mode_off(bool supports_fan_mode_off); - void set_supports_fan_mode_auto(bool supports_fan_mode_auto); - void set_supports_fan_mode_low(bool supports_fan_mode_low); - void set_supports_fan_mode_medium(bool supports_fan_mode_medium); - void set_supports_fan_mode_high(bool supports_fan_mode_high); - void set_supports_fan_mode_middle(bool supports_fan_mode_middle); - void set_supports_fan_mode_focus(bool supports_fan_mode_focus); - void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); - void set_supports_fan_mode_quiet(bool supports_fan_mode_quiet); - void set_supports_swing_mode_both(bool supports_swing_mode_both); - void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); - void set_supports_swing_mode_off(bool supports_swing_mode_off); - void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } + void set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } + void set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; } + void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } + void set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } + void set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } + void set_supports_fan_only_action_uses_fan_mode_timer(bool supports_fan_only_action_uses_fan_mode_timer) { + this->supports_fan_only_action_uses_fan_mode_timer_ = supports_fan_only_action_uses_fan_mode_timer; + } + void set_supports_fan_only_cooling(bool supports_fan_only_cooling) { + this->supports_fan_only_cooling_ = supports_fan_only_cooling; + } + void set_supports_fan_with_cooling(bool supports_fan_with_cooling) { + this->supports_fan_with_cooling_ = supports_fan_with_cooling; + } + void set_supports_fan_with_heating(bool supports_fan_with_heating) { + this->supports_fan_with_heating_ = supports_fan_with_heating; + } + void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } + void set_supports_fan_mode_on(bool supports_fan_mode_on) { this->supports_fan_mode_on_ = supports_fan_mode_on; } + void set_supports_fan_mode_off(bool supports_fan_mode_off) { this->supports_fan_mode_off_ = supports_fan_mode_off; } + void set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; + } + void set_supports_fan_mode_low(bool supports_fan_mode_low) { this->supports_fan_mode_low_ = supports_fan_mode_low; } + void set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; + } + void set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; + } + void set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; + } + void set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; + } + void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; + } + void set_supports_fan_mode_quiet(bool supports_fan_mode_quiet) { + this->supports_fan_mode_quiet_ = supports_fan_mode_quiet; + } + void set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; + } + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; + } + void set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; + } + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; + } void set_supports_dehumidification(bool supports_dehumidification); void set_supports_humidification(bool supports_humidification); - void set_supports_two_points(bool supports_two_points); + void set_supports_two_points(bool supports_two_points) { this->supports_two_points_ = supports_two_points; } - void set_preset_config(std::initializer_list presets); + void set_preset_config(std::initializer_list presets) { this->preset_config_ = presets; } void set_custom_preset_config(std::initializer_list presets); Trigger<> *get_cool_action_trigger(); @@ -181,10 +213,10 @@ class ThermostatClimate final : public climate::Climate, public Component { Trigger<> *get_humidity_control_humidify_action_trigger(); Trigger<> *get_humidity_control_off_action_trigger(); /// Get current hysteresis values - float cool_deadband(); - float cool_overrun(); - float heat_deadband(); - float heat_overrun(); + float cool_deadband() { return this->cooling_deadband_; } + float cool_overrun() { return this->cooling_overrun_; } + float heat_deadband() { return this->heating_deadband_; } + float heat_overrun() { return this->heating_overrun_; } /// Call triggers based on updated climate states (modes/actions) void refresh(); /// Returns true if a climate action/fan mode transition is being delayed @@ -193,7 +225,7 @@ class ThermostatClimate final : public climate::Climate, public Component { /// Returns the climate action that is being delayed (check climate_action_change_delayed(), first!) climate::ClimateAction delayed_climate_action(); /// Returns the fan mode that is locked in (check fan_mode_change_delayed(), first!) - climate::ClimateFanMode locked_fan_mode(); + climate::ClimateFanMode locked_fan_mode() { return this->prev_fan_mode_; } /// Set point and hysteresis validation bool hysteresis_valid(); // returns true if valid bool humidity_hysteresis_valid(); // returns true if valid From 78240c9a46f63fb6e7778f2b2e5720765e7a8bd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:04:57 -0500 Subject: [PATCH 070/155] [sprinkler] Inline the trivial Sprinkler accessors (#18636) --- esphome/components/sprinkler/sprinkler.cpp | 43 --------------------- esphome/components/sprinkler/sprinkler.h | 44 +++++++++++++--------- 2 files changed, 27 insertions(+), 60 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 336123a472..2edceb76a5 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -211,8 +211,6 @@ uint32_t SprinklerValveOperator::time_remaining() { return 0; // run completed } -SprinklerState SprinklerValveOperator::state() { return this->state_; } - switch_::Switch *SprinklerValveOperator::pump_switch() { if ((this->controller_ == nullptr) || (this->valve_ == nullptr)) { return nullptr; @@ -288,11 +286,8 @@ SprinklerValveRunRequest::SprinklerValveRunRequest(size_t valve_number, uint32_t SprinklerValveOperator *valve_op) : valve_number_(valve_number), run_duration_(run_duration), valve_op_(valve_op) {} -bool SprinklerValveRunRequest::has_request() { return this->has_valve_; } bool SprinklerValveRunRequest::has_valve_operator() { return !(this->valve_op_ == nullptr); } -void SprinklerValveRunRequest::set_request_from(SprinklerValveRunRequestOrigin origin) { this->origin_ = origin; } - void SprinklerValveRunRequest::set_run_duration(uint32_t run_duration) { this->run_duration_ = run_duration; } void SprinklerValveRunRequest::set_valve(size_t valve_number) { @@ -317,8 +312,6 @@ void SprinklerValveRunRequest::reset() { uint32_t SprinklerValveRunRequest::run_duration() { return this->run_duration_; } -size_t SprinklerValveRunRequest::valve() { return this->valve_number_; } - optional SprinklerValveRunRequest::valve_as_opt() { if (this->has_valve_) { return this->valve_number_; @@ -328,8 +321,6 @@ optional SprinklerValveRunRequest::valve_as_opt() { SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; } -SprinklerValveRunRequestOrigin SprinklerValveRunRequest::request_is_from() { return this->origin_; } - Sprinkler::Sprinkler() : Sprinkler("") {} Sprinkler::Sprinkler(const char *name) : name_(name) { // The `name` is stored for dump_config logging @@ -414,18 +405,6 @@ void Sprinkler::set_controller_main_switch(SprinklerControllerSwitch *controller this->sprinkler_turn_on_automation_->add_actions({sprinkler_resumeorstart_action_.get()}); } -void Sprinkler::set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch) { - this->auto_adv_sw_ = auto_adv_switch; -} - -void Sprinkler::set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch) { - this->queue_enable_sw_ = queue_enable_switch; -} - -void Sprinkler::set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch) { - this->reverse_sw_ = reverse_switch; -} - void Sprinkler::set_controller_standby_switch(SprinklerControllerSwitch *standby_switch) { this->standby_sw_ = standby_switch; @@ -434,14 +413,6 @@ void Sprinkler::set_controller_standby_switch(SprinklerControllerSwitch *standby this->sprinkler_standby_turn_on_automation_->add_actions({sprinkler_standby_shutdown_action_.get()}); } -void Sprinkler::set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number) { - this->multiplier_number_ = multiplier_number; -} - -void Sprinkler::set_controller_repeat_number(SprinklerControllerNumber *repeat_number) { - this->repeat_number_ = repeat_number; -} - void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) { if (this->is_a_valid_valve(valve_number)) { this->valve_[valve_number].valve_switch = valve_switch; @@ -498,10 +469,6 @@ void Sprinkler::set_multiplier(const optional multiplier) { call.perform(); } -void Sprinkler::set_next_prev_ignore_disabled_valves(bool ignore_disabled) { - this->next_prev_ignore_disabled_ = ignore_disabled; -} - void Sprinkler::set_pump_start_delay(uint32_t start_delay) { this->start_delay_is_valve_delay_ = false; this->start_delay_ = start_delay; @@ -522,10 +489,6 @@ void Sprinkler::set_valve_stop_delay(uint32_t stop_delay) { this->stop_delay_ = stop_delay; } -void Sprinkler::set_pump_switch_off_during_valve_open_delay(bool pump_switch_off_during_valve_open_delay) { - this->pump_switch_off_during_valve_open_delay_ = pump_switch_off_during_valve_open_delay; -} - void Sprinkler::set_valve_open_delay(const uint32_t valve_open_delay) { if (valve_open_delay > 0) { this->valve_overlap_ = false; @@ -945,8 +908,6 @@ optional Sprinkler::active_valve() { return this->active_req_.valve_as_opt(); } -optional Sprinkler::paused_valve() { return this->paused_valve_; } - optional Sprinkler::queued_valve() { if (!this->queued_valves_.empty()) { return this->queued_valves_.back().valve_number; @@ -954,10 +915,6 @@ optional Sprinkler::queued_valve() { return nullopt; } -optional Sprinkler::manual_valve() { return this->manual_valve_; } - -size_t Sprinkler::number_of_valves() { return this->valve_.size(); } - bool Sprinkler::is_a_valid_valve(const size_t valve_number) { return (valve_number < this->number_of_valves()); } bool Sprinkler::pump_in_use(switch_::Switch *pump_switch) { diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index bd610f7ad3..2499a0a591 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -124,9 +124,9 @@ class SprinklerValveOperator { void set_stop_delay(uint32_t stop_delay, bool stop_delay_is_valve_delay); void start(); void stop(); - uint32_t run_duration(); // returns the desired run duration in seconds - uint32_t time_remaining(); // returns seconds remaining (does not include stop_delay_) - SprinklerState state(); // returns the valve's state/status + uint32_t run_duration(); // returns the desired run duration in seconds + uint32_t time_remaining(); // returns seconds remaining (does not include stop_delay_) + SprinklerState state() { return this->state_; } switch_::Switch *pump_switch(); // returns this SprinklerValveOperator's pump switch protected: @@ -152,18 +152,18 @@ class SprinklerValveRunRequest { public: SprinklerValveRunRequest(); SprinklerValveRunRequest(size_t valve_number, uint32_t run_duration, SprinklerValveOperator *valve_op); - bool has_request(); + bool has_request() { return this->has_valve_; } bool has_valve_operator(); - void set_request_from(SprinklerValveRunRequestOrigin origin); + void set_request_from(SprinklerValveRunRequestOrigin origin) { this->origin_ = origin; } void set_run_duration(uint32_t run_duration); void set_valve(size_t valve_number); void set_valve_operator(SprinklerValveOperator *valve_op); void reset(); uint32_t run_duration(); - size_t valve(); + size_t valve() { return this->valve_number_; } optional valve_as_opt(); SprinklerValveOperator *valve_operator(); - SprinklerValveRunRequestOrigin request_is_from(); + SprinklerValveRunRequestOrigin request_is_from() { return this->origin_; } protected: bool has_valve_{false}; @@ -189,14 +189,20 @@ class Sprinkler final : public Component { /// configure important controller switches void set_controller_main_switch(SprinklerControllerSwitch *controller_switch); - void set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch); - void set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch); - void set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch); + void set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch) { + this->auto_adv_sw_ = auto_adv_switch; + } + void set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch) { + this->queue_enable_sw_ = queue_enable_switch; + } + void set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch) { this->reverse_sw_ = reverse_switch; } void set_controller_standby_switch(SprinklerControllerSwitch *standby_switch); /// configure important controller number components - void set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number); - void set_controller_repeat_number(SprinklerControllerNumber *repeat_number); + void set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number) { + this->multiplier_number_ = multiplier_number; + } + void set_controller_repeat_number(SprinklerControllerNumber *repeat_number) { this->repeat_number_ = repeat_number; } /// configure a valve's switch object and run duration. run_duration is time in seconds. void configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration); @@ -214,7 +220,9 @@ class Sprinkler final : public Component { void set_multiplier(optional multiplier); /// enable/disable skipping of disabled valves by the next and previous actions - void set_next_prev_ignore_disabled_valves(bool ignore_disabled); + void set_next_prev_ignore_disabled_valves(bool ignore_disabled) { + this->next_prev_ignore_disabled_ = ignore_disabled; + } /// set how long the pump should start after the valve (when the pump is starting) void set_pump_start_delay(uint32_t start_delay); @@ -230,7 +238,9 @@ class Sprinkler final : public Component { /// if pump_switch_off_during_valve_open_delay is true, the controller will switch off the pump during the /// valve_open_delay interval - void set_pump_switch_off_during_valve_open_delay(bool pump_switch_off_during_valve_open_delay); + void set_pump_switch_off_during_valve_open_delay(bool pump_switch_off_during_valve_open_delay) { + this->pump_switch_off_during_valve_open_delay_ = pump_switch_off_during_valve_open_delay; + } /// set how long the controller should wait to open/switch on the valve after it becomes active void set_valve_open_delay(uint32_t valve_open_delay); @@ -335,17 +345,17 @@ class Sprinkler final : public Component { optional active_valve(); /// returns the number of the valve that is paused, if any. check with 'has_value()' - optional paused_valve(); + optional paused_valve() { return this->paused_valve_; } /// returns the number of the next valve in the queue, if any. check with 'has_value()' optional queued_valve(); /// returns the number of the valve that is manually selected, if any. check with 'has_value()' /// this is set by next_valve() and previous_valve() when manual_selection_delay_ > 0 - optional manual_valve(); + optional manual_valve() { return this->manual_valve_; } /// returns the number of valves the controller is configured with - size_t number_of_valves(); + size_t number_of_valves() { return this->valve_.size(); } /// returns true if valve number is valid bool is_a_valid_valve(size_t valve_number); From 47156c9a5b5c517df04bb4c943a63a407435205a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:05:39 -0500 Subject: [PATCH 071/155] [mqtt] Inline the trivial MQTT client, component and sensor accessors (#18637) --- esphome/components/mqtt/mqtt_client.cpp | 8 -------- esphome/components/mqtt/mqtt_client.h | 12 ++++++------ esphome/components/mqtt/mqtt_component.cpp | 4 ---- esphome/components/mqtt/mqtt_component.h | 4 ++-- esphome/components/mqtt/mqtt_sensor.cpp | 2 -- esphome/components/mqtt/mqtt_sensor.h | 4 ++-- 6 files changed, 10 insertions(+), 24 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index ab665e2579..1127c36dc6 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -668,9 +668,7 @@ void MQTTClientComponent::on_message(const std::string &topic, const std::string // Setters void MQTTClientComponent::disable_log_message() { this->log_message_.topic = ""; } bool MQTTClientComponent::is_log_message_enabled() const { return !this->log_message_.topic.empty(); } -void MQTTClientComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { this->children_.push_back(component); } -void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; } void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); } void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); } const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; } @@ -683,10 +681,6 @@ void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, cons } } const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; } -void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) { - this->publish_nan_as_none_ = publish_nan_as_none; -} -bool MQTTClientComponent::is_publish_nan_as_none() const { return this->publish_nan_as_none_; } void MQTTClientComponent::disable_birth_message() { this->birth_message_.topic = ""; this->recalculate_availability_(); @@ -766,8 +760,6 @@ MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines- // MQTTMessageTrigger MQTTMessageTrigger::MQTTMessageTrigger(std::string topic) : topic_(std::move(topic)) {} -void MQTTMessageTrigger::set_qos(uint8_t qos) { this->qos_ = qos; } -void MQTTMessageTrigger::set_payload(const std::string &payload) { this->payload_ = payload; } void MQTTMessageTrigger::setup() { global_mqtt_client->subscribe( this->topic_, diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index f741be561c..fe0966e725 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -159,7 +159,7 @@ class MQTTClientComponent final : public Component { /// Manually set the topic used for logging. void set_log_message_template(MQTTMessage &&message); - void set_log_level(int level); + void set_log_level(int level) { this->log_level_ = level; } /// Get the topic used for logging. Defaults to "/debug" and the value is cached for speed. void disable_log_message(); bool is_log_message_enabled() const; @@ -241,7 +241,7 @@ class MQTTClientComponent final : public Component { void check_connected(); - void set_reboot_timeout(uint32_t reboot_timeout); + void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } void register_mqtt_component(MQTTComponent *component); @@ -262,8 +262,8 @@ class MQTTClientComponent final : public Component { void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback); // Publish None state instead of NaN for Home Assistant - void set_publish_nan_as_none(bool publish_nan_as_none); - bool is_publish_nan_as_none() const; + void set_publish_nan_as_none(bool publish_nan_as_none) { this->publish_nan_as_none_ = publish_nan_as_none; } + bool is_publish_nan_as_none() const { return this->publish_nan_as_none_; } void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; } @@ -344,8 +344,8 @@ class MQTTMessageTrigger final : public Trigger, public Component { public: explicit MQTTMessageTrigger(std::string topic); - void set_qos(uint8_t qos); - void set_payload(const std::string &payload); + void set_qos(uint8_t qos) { this->qos_ = qos; } + void set_payload(const std::string &payload) { this->payload_ = payload; } void setup() override; void dump_config() override; float get_setup_priority() const override; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 3bbc1cdfa3..18a759725f 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -340,10 +340,6 @@ bool MQTTComponent::send_discovery_() { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } -uint8_t MQTTComponent::get_qos() const { return this->qos_; } - -bool MQTTComponent::get_retain() const { return this->retain_; } - bool MQTTComponent::is_discovery_enabled() const { return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled(); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 7983e04870..b4ae624404 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -108,11 +108,11 @@ class MQTTComponent : public Component { /// Set QOS for state messages. void set_qos(uint8_t qos); - uint8_t get_qos() const; + uint8_t get_qos() const { return this->qos_; } /// Set whether state message should be retained. void set_retain(bool retain); - bool get_retain() const; + bool get_retain() const { return this->retain_; } /// Disable discovery. Sets friendly name to "". void disable_discovery(); diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index c66465dd16..1c0625d1c9 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -39,8 +39,6 @@ uint32_t MQTTSensorComponent::get_expire_after() const { return *this->expire_after_; return 0; } -void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; } -void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index 1d5ee8095c..a56963d9c1 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -22,9 +22,9 @@ class MQTTSensorComponent final : public mqtt::MQTTComponent { explicit MQTTSensorComponent(sensor::Sensor *sensor); /// Setup an expiry, 0 disables it - void set_expire_after(uint32_t expire_after); + void set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; } /// Disable Home Assistant value expiry. - void disable_expire_after(); + void disable_expire_after() { this->expire_after_ = 0; } void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; From a30238aab6de17a67d1624291db47e325935ac79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:05:51 -0500 Subject: [PATCH 072/155] [wireguard] Inline the trivial Wireguard setters (#18638) --- esphome/components/wireguard/wireguard.cpp | 21 --------------------- esphome/components/wireguard/wireguard.h | 18 +++++++++--------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index b4641894db..2f07344d3b 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -178,25 +178,6 @@ time_t Wireguard::get_latest_handshake() const { return result; } -void Wireguard::set_keepalive(const uint16_t seconds) { this->keepalive_ = seconds; } -void Wireguard::set_reboot_timeout(const uint32_t seconds) { this->reboot_timeout_ = seconds; } -void Wireguard::set_srctime(time::RealTimeClock *srctime) { this->srctime_ = srctime; } - -#ifdef USE_BINARY_SENSOR -void Wireguard::set_status_sensor(binary_sensor::BinarySensor *sensor) { this->status_sensor_ = sensor; } -void Wireguard::set_enabled_sensor(binary_sensor::BinarySensor *sensor) { this->enabled_sensor_ = sensor; } -#endif - -#ifdef USE_SENSOR -void Wireguard::set_handshake_sensor(sensor::Sensor *sensor) { this->handshake_sensor_ = sensor; } -#endif - -#ifdef USE_TEXT_SENSOR -void Wireguard::set_address_sensor(text_sensor::TextSensor *sensor) { this->address_sensor_ = sensor; } -#endif - -void Wireguard::disable_auto_proceed() { this->proceed_allowed_ = false; } - void Wireguard::enable() { this->enabled_ = true; ESP_LOGI(TAG, "Enabled"); @@ -218,8 +199,6 @@ void Wireguard::publish_enabled_state() { #endif } -bool Wireguard::is_enabled() { return this->enabled_; } - void Wireguard::start_connection_() { if (!this->enabled_) { ESP_LOGV(TAG, "Disabled, cannot start connection"); diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index 1fda802415..c9c2feb7ae 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -63,25 +63,25 @@ class Wireguard final : public PollingComponent { /// Prevent accidental use of std::string which would dangle void set_allowed_ips(std::initializer_list> ips) = delete; - void set_keepalive(uint16_t seconds); - void set_reboot_timeout(uint32_t seconds); - void set_srctime(time::RealTimeClock *srctime); + void set_keepalive(const uint16_t seconds) { this->keepalive_ = seconds; } + void set_reboot_timeout(const uint32_t seconds) { this->reboot_timeout_ = seconds; } + void set_srctime(time::RealTimeClock *srctime) { this->srctime_ = srctime; } #ifdef USE_BINARY_SENSOR - void set_status_sensor(binary_sensor::BinarySensor *sensor); - void set_enabled_sensor(binary_sensor::BinarySensor *sensor); + void set_status_sensor(binary_sensor::BinarySensor *sensor) { this->status_sensor_ = sensor; } + void set_enabled_sensor(binary_sensor::BinarySensor *sensor) { this->enabled_sensor_ = sensor; } #endif #ifdef USE_SENSOR - void set_handshake_sensor(sensor::Sensor *sensor); + void set_handshake_sensor(sensor::Sensor *sensor) { this->handshake_sensor_ = sensor; } #endif #ifdef USE_TEXT_SENSOR - void set_address_sensor(text_sensor::TextSensor *sensor); + void set_address_sensor(text_sensor::TextSensor *sensor) { this->address_sensor_ = sensor; } #endif /// Block the setup step until peer is connected. - void disable_auto_proceed(); + void disable_auto_proceed() { this->proceed_allowed_ = false; } /// Enable the WireGuard component. void enable(); @@ -93,7 +93,7 @@ class Wireguard final : public PollingComponent { void publish_enabled_state(); /// Return if the WireGuard component is or is not enabled. - bool is_enabled(); + bool is_enabled() { return this->enabled_; } bool is_peer_up() const; time_t get_latest_handshake() const; From b83ce91528c4c99043e0dee42b0e28ce24375c0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:06:03 -0500 Subject: [PATCH 073/155] [display] Inline the trivial DisplayPage setters and page navigation helpers (#18639) --- esphome/components/display/display.cpp | 6 ------ esphome/components/display/display.h | 9 ++++++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 115adf503a..c2d45dbb60 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -685,9 +685,6 @@ void Display::show_page(DisplayPage *page) { } } -void Display::show_next_page() { this->page_->show_next(); } -void Display::show_prev_page() { this->page_->show_prev(); } - void Display::do_update_() { if (this->auto_clear_enabled_) { this->clear(); @@ -892,9 +889,6 @@ void DisplayPage::show_prev() { this->prev_->show(); } -void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; } -void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } -void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } const display_writer_t &DisplayPage::get_writer() const { return this->writer_; } const LogString *text_align_to_string(TextAlign textalign) { diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 3a136937f6..a9ffda422d 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -802,9 +802,9 @@ class DisplayPage final { void show(); void show_next(); void show_prev(); - void set_parent(Display *parent); - void set_prev(DisplayPage *prev); - void set_next(DisplayPage *next); + void set_parent(Display *parent) { this->parent_ = parent; } + void set_prev(DisplayPage *prev) { this->prev_ = prev; } + void set_next(DisplayPage *next) { this->next_ = next; } const display_writer_t &get_writer() const; protected: @@ -814,6 +814,9 @@ class DisplayPage final { DisplayPage *next_{nullptr}; }; +inline void Display::show_next_page() { this->page_->show_next(); } +inline void Display::show_prev_page() { this->page_->show_prev(); } + template class DisplayPageShowAction final : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) From a63c3bc0c7f9eab2b08def454b89223480fcf9ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:06:31 -0500 Subject: [PATCH 074/155] [valve] Inline the trivial Valve and ValveCall accessors (#18641) --- esphome/components/valve/valve.cpp | 7 ------- esphome/components/valve/valve.h | 8 ++++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 8fccd1e6d6..d8fb18b1b7 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -120,10 +120,6 @@ ValveCall &ValveCall::set_stop(bool stop) { this->stop_ = stop; return *this; } -bool ValveCall::get_stop() const { return this->stop_; } - -ValveCall Valve::make_call() { return {this}; } - void Valve::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); @@ -162,9 +158,6 @@ optional Valve::restore_state_() { return recovered; } -bool Valve::is_fully_open() const { return this->position == VALVE_OPEN; } -bool Valve::is_fully_closed() const { return this->position == VALVE_CLOSED; } - ValveCall ValveRestoreState::to_call(Valve *valve) { auto call = valve->make_call(); call.set_position(this->position); diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index c6cdf07096..183680e5e4 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -47,7 +47,7 @@ class ValveCall { void perform(); const optional &get_position() const; - bool get_stop() const; + bool get_stop() const { return this->stop_; } const optional &get_toggle() const; protected: @@ -114,7 +114,7 @@ class Valve : public EntityBase { float position; /// Construct a new valve call used to control the valve. - ValveCall make_call(); + ValveCall make_call() { return {this}; } template void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward(f)); } @@ -130,9 +130,9 @@ class Valve : public EntityBase { virtual ValveTraits get_traits() = 0; /// Helper method to check if the valve is fully open. Equivalent to comparing .position against 1.0 - bool is_fully_open() const; + bool is_fully_open() const { return this->position == VALVE_OPEN; } /// Helper method to check if the valve is fully closed. Equivalent to comparing .position against 0.0 - bool is_fully_closed() const; + bool is_fully_closed() const { return this->position == VALVE_CLOSED; } protected: friend ValveCall; From 435d5226838d8f2818d637f1a284f4f85c214295 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:06:47 -0500 Subject: [PATCH 075/155] [text] Inline the trivial Text publish_state forwarding overloads (#18642) --- esphome/components/text/text.cpp | 4 ---- esphome/components/text/text.h | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 032ea468e6..a1df6286c7 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -8,10 +8,6 @@ namespace esphome::text { static const char *const TAG = "text"; -void Text::publish_state(const std::string &state) { this->publish_state(state.data(), state.size()); } - -void Text::publish_state(const char *state) { this->publish_state(state, strlen(state)); } - void Text::publish_state(const char *state, size_t len) { this->set_has_state(true); // Only assign if changed to avoid heap allocation diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index eb6a68f998..54afb8db8f 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -23,8 +23,8 @@ class Text : public EntityBase { std::string state; TextTraits traits; - void publish_state(const std::string &state); - void publish_state(const char *state); + void publish_state(const std::string &state) { this->publish_state(state.data(), state.size()); } + void publish_state(const char *state) { this->publish_state(state, strlen(state)); } void publish_state(const char *state, size_t len); /// Instantiate a TextCall object to modify this text component's state. From 01ad424d12bc6c376e695084c078e9cb8d5c54fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:07:03 -0500 Subject: [PATCH 076/155] [datetime] Inline the trivial make_call helpers (#18643) --- esphome/components/datetime/date_entity.cpp | 2 -- esphome/components/datetime/date_entity.h | 2 ++ esphome/components/datetime/datetime_entity.cpp | 2 -- esphome/components/datetime/datetime_entity.h | 2 ++ esphome/components/datetime/time_entity.cpp | 2 -- esphome/components/datetime/time_entity.h | 2 ++ 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index 997aec3f69..b99b89259f 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -37,8 +37,6 @@ void DateEntity::publish_state() { #endif } -DateCall DateEntity::make_call() { return DateCall(this); } - void DateCall::validate_() { if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) { ESP_LOGE(TAG, "Year must be between 1970 and 3000"); diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index 9b86c12228..93ce1411f8 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -96,6 +96,8 @@ class DateCall { optional day_; }; +inline DateCall DateEntity::make_call() { return DateCall(this); } + template class DateSetAction final : public Action, public Parented { public: TEMPLATABLE_VALUE(ESPTime, date) diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index a8e00d6eb3..8f180fd081 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -53,8 +53,6 @@ void DateTimeEntity::publish_state() { #endif } -DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } - ESPTime DateTimeEntity::state_as_esptime() const { ESPTime obj; obj.year = this->year_; diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 159e4ccc6f..fec620b5ba 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -121,6 +121,8 @@ class DateTimeCall { optional second_; }; +inline DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } + template class DateTimeSetAction final : public Action, public Parented { public: TEMPLATABLE_VALUE(ESPTime, datetime) diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 1cc9eaf2fb..da4c9eb31e 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -33,8 +33,6 @@ void TimeEntity::publish_state() { #endif } -TimeCall TimeEntity::make_call() { return TimeCall(this); } - void TimeCall::validate_() { if (this->hour_.has_value() && this->hour_ > 23) { ESP_LOGE(TAG, "Hour must be between 0 and 23"); diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index 643f4bd176..736e26f4a7 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -98,6 +98,8 @@ class TimeCall { optional second_; }; +inline TimeCall TimeEntity::make_call() { return TimeCall(this); } + template class TimeSetAction final : public Action, public Parented { public: TEMPLATABLE_VALUE(ESPTime, time) From f24b731f9510815fe164e975b7e4a4f4605cd45e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:07:14 -0500 Subject: [PATCH 077/155] [infrared] Inline the trivial make_call helper (#18644) --- esphome/components/infrared/infrared.cpp | 2 -- esphome/components/infrared/infrared.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 9b97995a96..5a909738c6 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -75,8 +75,6 @@ void Infrared::dump_config() { YESNO(this->traits_.get_supports_receiver())); } -InfraredCall Infrared::make_call() { return InfraredCall(this); } - void Infrared::control(const InfraredCall &call) { if (this->transmitter_ == nullptr) { ESP_LOGW(TAG, "No transmitter configured"); diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index 6d91c97cce..b6863e37ce 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -134,7 +134,7 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote const InfraredTraits &get_traits() const { return this->traits_; } /// Create a call object for transmitting - InfraredCall make_call(); + InfraredCall make_call() { return InfraredCall(this); } /// Get capability flags for this infrared instance uint32_t get_capability_flags() const; From f0651e5c9b2ae24c33256819dd78eedce73c45a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:07:34 -0500 Subject: [PATCH 078/155] [radio_frequency] Inline the trivial make_call helper (#18645) --- esphome/components/radio_frequency/radio_frequency.cpp | 2 -- esphome/components/radio_frequency/radio_frequency.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/radio_frequency/radio_frequency.cpp b/esphome/components/radio_frequency/radio_frequency.cpp index 3e0a905737..61e7feb9af 100644 --- a/esphome/components/radio_frequency/radio_frequency.cpp +++ b/esphome/components/radio_frequency/radio_frequency.cpp @@ -81,8 +81,6 @@ void RadioFrequency::dump_config() { } } -RadioFrequencyCall RadioFrequency::make_call() { return RadioFrequencyCall(this); } - uint32_t RadioFrequency::get_capability_flags() const { uint32_t flags = 0; if (this->traits_.get_supports_transmitter()) diff --git a/esphome/components/radio_frequency/radio_frequency.h b/esphome/components/radio_frequency/radio_frequency.h index 7dfd2dd77e..8782c255f0 100644 --- a/esphome/components/radio_frequency/radio_frequency.h +++ b/esphome/components/radio_frequency/radio_frequency.h @@ -157,7 +157,7 @@ class RadioFrequency : public Component, public EntityBase, public remote_base:: const RadioFrequencyTraits &get_traits() const { return this->traits_; } /// Create a call object for transmitting - RadioFrequencyCall make_call(); + RadioFrequencyCall make_call() { return RadioFrequencyCall(this); } /// Get capability flags for this radio frequency instance uint32_t get_capability_flags() const; From cd536817876caff90c750fa9e17b2bdbb181034f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:51:39 -0500 Subject: [PATCH 079/155] [wifi] Inline the remaining trivial WiFiAP and WiFiComponent accessors (#18617) --- esphome/components/wifi/wifi_component.cpp | 38 ------------------ esphome/components/wifi/wifi_component.h | 46 +++++++++++----------- 2 files changed, 24 insertions(+), 60 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 5ed5fc9094..b8a31f97a3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -618,8 +618,6 @@ static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) { } #endif -float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } - void WiFiComponent::setup() { this->wifi_pre_setup_(); @@ -931,10 +929,6 @@ void WiFiComponent::loop() { WiFiComponent::WiFiComponent() { global_wifi_component = this; } -#ifdef USE_WIFI_11KV_SUPPORT -void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } -void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } -#endif network::IPAddresses WiFiComponent::get_ip_addresses() { if (this->has_sta()) return this->wifi_sta_ip_addresses(); @@ -1327,8 +1321,6 @@ void WiFiComponent::disable() { this->wifi_mode_(false, false); } -bool WiFiComponent::is_disabled() { return this->state_ == WIFI_COMPONENT_STATE_DISABLED; } - void WiFiComponent::start_scanning() { this->action_started_ = millis(); ESP_LOGD(TAG, "Starting scan"); @@ -2196,7 +2188,6 @@ void WiFiComponent::retry_connect() { } } -void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) @@ -2204,8 +2195,6 @@ void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { #endif } -void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; } - bool WiFiComponent::is_captive_portal_active_() { #ifdef USE_CAPTIVE_PORTAL return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active(); @@ -2324,33 +2313,6 @@ void WiFiComponent::save_fast_connect_settings_(const bssid_t &bssid, uint8_t ch } #endif -void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } -void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); } -void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; } -void WiFiAP::clear_bssid() { this->bssid_ = {}; } -void WiFiAP::set_password(const std::string &password) { - this->password_ = CompactString(password.c_str(), password.size()); -} -void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); } -#ifdef USE_WIFI_WPA2_EAP -void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } -#endif -void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; } -void WiFiAP::clear_channel() { this->channel_ = 0; } -#ifdef USE_WIFI_MANUAL_IP -void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } -#endif -void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } -const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; } -bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; } -#ifdef USE_WIFI_WPA2_EAP -const optional &WiFiAP::get_eap() const { return this->eap_; } -#endif -#ifdef USE_WIFI_MANUAL_IP -const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } -#endif -bool WiFiAP::get_hidden() const { return this->hidden_; } - WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden) : bssid_(bssid), diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index c54fbc004b..07d4ff23c6 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #ifdef USE_LIBRETINY @@ -261,38 +262,38 @@ class WiFiAP { friend class WiFiScanResult; public: - void set_ssid(const std::string &ssid); - void set_ssid(const char *ssid); + void set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } + void set_ssid(const char *ssid) { this->set_ssid(StringRef(ssid)); } void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } - void set_bssid(const bssid_t &bssid); - void clear_bssid(); - void set_password(const std::string &password); - void set_password(const char *password); + void set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; } + void clear_bssid() { this->bssid_ = {}; } + void set_password(const std::string &password) { this->password_ = CompactString(password.c_str(), password.size()); } + void set_password(const char *password) { this->set_password(StringRef(password)); } void set_password(StringRef password) { this->password_ = CompactString(password.c_str(), password.size()); } #ifdef USE_WIFI_WPA2_EAP - void set_eap(optional eap_auth); + void set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif // USE_WIFI_WPA2_EAP - void set_channel(uint8_t channel); - void clear_channel(); + void set_channel(uint8_t channel) { this->channel_ = channel; } + void clear_channel() { this->channel_ = 0; } void set_priority(int8_t priority) { priority_ = priority; } #ifdef USE_WIFI_MANUAL_IP - void set_manual_ip(optional manual_ip); + void set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } #endif - void set_hidden(bool hidden); + void set_hidden(bool hidden) { this->hidden_ = hidden; } StringRef get_ssid() const { return this->ssid_.ref(); } StringRef get_password() const { return this->password_.ref(); } - const bssid_t &get_bssid() const; - bool has_bssid() const; + const bssid_t &get_bssid() const { return this->bssid_; } + bool has_bssid() const { return this->bssid_ != bssid_t{}; } #ifdef USE_WIFI_WPA2_EAP - const optional &get_eap() const; + const optional &get_eap() const { return this->eap_; } #endif // USE_WIFI_WPA2_EAP uint8_t get_channel() const { return this->channel_; } bool has_channel() const { return this->channel_ != 0; } int8_t get_priority() const { return priority_; } #ifdef USE_WIFI_MANUAL_IP - const optional &get_manual_ip() const; + const optional &get_manual_ip() const { return this->manual_ip_; } #endif - bool get_hidden() const; + bool get_hidden() const { return this->hidden_; } protected: CompactString ssid_; @@ -442,6 +443,7 @@ class WiFiComponent final : public Component { void set_sta(const WiFiAP &ap); // Returns a copy of the currently selected AP configuration WiFiAP get_sta() const; + // init_sta/add_sta kept out of line: inlining them into the generated setup() grows flash void init_sta(size_t count); void add_sta(const WiFiAP &ap); void clear_sta(); @@ -461,7 +463,7 @@ class WiFiComponent final : public Component { void enable(); void disable(); - bool is_disabled(); + bool is_disabled() { return this->state_ == WIFI_COMPONENT_STATE_DISABLED; } void start_scanning(); void check_scanning_finished(); void start_connecting(const WiFiAP &ap); @@ -472,7 +474,7 @@ class WiFiComponent final : public Component { void retry_connect(); - void set_reboot_timeout(uint32_t reboot_timeout); + void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool is_connected() const { return this->connected_; } @@ -492,7 +494,7 @@ class WiFiComponent final : public Component { void set_phy_mode(WiFi8266PhyMode phy_mode) { this->phy_mode_ = phy_mode; } #endif - void set_passive_scan(bool passive); + void set_passive_scan(bool passive) { this->passive_scan_ = passive; } void save_wifi_sta(const std::string &ssid, const std::string &password); void save_wifi_sta(const char *ssid, const char *password); @@ -506,7 +508,7 @@ class WiFiComponent final : public Component { void dump_config() override; void restart_adapter(); /// WIFI setup_priority. - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::WIFI; } /// Reconnect WiFi if required. void loop() override; @@ -515,8 +517,8 @@ class WiFiComponent final : public Component { bool is_ap_active() const { return this->ap_started_; } #ifdef USE_WIFI_11KV_SUPPORT - void set_btm(bool btm); - void set_rrm(bool rrm); + void set_btm(bool btm) { this->btm_ = btm; } + void set_rrm(bool rrm) { this->rrm_ = rrm; } #endif network::IPAddress get_dns_address(int num); From 5b3a6c05bf40a5776da603d422495e345363da3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:59:47 -0500 Subject: [PATCH 080/155] [core] Remove deprecated esp_log_vprintf_ flash-string overload (#18377) --- esphome/core/log.cpp | 10 ---------- esphome/core/log.h | 5 ----- 2 files changed, 15 deletions(-) diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 0da457adec..9fcddfeff6 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -60,16 +60,6 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *form #endif } -#ifdef USE_STORE_LOG_STR_IN_FLASH -// Remove before 2026.9.0 -void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { -#ifdef USE_LOGGER - ESPHOME_DEBUG_ASSERT(logger::global_logger != nullptr); - logger::global_logger->log_vprintf_(static_cast(level), tag, line, format, args); -#endif -} -#endif - #ifdef USE_ESP32 int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER diff --git a/esphome/core/log.h b/esphome/core/log.h index 72e06cabac..272e516808 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -68,11 +68,6 @@ void esp_log_printf_(int level, const char *tag, int line, const char *format, . void esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...); #endif void esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT -#ifdef USE_STORE_LOG_STR_IN_FLASH -// Remove before 2026.9.0 -__attribute__((deprecated("Use esp_log_printf_() instead. Removed in 2026.9.0."))) void esp_log_vprintf_( - int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); -#endif #if defined(USE_ESP32) int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #endif From ab45ab316a0190cf898e436a345e2a20a5f15c42 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 22:00:02 -0500 Subject: [PATCH 081/155] [core] Remove deprecated entity_base getters (#18375) --- esphome/core/entity_base.cpp | 40 ------------------------------- esphome/core/entity_base.h | 46 ------------------------------------ 2 files changed, 86 deletions(-) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index fc6ac503b5..21a5fc3706 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -80,24 +80,6 @@ const char *EntityBase::get_device_class_to([[maybe_unused]] std::spandevice_class_idx_)); -#else - return StringRef(entity_device_class_lookup(0)); -#endif -} -std::string EntityBase::get_device_class() const { -#ifdef USE_ENTITY_DEVICE_CLASS - return std::string(entity_device_class_lookup(this->device_class_idx_)); -#else - return std::string(entity_device_class_lookup(0)); -#endif -} -#endif // !USE_ESP8266 - // Entity unit of measurement (from index) StringRef EntityBase::get_unit_of_measurement_ref() const { #ifdef USE_ENTITY_UNIT_OF_MEASUREMENT @@ -106,10 +88,6 @@ StringRef EntityBase::get_unit_of_measurement_ref() const { return StringRef(entity_uom_lookup(0)); #endif } -std::string EntityBase::get_unit_of_measurement() const { - return std::string(this->get_unit_of_measurement_ref().c_str()); -} - // Entity icon — buffer-based API for PROGMEM safety on ESP8266 const char *EntityBase::get_icon_to([[maybe_unused]] std::span buffer) const { #ifdef USE_ENTITY_ICON @@ -129,24 +107,6 @@ const char *EntityBase::get_icon_to([[maybe_unused]] std::spanicon_idx_)); -#else - return StringRef(entity_icon_lookup(0)); -#endif -} -std::string EntityBase::get_icon() const { -#ifdef USE_ENTITY_ICON - return std::string(entity_icon_lookup(this->icon_idx_)); -#else - return std::string(entity_icon_lookup(0)); -#endif -} -#endif // !USE_ESP8266 - // Calculate Object ID Hash directly from name using snake_case + sanitize void EntityBase::calc_object_id_() { this->object_id_hash_ = fnv1_hash_object_id(this->name_.c_str(), this->name_.size()); diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 5f2e173d8d..f38e30bf52 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -109,60 +109,14 @@ class EntityBase { // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. const char *get_device_class_to(std::span buffer) const; -#ifdef USE_ESP8266 - // On ESP8266, rodata is RAM. Device classes are in PROGMEM and cannot be accessed - // directly as const char*. Use get_device_class_to() with a stack buffer instead. - template StringRef get_device_class_ref() const { - static_assert(sizeof(T) == 0, "get_device_class_ref() unavailable on ESP8266 (rodata is RAM). " - "Use get_device_class_to() with a stack buffer."); - return StringRef(""); - } - template std::string get_device_class() const { - static_assert(sizeof(T) == 0, "get_device_class() unavailable on ESP8266 (rodata is RAM). " - "Use get_device_class_to() with a stack buffer."); - return ""; - } -#else - // Deprecated: use get_device_class_to() instead. Device classes are in PROGMEM. - ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") - StringRef get_device_class_ref() const; - ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") - std::string get_device_class() const; -#endif // Get unit of measurement as StringRef (from packed index) StringRef get_unit_of_measurement_ref() const; - /// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref()) - ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be " - "removed in ESPHome 2026.9.0", - "2026.3.0") - std::string get_unit_of_measurement() const; // Get this entity's icon into a stack buffer. // On ESP32: returns pointer to PROGMEM string directly (buffer unused). // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. const char *get_icon_to(std::span buffer) const; -#ifdef USE_ESP8266 - // On ESP8266, rodata is RAM. Icons are in PROGMEM and cannot be accessed - // directly as const char*. Use get_icon_to() with a stack buffer instead. - template StringRef get_icon_ref() const { - static_assert(sizeof(T) == 0, - "get_icon_ref() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); - return StringRef(""); - } - template std::string get_icon() const { - static_assert(sizeof(T) == 0, - "get_icon() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); - return ""; - } -#else - // Deprecated: use get_icon_to() instead. Icons are in PROGMEM. - ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") - StringRef get_icon_ref() const; - ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") - std::string get_icon() const; -#endif - #ifdef USE_DEVICES // Get this entity's device id uint32_t get_device_id() const { From b115813fbe2880a3e7ffbc2e4725e208c589d97b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 22:00:17 -0500 Subject: [PATCH 082/155] [esp32] Report abort and task watchdog panics correctly in crash handler (#18575) --- esphome/components/esp32/crash_handler.cpp | 61 +++++++++++++++++++--- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index b61dad7386..6f65243aaa 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -124,6 +124,15 @@ static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *ou // Version is uint32_t because it would be padded to 4 bytes anyway before the next // uint32_t field, so we use the full width rather than wasting 3 bytes of padding. static constexpr uint32_t CRASH_DATA_VERSION = 4; +#if CONFIG_IDF_TARGET_ARCH_XTENSA +// EXCCAUSE is a 6-bit register; larger recorded values mean the frame's +// cause/vaddr slots were never written (not a real exception frame). +static constexpr uint32_t XTENSA_EXCCAUSE_COUNT = XCHAL_EXCCAUSE_NUM; +#elif CONFIG_IDF_TARGET_ARCH_RISCV +// Synchronous mcause exception codes are small and have no interrupt bit; +// anything else in a non-pseudo record is a stale slot. +static constexpr uint32_t RISCV_EXCEPTION_CAUSE_COUNT = 32; +#endif struct RawCrashData { uint32_t version; uint32_t magic; @@ -198,10 +207,28 @@ void crash_handler_clear() { s_raw_crash_data.magic = 0; } +// Whether the cause slot was written by a real exception frame. +static bool cause_slot_was_written() { +#if CONFIG_IDF_TARGET_ARCH_XTENSA + return s_raw_crash_data.cause < XTENSA_EXCCAUSE_COUNT; +#else + return s_raw_crash_data.cause < RISCV_EXCEPTION_CAUSE_COUNT; +#endif +} + // Look up the exception cause as a human-readable string. // Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays // not exposed via any public API. static const char *get_exception_reason() { + uint8_t exception = s_raw_crash_data.exception; + if (exception == PANIC_EXCEPTION_ABORT || exception == PANIC_EXCEPTION_TWDT) { + // Abort-class panics carry no cause register + return nullptr; + } + if (!cause_slot_was_written()) { + // Garbage from old-build or corrupt records; report just the type + return nullptr; + } #if CONFIG_IDF_TARGET_ARCH_XTENSA if (s_raw_crash_data.pseudo_excause) { // SoC-level panic: watchdog, cache error, etc. @@ -354,10 +381,11 @@ static const char *const FAULT_ADDR_REG = "MTVAL"; static const char *const FAULT_ADDR_REG_LOWER = "mtval"; #endif -// Whether the fault address is meaningful — real CPU faults only, not -// aborts/watchdogs or SoC-level pseudo exceptions. +// Whether the fault address is meaningful: real CPU faults with a validly +// written frame only. static bool has_fault_addr() { - return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause; + return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause && + cause_slot_was_written(); } // The record was captured by a different firmware build (it survives soft @@ -458,6 +486,10 @@ void crash_handler_log() { // into NOINIT memory before the normal panic handler runs. // extern "C" { +// Set by IDF's task watchdog (task_wdt.c, no header) before it simulates an +// abort; weak so builds without the task watchdog still link. +extern bool g_twdt_isr __attribute__((weak)); + // NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) // Names are mandated by the --wrap linker mechanism extern void __real_esp_panic_handler(panic_info_t *info); @@ -470,6 +502,14 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { s_raw_crash_data.exception = (uint8_t) info->exception; s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0; s_raw_crash_data.crashed_core = (uint8_t) info->core; + if (g_panic_abort) { + // IDF reclassifies to ABORT only inside esp_panic_handler(), after this + // wrapper captured info->exception; correct it here. TWDT is our own + // distinction (IDF never assigns PANIC_EXCEPTION_TWDT). The abort text is + // not stored; the symbolized backtrace already identifies the site. + bool is_twdt = &g_twdt_isr != nullptr && g_twdt_isr; + s_raw_crash_data.exception = (uint8_t) (is_twdt ? PANIC_EXCEPTION_TWDT : PANIC_EXCEPTION_ABORT); + } // Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot s_raw_crash_data.cause = 0; s_raw_crash_data.fault_addr = 0; @@ -487,8 +527,12 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { // Xtensa: walk the backtrace using the public API if (info->frame != nullptr) { auto *xt_frame = (XtExcFrame *) info->frame; - s_raw_crash_data.cause = xt_frame->exccause; - s_raw_crash_data.fault_addr = xt_frame->excvaddr; + if (!g_panic_abort) { + // Abort-class frames carry no useful cause/vaddr: TWDT task snapshots + // never wrote them and abort() traps describe only the synthetic trap. + s_raw_crash_data.cause = xt_frame->exccause; + s_raw_crash_data.fault_addr = xt_frame->excvaddr; + } s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE); } @@ -510,8 +554,11 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { // RISC-V: capture MEPC + RA, then scan stack for code addresses if (info->frame != nullptr) { auto *rv_frame = (RvExcFrame *) info->frame; - s_raw_crash_data.cause = rv_frame->mcause; - s_raw_crash_data.fault_addr = rv_frame->mtval; + if (!g_panic_abort) { + // See the Xtensa branch: abort-class frames carry no valid cause/vaddr. + s_raw_crash_data.cause = rv_frame->mcause; + s_raw_crash_data.fault_addr = rv_frame->mtval; + } s_raw_crash_data.backtrace_count = capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count); } From f3cdefce210b9b418da3487ff57c1421bb779137 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 22:00:38 -0500 Subject: [PATCH 083/155] [wifi] Remove deprecated wifi_ssid() (#18378) --- esphome/components/wifi/wifi_component.h | 3 --- esphome/components/wifi/wifi_component_esp8266.cpp | 10 ---------- esphome/components/wifi/wifi_component_esp_idf.cpp | 12 ------------ esphome/components/wifi/wifi_component_libretiny.cpp | 1 - esphome/components/wifi/wifi_component_pico_w.cpp | 1 - 5 files changed, 27 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 07d4ff23c6..ada7be4ba4 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -552,9 +552,6 @@ class WiFiComponent final : public Component { void set_sta_priority(bssid_t bssid, int8_t priority); network::IPAddresses wifi_sta_ip_addresses(); - // Remove before 2026.9.0 - ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0") - std::string wifi_ssid(); /// Write SSID to buffer without heap allocation. /// Returns pointer to buffer, or empty string if not connected. const char *wifi_ssid_to(std::span buffer); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index acaa94b13c..005d655d88 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -944,16 +944,6 @@ bssid_t WiFiComponent::wifi_bssid() { } return bssid; } -std::string WiFiComponent::wifi_ssid() { - struct station_config conf {}; - if (!wifi_station_get_config(&conf)) { - return ""; - } - // conf.ssid is uint8[32], not null-terminated if full - auto *ssid_s = reinterpret_cast(conf.ssid); - size_t len = strnlen(ssid_s, sizeof(conf.ssid)); - return {ssid_s, len}; -} const char *WiFiComponent::wifi_ssid_to(std::span buffer) { struct station_config conf {}; if (!wifi_station_get_config(&conf)) { diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 24cb060edb..32d46887b6 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1237,18 +1237,6 @@ bssid_t WiFiComponent::wifi_bssid() { std::copy(info.bssid, info.bssid + 6, bssid.begin()); return bssid; } -std::string WiFiComponent::wifi_ssid() { - wifi_ap_record_t info{}; - esp_err_t err = esp_wifi_sta_get_ap_info(&info); - if (err != ESP_OK) { - // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) - ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); - return ""; - } - auto *ssid_s = reinterpret_cast(info.ssid); - size_t len = strnlen(ssid_s, sizeof(info.ssid)); - return {ssid_s, len}; -} const char *WiFiComponent::wifi_ssid_to(std::span buffer) { wifi_ap_record_t info{}; esp_err_t err = esp_wifi_sta_get_ap_info(&info); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 66c397a8ad..e3c08416e8 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -762,7 +762,6 @@ bssid_t WiFiComponent::wifi_bssid() { } return bssid; } -std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } const char *WiFiComponent::wifi_ssid_to(std::span buffer) { #ifdef USE_BK72XX LinkStatusTypeDef link_status{}; diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 69af9e9a4e..325bcf2652 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -265,7 +265,6 @@ bssid_t WiFiComponent::wifi_bssid() { bssid[i] = raw_bssid[i]; return bssid; } -std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } const char *WiFiComponent::wifi_ssid_to(std::span buffer) { // TODO: Find direct CYW43 API to avoid Arduino String allocation String ssid = WiFi.SSID(); From 8899713ef97229f881ab8694802ef03a9290c65a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 22:00:53 -0500 Subject: [PATCH 084/155] [core] Remove deprecated gamma_correct and gamma_uncorrect (#18376) --- esphome/core/helpers.cpp | 17 ----------------- esphome/core/helpers.h | 9 --------- 2 files changed, 26 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index a276020be4..ded8051df8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -723,23 +723,6 @@ bool base64_decode_int32_vector(const std::string &base64, std::vector // Colors -float gamma_correct(float value, float gamma) { - if (value <= 0.0f) - return 0.0f; - if (gamma <= 0.0f) - return value; - - return powf(value, gamma); // NOLINT - deprecated, removal 2026.9.0 -} -float gamma_uncorrect(float value, float gamma) { - if (value <= 0.0f) - return 0.0f; - if (gamma <= 0.0f) - return value; - - return powf(value, 1 / gamma); // NOLINT - deprecated, removal 2026.9.0 -} - void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) { float max_color_value = std::max({red, green, blue}); float min_color_value = std::min({red, green, blue}); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 5a9c120b84..b13d92ccce 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1646,15 +1646,6 @@ bool base64_decode_int32_vector(const std::string &base64, std::vector /// @name Colors ///@{ -/// Applies gamma correction of \p gamma to \p value. -// Remove before 2026.9.0 -ESPDEPRECATED("Use LightState::gamma_correct_lut() instead. Removed in 2026.9.0.", "2026.3.0") -float gamma_correct(float value, float gamma); -/// Reverts gamma correction of \p gamma to \p value. -// Remove before 2026.9.0 -ESPDEPRECATED("Use LightState::gamma_uncorrect_lut() instead. Removed in 2026.9.0.", "2026.3.0") -float gamma_uncorrect(float value, float gamma); - /// Convert \p red, \p green and \p blue (all 0-1) values to \p hue (0-360), \p saturation (0-1) and \p value (0-1). void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value); /// Convert \p hue (0-360), \p saturation (0-1) and \p value (0-1) to \p red, \p green and \p blue (all 0-1). From 160d8b8f0ccdb5362daf2b144131fe5e35355991 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 22:02:05 -0500 Subject: [PATCH 085/155] [web_server_idf] Remove deprecated AsyncWebServerRequest::url() (#18382) --- esphome/components/web_server_idf/web_server_idf.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index baa55898bb..6469b4c564 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -117,12 +117,6 @@ class AsyncWebServerRequest { /// Write URL (without query string) to buffer, returns StringRef pointing to buffer. /// URL is decoded (e.g., %20 -> space). StringRef url_to(std::span buffer) const; - // Remove before 2026.9.0 - ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0") - std::string url() const { - char buffer[URL_BUF_SIZE]; - return std::string(this->url_to(buffer)); - } // NOLINTNEXTLINE(readability-identifier-naming) size_t contentLength() const { return this->req_->content_len; } From b2440cb655f794ec7a2d3e83f87fea0d99fc8ed8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 22:02:22 -0500 Subject: [PATCH 086/155] [modbus] Remove deprecated waiting_for_response() (#18381) --- esphome/components/modbus/modbus.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index bb303c43a8..e5cbba88ec 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -618,9 +618,6 @@ class ModbusClientDevice { inline void clear_tx_queue_for_address() { this->parent_->clear_tx_queue_for_address(this->address_); } inline void clear_tx_queue_for_device() { this->parent_->clear_tx_queue_for_device(this); } - // If more than one device is connected block sending a new command before a response is received - ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0") - bool waiting_for_response() { return !this->ready_for_immediate_send(); } bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); } protected: From 02da5c6484ecc478f80617ed956c9337dd42f653 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 22:02:41 -0500 Subject: [PATCH 087/155] [ethernet] Remove deprecated get_eth_mac_address_pretty() (#18379) --- esphome/components/ethernet/ethernet_component.h | 3 --- esphome/components/ethernet/ethernet_component_esp32.cpp | 5 ----- esphome/components/ethernet/ethernet_component_rp2.cpp | 5 ----- 3 files changed, 13 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 2da070b5e0..1482e7a828 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -159,9 +159,6 @@ class EthernetComponent final : public Component { const char *get_use_address() const { return this->use_address_; } void set_use_address(const char *use_address) { this->use_address_ = use_address; } void get_eth_mac_address_raw(uint8_t *mac); - // Remove before 2026.9.0 - ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") - std::string get_eth_mac_address_pretty(); const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); eth_speed_t get_link_speed(); diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index 4af2d5f93c..069478e70c 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -928,11 +928,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); } -std::string EthernetComponent::get_eth_mac_address_pretty() { - char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); -} - const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( std::span buf) { uint8_t mac[MAC_ADDRESS_SIZE]; diff --git a/esphome/components/ethernet/ethernet_component_rp2.cpp b/esphome/components/ethernet/ethernet_component_rp2.cpp index 7f4db4fab7..94d84cc891 100644 --- a/esphome/components/ethernet/ethernet_component_rp2.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2.cpp @@ -249,11 +249,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { } } -std::string EthernetComponent::get_eth_mac_address_pretty() { - char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); -} - const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( std::span buf) { uint8_t mac[MAC_ADDRESS_SIZE]; From d1f065671eab8fcfd25dae8106beedbcc0499452 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 22:21:04 -0500 Subject: [PATCH 088/155] [http_request] Abort OTA backend when update fails before first write (#18581) --- .../http_request/ota/ota_http_request.cpp | 17 +++++++++-------- .../http_request/ota/ota_http_request.h | 3 +-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 8893b96c65..7e7594c3c3 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -64,8 +64,9 @@ void OtaHttpRequestComponent::flash() { } } -void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container) { - if (this->update_started_) { +void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container, + bool abort_backend) { + if (abort_backend) { ESP_LOGV(TAG, "Aborting OTA backend"); backend->abort(); } @@ -106,7 +107,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() { auto error_code = backend->begin(container->content_length); if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGW(TAG, "backend->begin error: %d", error_code); - this->cleanup_(std::move(backend), container); + // Nothing to abort: begin() failed, so no OTA handle was opened + this->cleanup_(std::move(backend), container, /*abort_backend=*/false); return error_code; } @@ -140,7 +142,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { } else { ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error); } - this->cleanup_(std::move(backend), container); + this->cleanup_(std::move(backend), container, /*abort_backend=*/true); return OTA_CONNECTION_ERROR; } @@ -150,14 +152,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() { md5_receive.add(buf, bufsize_or_error); // write bytes to OTA backend - this->update_started_ = true; error_code = backend->write(buf, bufsize_or_error); if (error_code != ota::OTA_RESPONSE_OK) { // error code explanation available at // https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code, container->get_bytes_read() - bufsize_or_error, container->content_length); - this->cleanup_(std::move(backend), container); + this->cleanup_(std::move(backend), container, /*abort_backend=*/true); return error_code; } } @@ -181,7 +182,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { this->md5_computed_ = md5_receive_str; if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) { ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str()); - this->cleanup_(std::move(backend), container); + this->cleanup_(std::move(backend), container, /*abort_backend=*/true); return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH; } else { backend->set_update_md5(md5_receive_str); @@ -197,7 +198,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { error_code = backend->end(); if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code); - this->cleanup_(std::move(backend), container); + this->cleanup_(std::move(backend), container, /*abort_backend=*/true); return error_code; } diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index a706331d9a..9bb748f175 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -38,7 +38,7 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented< void flash(); protected: - void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container); + void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container, bool abort_backend); uint8_t do_ota_(); std::string get_url_with_auth_(const std::string &url); bool http_get_md5_(); @@ -51,7 +51,6 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented< std::string username_{}; std::string url_{}; int status_ = -1; - bool update_started_ = false; static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size }; From e697a40fda84887373d1ab3ba77bb3af64da8560 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 09:04:06 -0500 Subject: [PATCH 089/155] [core] Register the OTA component in dummy_main like its siblings (#18666) --- tests/dummy_main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 6fa0c08aa3..228d54ef01 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -29,6 +29,7 @@ void setup() { auto *ota = new esphome::ESPHomeOTAComponent(); // NOLINT ota->set_port(8266); + App.register_component_(ota); App.setup(); } From 33484108a982678208a9619d03e67d691df49f03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 09:04:24 -0500 Subject: [PATCH 090/155] [core] Replace a damaged existing file in write_file_if_changed (#18665) --- esphome/helpers.py | 13 ++++++++++++- tests/unit_tests/test_helpers.py | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 9b2a461ccd..7aa1a9a88c 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -552,7 +552,18 @@ def write_file_if_changed(path: Path, text: str) -> bool: """ src_content = None if path.is_file(): - src_content = read_file(path) + try: + src_content = path.read_text(encoding="utf-8") + except UnicodeDecodeError as err: + # Replace a damaged file rather than abort the regeneration that + # fixes it; an OSError may hide an intact file, so it still raises + _LOGGER.warning("Replacing damaged file %s: %s", path, err) + with suppress(OSError): + path.unlink(missing_ok=True) + except OSError as err: + from esphome.core import EsphomeError + + raise EsphomeError(f"Error reading file {path}: {err}") from err if src_content == text: return False write_file(path, text) diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 6e00e5b80f..eaa7d5a8dc 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -253,6 +253,31 @@ class Test_write_file_if_changed: assert dst.read_text() == text + def test_damaged_existing_file_is_replaced( + self, tmp_path: Path, caplog: pytest.LogCaptureFixture + ): + """A non-UTF-8 existing file is logged and overwritten.""" + dst = tmp_path / "generated.txt" + dst.write_bytes(b"\xff\xfe") + + assert helpers.write_file_if_changed(dst, "fresh content") is True + + assert dst.read_text(encoding="utf-8") == "fresh content" + assert "Replacing damaged file" in caplog.text + + def test_unreadable_existing_file_still_raises(self, tmp_path: Path): + """An OSError on the comparison read still raises EsphomeError.""" + dst = tmp_path / "generated.txt" + dst.write_text("intact") + + with ( + patch.object(Path, "read_text", side_effect=OSError("permission denied")), + pytest.raises(EsphomeError, match="Error reading file"), + ): + helpers.write_file_if_changed(dst, "fresh content") + + assert dst.exists() + def test_dst_does_not_exist(self, tmp_path: Path): text = "A files are unique.\n" dst = tmp_path / "file-a.txt" From e7574a574b6d5e2303df20edb73e64474ef23113 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 09:04:48 -0500 Subject: [PATCH 091/155] [ota] Restore lazy flash erase for ESP32 OTA with 64 KiB block erase (#18580) --- .../components/esphome/ota/ota_esphome.cpp | 2 +- esphome/components/ota/ota_backend.h | 13 +++ .../components/ota/ota_backend_esp_idf.cpp | 86 +++++++++++++++---- esphome/components/ota/ota_backend_esp_idf.h | 19 +++- .../components/ota/ota_bootloader_esp_idf.cpp | 11 ++- .../components/ota/ota_signature_esp_idf.cpp | 2 +- tests/components/ota/test_erase_ahead.cpp | 41 +++++++++ 7 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 tests/components/ota/test_erase_ahead.cpp diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 9cbb25b373..74f84b71fb 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -398,7 +398,7 @@ void ESPHomeOTAComponent::handle_data_() { this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif - // begin() may block for a few seconds while it locks flash. + // begin() returns quickly; flash sectors are erased incrementally during write(). error_code = this->backend_->begin(ota_size, ota_type); if (error_code != ota::OTA_RESPONSE_OK) goto error; // NOLINT(cppcoreguidelines-avoid-goto) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index aa93df60a5..1c24fc320a 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -66,6 +66,19 @@ enum OTAResponseTypes { */ bool version_is_older(const char *candidate, const char *reference); +// 64 KiB flash block; the erase granularity the ESP-IDF backend erases ahead with. +static constexpr size_t OTA_BLOCK_ERASE_SIZE = 64 * 1024; + +/** Target erased watermark for lazy block erase-ahead. + * + * Rounds the write end offset up to a block boundary, clamped to the partition + * size. Platform-independent so the arithmetic is host-testable. + */ +constexpr size_t next_erase_end(size_t write_end, size_t partition_size) { + const size_t rounded = (write_end + OTA_BLOCK_ERASE_SIZE - 1) & ~(OTA_BLOCK_ERASE_SIZE - 1); + return rounded < partition_size ? rounded : partition_size; +} + enum OTAState { OTA_COMPLETED = 0, OTA_STARTED, diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index eb23ad82dd..f33f37bbeb 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -7,7 +7,7 @@ #include "esphome/core/log.h" #include -#include +#include #include #ifdef USE_OTA_DOWNGRADE_PROTECTION #include @@ -60,27 +60,38 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION; } - // esp_ota_begin() erases the destination region, which blocks loopTask and - // scales with the erase size -- a fixed watchdog overruns on large OTA slots. - // An unknown size (0, e.g. web_server uploads) erases the whole partition, so - // budget against the bytes actually erased. ~10ms/KiB (conservative - // ~100 KiB/s erase) over a 15s floor; panic stays on so a stuck erase still - // resets rather than hanging forever. - size_t erase_size = image_size; - if (erase_size == 0 || erase_size > this->partition_->size) { - erase_size = this->partition_->size; + // Both lazy-erase paths below replace esp_ota_begin()'s blocking full erase. + // Size check replaces the one that erase performed (0 = unknown size, + // e.g. web_server uploads). + if (image_size != 0 && image_size > this->partition_->size) { + return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; } - const uint32_t erase_budget_ms = 15000 + (erase_size >> 10) * 10; - watchdog::WatchdogManager watchdog(erase_budget_ms); - esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); + this->written_ = 0; + esp_err_t err; +#ifdef USE_OTA_BLOCK_ERASE_AHEAD + this->erased_end_ = 0; + // Unlike esp_ota_begin(), esp_ota_resume() does not reject a running app in + // ESP_OTA_IMG_PENDING_VERIFY; that state is unreachable here because the app + // was marked valid at boot (esp32/hal.cpp) or just above under USE_OTA_ROLLBACK. + // erase_size 0 (!= OTA_WITH_SEQUENTIAL_WRITES) means no erase; erase_ahead_() handles it + err = esp_ota_resume(this->partition_, 0, 0, &this->update_handle_); +#if defined(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + // esp_ota_begin() does this on IDF 5.5+; esp_ota_resume() does not. Prevents + // booting a half-written slot after a crash mid-OTA. Not available on the + // 5.3.3/5.4.2 backports, whose esp_ota_begin() did not invalidate either. + if (err == ESP_OK) { + esp_ota_invalidate_inactive_ota_data_slot(); + } +#endif +#else + err = esp_ota_begin(this->partition_, OTA_WITH_SEQUENTIAL_WRITES, &this->update_handle_); +#endif if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ota_begin failed (err=0x%X)", err); + ESP_LOGE(TAG, "OTA begin failed (err=0x%X)", err); esp_ota_abort(this->update_handle_); this->update_handle_ = 0; - if (err == ESP_ERR_INVALID_SIZE) { - return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; - } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { + if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { return OTA_RESPONSE_ERROR_WRITING_FLASH; } else if (err == ESP_ERR_OTA_PARTITION_CONFLICT) { // This error appears with 1 factory and 1 ota partition @@ -120,6 +131,17 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { if (!this->is_app_or_bootloader_update_()) { return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; } +#endif + // Overflow can only happen on unknown-size uploads (web_server); known + // sizes were rejected in begin(). + if (this->written_ + len > this->partition_->size) { + return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; + } +#ifdef USE_OTA_BLOCK_ERASE_AHEAD + OTAResponseTypes erase_result = this->erase_ahead_(len); + if (erase_result != OTA_RESPONSE_OK) { + return erase_result; + } #endif esp_err_t err = esp_ota_write(this->update_handle_, data, len); this->md5_.add(data, len); @@ -127,14 +149,40 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err); if (err == ESP_ERR_OTA_VALIDATE_FAILED) { return OTA_RESPONSE_ERROR_MAGIC; + } else if (err == ESP_ERR_INVALID_SIZE) { + // Sequential-writes fallback: IDF's lazy erase reports overflow here + return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { return OTA_RESPONSE_ERROR_WRITING_FLASH; } return OTA_RESPONSE_ERROR_UNKNOWN; } + this->written_ += len; return OTA_RESPONSE_OK; } +#ifdef USE_OTA_BLOCK_ERASE_AHEAD +OTAResponseTypes IDFOTABackend::erase_ahead_(size_t len) { + const size_t end = this->written_ + len; + if (this->erased_end_ >= end) { + return OTA_RESPONSE_OK; + } + // Round up to a block boundary, clamped to the partition end; IDF splits the + // range into 64 KiB block erases where aligned, sector erases elsewhere. + const size_t erase_to = next_erase_end(end, this->partition_->size); + // A block erase is one uninterruptible flash op (typically ~150 ms, seconds + // on aged flash) and the transfer loop may not have fed the WDT for ~1s. + watchdog::WatchdogManager watchdog(15000); + esp_err_t err = esp_partition_erase_range(this->partition_, this->erased_end_, erase_to - this->erased_end_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_erase_range failed (err=0x%X)", err); + return err == ESP_ERR_INVALID_SIZE ? OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE : OTA_RESPONSE_ERROR_WRITING_FLASH; + } + this->erased_end_ = erase_to; + return OTA_RESPONSE_OK; +} +#endif + OTAResponseTypes IDFOTABackend::end() { if (this->md5_set_) { this->md5_.calculate(); @@ -226,6 +274,10 @@ void IDFOTABackend::abort() { // or not an update is in flight. esp_ota_abort(this->update_handle_); this->update_handle_ = 0; + this->written_ = 0; +#ifdef USE_OTA_BLOCK_ERASE_AHEAD + this->erased_end_ = 0; +#endif } } // namespace esphome::ota diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index 9dffd5429e..c991f896e8 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -5,8 +5,18 @@ #include "esphome/components/md5/md5.h" #include "esphome/core/defines.h" +#include #include +// esp_ota_resume() (IDF 5.4.2+, backported to 5.3.3) provides a no-erase OTA +// handle, letting write() block-erase 64 KiB ahead of the write cursor +// (~4x faster than the per-sector lazy erase of OTA_WITH_SEQUENTIAL_WRITES, +// used as fallback on older IDF). +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) || \ + (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 3) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0)) +#define USE_OTA_BLOCK_ERASE_AHEAD +#endif + namespace esphome::ota { #ifdef USE_OTA_PARTITIONS @@ -54,6 +64,9 @@ class IDFOTABackend final { #endif private: +#ifdef USE_OTA_BLOCK_ERASE_AHEAD + OTAResponseTypes erase_ahead_(size_t len); +#endif #ifdef USE_OTA_SIGNED_VERIFICATION_MULTI_KEY // Accept an image signed by any key the running app trusts (up to 3 blocks), // so rotation and backup keys work. Fails closed. Covers app and bootloader. @@ -62,7 +75,11 @@ class IDFOTABackend final { // Keep md5_ first since its digest_ is alignas(32) on DMA-SHA variants; md5_set_ stays last so buf_ packs tightly. md5::MD5Digest md5_{}; esp_ota_handle_t update_handle_{0}; - const esp_partition_t *partition_; + const esp_partition_t *partition_{nullptr}; + size_t written_{0}; // Bytes handed to esp_ota_write() +#ifdef USE_OTA_BLOCK_ERASE_AHEAD + size_t erased_end_{0}; // Erased up to this partition offset; must stay >= written_ +#endif char expected_bin_md5_[32]; bool md5_set_{false}; #ifdef USE_OTA_PARTITIONS diff --git a/esphome/components/ota/ota_bootloader_esp_idf.cpp b/esphome/components/ota/ota_bootloader_esp_idf.cpp index 57b5529350..5a83d92689 100644 --- a/esphome/components/ota/ota_bootloader_esp_idf.cpp +++ b/esphome/components/ota/ota_bootloader_esp_idf.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "ota_backend_esp_idf.h" +#include "esphome/components/watchdog/watchdog.h" #include "esphome/core/defines.h" #ifdef USE_OTA_PARTITIONS @@ -69,12 +70,20 @@ OTAResponseTypes IDFOTABackend::setup_bootloader_staging_() { return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY; } // Erase full size of the bootloader partition in the staging partition - // to avoid copying old data to the bootloader partition later + // to avoid copying old data to the bootloader partition later. Up to + // ESP_BOOTLOADER_SIZE of blocking erase; widen the WDT for its duration. + watchdog::WatchdogManager watchdog(15000); esp_err_t err = esp_partition_erase_range(this->partition_, 0, this->bootloader_part_->size); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_partition_erase_range failed (err=0x%X)", err); // No critical error, don't return } +#ifdef USE_OTA_BLOCK_ERASE_AHEAD + if (err == ESP_OK) { + // Skip re-erasing the pre-erased staging region in erase_ahead_() + this->erased_end_ = this->bootloader_part_->size; + } +#endif err = esp_ota_set_final_partition(this->update_handle_, this->bootloader_part_, false); if (err != ESP_OK) { esp_ota_abort(this->update_handle_); diff --git a/esphome/components/ota/ota_signature_esp_idf.cpp b/esphome/components/ota/ota_signature_esp_idf.cpp index 71dcc0eb83..501d6ac241 100644 --- a/esphome/components/ota/ota_signature_esp_idf.cpp +++ b/esphome/components/ota/ota_signature_esp_idf.cpp @@ -211,7 +211,7 @@ bool rsa_pss_verify(uint8_t *block, const uint8_t *digest) { bool IDFOTABackend::verify_signed_image_(const esp_partition_t *incoming) { // Verification re-hashes the full image (after esp_ota_end already did one // pass), which can approach the task WDT budget on a large app. Extend it for - // the duration, mirroring the erase budget in begin(). + // the duration, scaled to the image size over a 15 s floor. const uint32_t verify_budget_ms = 15000 + (incoming->size >> 10) * 10; watchdog::WatchdogManager watchdog(verify_budget_ms); diff --git a/tests/components/ota/test_erase_ahead.cpp b/tests/components/ota/test_erase_ahead.cpp new file mode 100644 index 0000000000..f84dd8a85d --- /dev/null +++ b/tests/components/ota/test_erase_ahead.cpp @@ -0,0 +1,41 @@ +// Pins the lazy erase-ahead arithmetic used by the ESP-IDF OTA backend: the +// erased watermark must always cover the write end, stay 64 KiB block-aligned +// until the clamp, and never exceed the partition. + +#include + +#include "esphome/components/ota/ota_backend.h" + +namespace esphome::ota::testing { + +static constexpr size_t BLOCK = 64 * 1024; +static constexpr size_t PART = 1835008; // 0x1C0000, a real app slot size + +TEST(NextEraseEnd, FirstWriteRoundsUpToOneBlock) { EXPECT_EQ(next_erase_end(1024, PART), BLOCK); } + +TEST(NextEraseEnd, ExactBlockBoundaryDoesNotOverErase) { EXPECT_EQ(next_erase_end(BLOCK, PART), BLOCK); } + +TEST(NextEraseEnd, StraddlingWriteCoversNextBlock) { EXPECT_EQ(next_erase_end(BLOCK + 1, PART), 2 * BLOCK); } + +TEST(NextEraseEnd, ClampsToPartitionEnd) { + // Partition sizes are sector multiples but not always block multiples + constexpr size_t part = 27 * BLOCK + 4096; + EXPECT_EQ(next_erase_end(27 * BLOCK + 1, part), part); + EXPECT_EQ(next_erase_end(part, part), part); +} + +// Bootloader staging seeds erased_end_ mid-block (e.g. 0x8000); the target for +// a write past that seed must still cover the write end. +TEST(NextEraseEnd, MidBlockSeedStillCovered) { EXPECT_EQ(next_erase_end(0x8000 + 1024, PART), BLOCK); } + +TEST(NextEraseEnd, SweepAlwaysCoversWriteEndWithinPartition) { + for (size_t end = 1; end <= PART; end += 4093) { + const size_t erased = next_erase_end(end, PART); + ASSERT_GE(erased, end); + ASSERT_LE(erased, PART); + // Block-aligned unless clamped at the partition end + ASSERT_TRUE(erased == PART || erased % BLOCK == 0); + } +} + +} // namespace esphome::ota::testing From cf31c08a5cc0b92c667966bf5abc55e9c0b502e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 09:05:04 -0500 Subject: [PATCH 092/155] [core] Skip copying entity automation and filter sources when unused (#18602) --- esphome/components/binary_sensor/__init__.py | 18 +++++++++ .../components/binary_sensor/automation.cpp | 12 ++++++ esphome/components/esp32/__init__.py | 8 ++++ esphome/components/esp32/gpio.cpp | 7 +++- esphome/components/esp32/gpio.py | 1 + esphome/components/ota/__init__.py | 38 +++++++++---------- esphome/components/sensor/__init__.py | 6 +++ esphome/components/text_sensor/__init__.py | 6 +++ esphome/components/uptime/sensor/__init__.py | 11 ++---- esphome/config_helpers.py | 25 ++++++++++++ esphome/core/defines.h | 3 ++ tests/components/binary_sensor/common.yaml | 16 ++++++++ tests/unit_tests/test_config_helpers.py | 24 ++++++++++++ 13 files changed, 145 insertions(+), 30 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 5800e0bd9e..1ab6f7103f 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -5,6 +5,7 @@ from esphome.automation import Condition, maybe_simple_id import esphome.codegen as cg from esphome.components import mqtt, web_server, zigbee from esphome.components.const import CONF_ON_STATE_CHANGE +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_DELAY, @@ -560,6 +561,11 @@ _CALLBACK_AUTOMATIONS = ( async def _build_binary_sensor_automations(var, config): await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) + if config.get(CONF_ON_CLICK) or config.get(CONF_ON_DOUBLE_CLICK): + cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER") + if config.get(CONF_ON_MULTI_CLICK): + cg.add_define("USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER") + for conf in config.get(CONF_ON_CLICK, []): trigger = cg.new_Pvariable( conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH] @@ -673,3 +679,15 @@ async def to_code(config): async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) + + +# automation.cpp only implements the click/double_click/multi_click triggers +FILTER_SOURCE_FILES = filter_source_files_from_defines( + { + "automation.cpp": ( + "USE_BINARY_SENSOR_CLICK_TRIGGER", + "USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER", + ), + "filter.cpp": "USE_BINARY_SENSOR_FILTER", + } +) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index b13e4a88dd..1a3c1f7536 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -1,8 +1,13 @@ +#include "esphome/core/defines.h" +#if defined(USE_BINARY_SENSOR_CLICK_TRIGGER) || defined(USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER) + #include "automation.h" #include "esphome/core/log.h" namespace esphome::binary_sensor { +#ifdef USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER + static const char *const TAG = "binary_sensor.automation"; // MultiClickTrigger timeout IDs. @@ -120,6 +125,9 @@ void MultiClickTriggerBase::trigger_() { this->trigger(); } +#endif // USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER + +#ifdef USE_BINARY_SENSOR_CLICK_TRIGGER bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) { if (max_length == 0) { return length >= min_length; @@ -127,4 +135,8 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) { return length >= min_length && length <= max_length; } } +#endif // USE_BINARY_SENSOR_CLICK_TRIGGER + } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_CLICK_TRIGGER || USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 501c2e525f..cde0cfd68b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -12,6 +12,7 @@ from typing import Any from esphome import yaml_util import esphome.codegen as cg from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_ADVANCED, @@ -3451,3 +3452,10 @@ def process_stacktrace(config, line, backtrace_state): _decode_pc(config, addr.group()) return backtrace_state + + +# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which +# are instantiated solely by the pin schema codegen (esp32_pin_to_code) +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"gpio.cpp": "USE_ESP32_INTERNAL_GPIO"} +) diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 4b53d3a172..74665f3126 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -1,4 +1,7 @@ -#ifdef USE_ESP32 +#include "esphome/core/defines.h" +// Also defines the core ISRInternalGPIOPin methods; those are only reachable +// via ESP32InternalGPIOPin::to_isr(), so the same define gates both safely. +#if defined(USE_ESP32) && defined(USE_ESP32_INTERNAL_GPIO) #include "gpio.h" #include "esphome/core/log.h" @@ -204,4 +207,4 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { } // namespace esphome -#endif // USE_ESP32 +#endif // USE_ESP32 && USE_ESP32_INTERNAL_GPIO diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 321dd3d498..98aac209ec 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -257,6 +257,7 @@ ESP32_PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA) async def esp32_pin_to_code(config): + cg.add_define("USE_ESP32_INTERNAL_GPIO") var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 5240db9e8f..a2e6953a16 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,6 +1,9 @@ from esphome import automation import esphome.codegen as cg -from esphome.config_helpers import filter_source_files_from_platform +from esphome.config_helpers import ( + filter_source_files_from_defines, + filter_source_files_from_platform, +) import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -171,24 +174,17 @@ _filter_backend_source_files = filter_source_files_from_platform( ) +# USE_OTA_SIGNED_VERIFICATION_MULTI_KEY is set only on ESP32/IDF; +# USE_OTA_PARTITIONS is set by the esphome OTA platform when +# allow_partition_access is enabled. +_filter_define_source_files = filter_source_files_from_defines( + { + "ota_signature_esp_idf.cpp": "USE_OTA_SIGNED_VERIFICATION_MULTI_KEY", + "ota_bootloader_esp_idf.cpp": "USE_OTA_PARTITIONS", + "ota_partitions_esp_idf.cpp": "USE_OTA_PARTITIONS", + } +) + + def FILTER_SOURCE_FILES() -> list[str]: - files = _filter_backend_source_files() - # ota_signature_esp_idf.cpp implements multi-key OTA signature verification, - # compiled only when the esp32 component enables it (external RSA signed - # OTA sets USE_OTA_SIGNED_VERIFICATION_MULTI_KEY). The define is set only on - # ESP32/IDF, so this also excludes the file on every other platform. Filter - # it out otherwise so the (otherwise fully #ifdef'd-out) file isn't opened - # and parsed on every build. - if not any( - define.name == "USE_OTA_SIGNED_VERIFICATION_MULTI_KEY" - for define in CORE.defines - ): - files.append("ota_signature_esp_idf.cpp") - # ota_bootloader_esp_idf.cpp and ota_partitions_esp_idf.cpp are fully - # #ifdef'd on USE_OTA_PARTITIONS (set by the esphome OTA platform when - # allow_partition_access is enabled). Filter them out otherwise for the - # same reason as above. - if not any(define.name == "USE_OTA_PARTITIONS" for define in CORE.defines): - files.append("ota_bootloader_esp_idf.cpp") - files.append("ota_partitions_esp_idf.cpp") - return files + return _filter_backend_source_files() + _filter_define_source_files() diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 6ad76046a1..79d4ce5e0c 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -5,6 +5,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import mqtt, web_server, zigbee from esphome.components.const import CONF_B_CONSTANT +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_ABOVE, @@ -1303,3 +1304,8 @@ def _lstsq(a, b): @coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(sensor_ns.using) + + +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"filter.cpp": "USE_SENSOR_FILTER"} +) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index a3f4999a8f..29399a51b7 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -1,6 +1,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import mqtt, web_server +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, @@ -256,3 +257,8 @@ async def text_sensor_state_to_code(config, condition_id, template_arg, args): templ = await cg.templatable(config[CONF_STATE], args, cg.std_string) cg.add(var.set_state(templ)) return var + + +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"filter.cpp": "USE_TEXT_SENSOR_FILTER"} +) diff --git a/esphome/components/uptime/sensor/__init__.py b/esphome/components/uptime/sensor/__init__.py index debeb41444..dd76bb5a87 100644 --- a/esphome/components/uptime/sensor/__init__.py +++ b/esphome/components/uptime/sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor, time +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_TIME_ID, @@ -10,7 +11,6 @@ from esphome.const import ( STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ) -from esphome.core import CORE uptime_ns = cg.esphome_ns.namespace("uptime") UptimeSecondsSensor = uptime_ns.class_( @@ -62,9 +62,6 @@ async def to_code(config): cg.add(var.set_time(time_id)) -def FILTER_SOURCE_FILES() -> list[str]: - # uptime_timestamp_sensor.cpp is fully #ifdef'd on USE_TIME; skip it - # when no time component is configured. - if not any(define.name == "USE_TIME" for define in CORE.defines): - return ["uptime_timestamp_sensor.cpp"] - return [] +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"uptime_timestamp_sensor.cpp": "USE_TIME"} +) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index c82c2b3dbe..60bed1537e 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -151,6 +151,31 @@ def filter_source_files_from_platform( return filter_source_files +def filter_source_files_from_defines( + files_map: dict[str, str | tuple[str, ...]], +) -> Callable[[], list[str]]: + """Helper to build a FILTER_SOURCE_FILES function from a define mapping. + + Args: + files_map: Dict mapping filename to the define name (or tuple of + define names) that keeps the file in the build; the file is + excluded when none of its defines is set for the current config. + + Returns: + Function that returns the files to exclude for the current config. + """ + + def filter_source_files() -> list[str]: + defines = {define.name for define in CORE.defines} + return [ + filename + for filename, needed in files_map.items() + if defines.isdisjoint((needed,) if isinstance(needed, str) else needed) + ] + + return filter_source_files + + def get_logger_level() -> str: """Get the configured logger level. diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bb4960aec7..20aca3776f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -43,7 +43,9 @@ #define USE_ALARM_CONTROL_PANEL #define USE_AREAS #define USE_BINARY_SENSOR +#define USE_BINARY_SENSOR_CLICK_TRIGGER #define USE_BINARY_SENSOR_FILTER +#define USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER #define USE_BLE_DEVICE_IRK #define USE_BUTTON #define USE_CAMERA @@ -281,6 +283,7 @@ // ESP32-specific feature flags #ifdef USE_ESP32 #define USE_ESP32_CRASH_HANDLER +#define USE_ESP32_INTERNAL_GPIO #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER #define ESPHOME_TASK_LOG_BUFFER_SIZE 768 diff --git a/tests/components/binary_sensor/common.yaml b/tests/components/binary_sensor/common.yaml index 4f4cf6ea59..d0a16cc99c 100644 --- a/tests/components/binary_sensor/common.yaml +++ b/tests/components/binary_sensor/common.yaml @@ -136,3 +136,19 @@ binary_sensor: invalid_cooldown: 2s then: - logger.log: "Click with custom cooldown" + + # Test on_click and on_double_click (compiles match_interval via + # USE_BINARY_SENSOR_CLICK_TRIGGER) + - platform: template + id: click_triggers + name: "Click Triggers" + on_click: + min_length: 50ms + max_length: 350ms + then: + - logger.log: "Clicked" + on_double_click: + min_length: 50ms + max_length: 350ms + then: + - logger.log: "Double clicked" diff --git a/tests/unit_tests/test_config_helpers.py b/tests/unit_tests/test_config_helpers.py index 88913c0f23..e53016dfc3 100644 --- a/tests/unit_tests/test_config_helpers.py +++ b/tests/unit_tests/test_config_helpers.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest from esphome.config_helpers import ( + filter_source_files_from_defines, filter_source_files_from_platform, frameworks_for_platforms, get_logger_level, @@ -18,6 +19,7 @@ from esphome.const import ( KEY_TARGET_PLATFORM, PlatformFramework, ) +from esphome.core import Define def test_filter_source_files_from_platform_esp32() -> None: @@ -148,3 +150,25 @@ def test_frameworks_for_platforms_derives_and_rejects_unknown() -> None: } with pytest.raises(ValueError, match="unknown platform"): frameworks_for_platforms(["esp32", "not_a_platform"]) + + +def test_filter_source_files_from_defines() -> None: + """Files are excluded unless one of their defines is set.""" + files_map: dict[str, str | tuple[str, ...]] = { + "filter.cpp": "USE_SENSOR_FILTER", + "automation.cpp": ("USE_CLICK", "USE_MULTI_CLICK"), + } + filter_func: Callable[[], list[str]] = filter_source_files_from_defines(files_map) + + with patch("esphome.config_helpers.CORE") as mock_core: + mock_core.defines = {Define("USE_SENSOR_FILTER")} + assert filter_func() == ["automation.cpp"] + + mock_core.defines = {Define("USE_MULTI_CLICK")} + assert filter_func() == ["filter.cpp"] + + mock_core.defines = {Define("USE_SENSOR_FILTER"), Define("USE_CLICK")} + assert filter_func() == [] + + mock_core.defines = set() + assert sorted(filter_func()) == ["automation.cpp", "filter.cpp"] From f741c274d577f748afc31b9156b1daecca9392cc Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:55:42 +0000 Subject: [PATCH 093/155] Bump aioesphomeapi from 45.13.1 to 46.0.0 (#18683) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3362e43239..822eebc1f2 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.13.1 +aioesphomeapi==46.0.0 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi zeroconf==0.150.0 puremagic==2.2.0 From 4efd30834575606ffc878549d7c4626c79e5eba4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 11:26:55 -0500 Subject: [PATCH 094/155] [tests] Fix flaky pty log probe test on macOS (#18681) --- tests/unit_tests/test_log.py | 48 +++++++++++++++++------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/tests/unit_tests/test_log.py b/tests/unit_tests/test_log.py index 194b38209b..40e3aa6d22 100644 --- a/tests/unit_tests/test_log.py +++ b/tests/unit_tests/test_log.py @@ -1,5 +1,4 @@ from collections.abc import Generator -import errno import io import logging import os @@ -178,37 +177,34 @@ def _run_probe_on_pty( output = b"" deadline = time.monotonic() + 60 try: - try: - proc = subprocess.Popen( - _probe_command(fixture_path), - stdout=follower, - stderr=follower if stderr_to_pty else subprocess.PIPE, - stdin=follower, - env=probe_env, - ) - finally: - os.close(follower) - while True: - timeout = deadline - time.monotonic() - if timeout <= 0 or not select.select([controller], [], [], timeout)[0]: - pytest.fail(f"pty probe produced no EOF in time; got {output!r}") - try: - chunk = os.read(controller, 1024) - except OSError as err: - # macOS raises EIO once the child closes its end of the pty; - # anything else is a real failure, not end-of-stream. - if err.errno != errno.EIO: - raise - break - if not chunk: - break + proc = subprocess.Popen( + _probe_command(fixture_path), + stdout=follower, + stderr=follower if stderr_to_pty else subprocess.PIPE, + stdin=follower, + env=probe_env, + ) + # The parent keeps the follower open until the child has exited and + # the controller is drained: macOS discards buffered pty output once + # the last follower closes, so closing it early loses the probe's + # output whenever the child finishes before the first read. + while proc.poll() is None: + if time.monotonic() > deadline: + pytest.fail(f"pty probe did not exit in time; got {output!r}") + if select.select([controller], [], [], 0.01)[0]: + output += os.read(controller, 4096) + # Everything the child wrote is already buffered, so drain without waiting. + while select.select([controller], [], [], 0)[0] and ( + chunk := os.read(controller, 4096) + ): output += chunk stderr_text = "" if proc.stderr is not None: stderr_text = proc.stderr.read().decode(errors="replace") proc.stderr.close() - assert proc.wait(60) == 0, stderr_text + assert proc.returncode == 0, stderr_text finally: + os.close(follower) os.close(controller) if proc is not None and proc.poll() is None: proc.kill() From dde6906f980ecff7dedf08f289edf97b6e9ef5ba Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Sun, 23 Aug 2026 11:01:34 -0700 Subject: [PATCH 095/155] [modbus_client] Add continuous option to the read and send actions (#18542) Co-authored-by: Claude --- esphome/components/modbus/__init__.py | 79 ++++++++++++++++++- esphome/components/modbus/modbus.cpp | 23 +++--- esphome/components/modbus/modbus.h | 48 +++++++---- esphome/components/modbus_client/__init__.py | 70 +++++++++++++--- .../components/modbus_client/modbus_client.h | 40 ++++++++-- .../modbus_client/test_modbus_client.py | 31 +++++++- .../modbus/modbus_client_hub_test.cpp | 40 +++++----- tests/components/modbus_client/common.yaml | 5 ++ 8 files changed, 261 insertions(+), 75 deletions(-) diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index a98591c6bc..89ffc7facf 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -1,17 +1,23 @@ from __future__ import annotations import logging -from typing import Any, Literal +from typing import Any, Literal, NamedTuple from esphome import pins import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ADDRESS, CONF_DISABLE_CRC, CONF_FLOW_CONTROL_PIN, CONF_ID +from esphome.const import ( + CONF_ADDRESS, + CONF_CONTINUOUS, + CONF_DISABLE_CRC, + CONF_FLOW_CONTROL_PIN, + CONF_ID, +) from esphome.cpp_generator import MockObj from esphome.cpp_helpers import gpio_pin_expression import esphome.final_validate as fv -from esphome.types import ConfigType +from esphome.types import ConfigType, TemplateArgsType _LOGGER = logging.getLogger(__name__) @@ -48,6 +54,73 @@ CONF_TURNAROUND_TIME = "turnaround_time" MODBUS_ROLES = ["client", "server"] + +class _CommandOption(NamedTuple): + """One per-command option forwarded to the hub (modbus::CommandOptions).""" + + conf_key: str + field: str # the C++ field, and so the set_() setter name + validator: Any # the static (non-templatable) validator for the key + cpp_type: Any # the C++ type the value is generated as + default: Any + + +# Per-direction command options. Single-sourcing the schema and the setter generation here keeps +# them from drifting; the C++ side must add the matching field per the rules documented on +# CommandOptions (modbus.h). +_COMMAND_OPTIONS: dict[str, list[_CommandOption]] = { + "read": [_CommandOption(CONF_CONTINUOUS, "continuous", cv.boolean, bool, False)], + "write": [], +} + + +def _command_options(direction: str) -> list[_CommandOption]: + try: + return _COMMAND_OPTIONS[direction] + except KeyError: + raise ValueError(f"unknown command-options direction {direction!r}") from None + + +def command_options_schema( + *, direction: Literal["read", "write"], templatable: bool = False +) -> dict[cv.Optional, Any]: + """Schema fragment for the per-command options a component forwards to the hub + (modbus::CommandOptions). Extend this into any schema that queues commands. Keys are + direction-specific so a schema never offers an option the hub would strip (e.g. + continuous on a write); the write side has no options yet. For actions (templatable=True the + keys also accept lambdas), register the values with register_templatable_command_options(). + """ + return { + cv.Optional(option.conf_key, default=option.default): ( + cv.templatable(option.validator) if templatable else option.validator + ) + for option in _command_options(direction) + } + + +async def register_templatable_command_options( + var: MockObj, config: ConfigType, args: TemplateArgsType, direction: str +) -> None: + """Generate the set_