From 3665bcc4e37d3fe43c34ebafa278d743f597449b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Oct 2026 02:40:35 -1000 Subject: [PATCH] [ble_device_base][ble_client][modbus] Add new_ble_device_component, new_ble_node_component and new_modbus_client_component (#21025) --- esphome/components/ble_client/__init__.py | 10 +++++++++ .../components/ble_device_base/__init__.py | 10 ++++++++- esphome/components/modbus/__init__.py | 9 +++++++- .../components/test_new_bus_component.py | 22 +++++++++++++++++-- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index b783ef1c153..64dd02c4dc0 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -1,3 +1,6 @@ +from collections.abc import Coroutine +from typing import Any + from esphome import automation from esphome.automation import maybe_simple_id import esphome.codegen as cg @@ -171,6 +174,13 @@ async def register_ble_node(var, config): cg.add(parent.register_ble_node(var)) +def new_ble_node_component( + config: ConfigType, *args: Any +) -> Coroutine[Any, Any, cg.MockObj]: + """Construct, register and attach to the BLE client; args go to the constructor.""" + return cg.new_bus_component(register_ble_node, config, *args) + + BLE_WRITE_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.use_id(BLEClient), diff --git a/esphome/components/ble_device_base/__init__.py b/esphome/components/ble_device_base/__init__.py index e8283457c6c..9f089034143 100644 --- a/esphome/components/ble_device_base/__init__.py +++ b/esphome/components/ble_device_base/__init__.py @@ -24,8 +24,9 @@ AES-CCM decryption for encrypted advertisements is provided portably in ble_aes_ccm.h. """ -from collections.abc import Callable +from collections.abc import Callable, Coroutine import re +from typing import Any import esphome.codegen as cg from esphome.components.const import CONF_WINDOW @@ -182,6 +183,13 @@ async def register_ble_device(var: cg.MockObj, config: ConfigType) -> cg.MockObj return var +def new_ble_device_component( + config: ConfigType, *args: Any +) -> Coroutine[Any, Any, cg.MockObj]: + """Construct, register and attach to the BLE hub; args go to the constructor.""" + return cg.new_bus_component(register_ble_device, config, *args) + + # ---- shared validation / codegen helpers (platform-neutral) ---- diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 4e4d7fe1608..2c3aba950fe 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine import logging from typing import Any, Literal, NamedTuple @@ -390,6 +390,13 @@ async def register_modbus_client_device(var: MockObj, config: ConfigType) -> Non cg.add(var.set_address(config[CONF_ADDRESS])) +def new_modbus_client_component( + config: ConfigType, *args: Any +) -> Coroutine[Any, Any, MockObj]: + """Construct, register and attach to the modbus bus; args go to the constructor.""" + return cg.new_bus_component(register_modbus_client_device, config, *args) + + 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])) diff --git a/tests/unit_tests/components/test_new_bus_component.py b/tests/unit_tests/components/test_new_bus_component.py index 54f947d6257..2fa5c67b24e 100644 --- a/tests/unit_tests/components/test_new_bus_component.py +++ b/tests/unit_tests/components/test_new_bus_component.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, call, patch import pytest from esphome import cpp_helpers -from esphome.components import i2c, spi, uart +from esphome.components import ble_client, ble_device_base, i2c, modbus, spi, uart from esphome.const import CONF_ID @@ -18,6 +18,17 @@ from esphome.const import CONF_ID (i2c.new_i2c_component, i2c, "register_i2c_device"), (uart.new_uart_component, uart, "register_uart_device"), (spi.new_spi_component, spi, "register_spi_device"), + ( + ble_device_base.new_ble_device_component, + ble_device_base, + "register_ble_device", + ), + (ble_client.new_ble_node_component, ble_client, "register_ble_node"), + ( + modbus.new_modbus_client_component, + modbus, + "register_modbus_client_device", + ), ], ) @pytest.mark.asyncio @@ -47,7 +58,14 @@ async def test_creates_registers_and_attaches_in_order( @pytest.mark.parametrize( "helper", - [i2c.new_i2c_component, uart.new_uart_component, spi.new_spi_component], + [ + i2c.new_i2c_component, + uart.new_uart_component, + spi.new_spi_component, + ble_device_base.new_ble_device_component, + ble_client.new_ble_node_component, + modbus.new_modbus_client_component, + ], ) def test_wrappers_are_plain_functions(helper: Callable[..., Any]) -> None: # An async wrapper would stay on the stack and become the component source.