[sensor] Defer integration imports out of module scope

This commit is contained in:
J. Nick Koston
2026-08-13 21:21:56 -05:00
parent edeaded161
commit a04dca7823
8 changed files with 34 additions and 25 deletions
+17 -7
View File
@@ -3,7 +3,6 @@ import math
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
import esphome.config_validation as cv
from esphome.const import (
@@ -110,10 +109,12 @@ from esphome.const import (
DEVICE_CLASS_WIND_SPEED,
ENTITY_CATEGORY_CONFIG,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
from esphome.core.entity_helpers import (
entity_duplicate_validator,
lazy_load_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -314,12 +315,14 @@ validate_icon = cv.icon
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(zigbee.SENSOR_SCHEMA)
.extend(entity_helpers.ZIGBEE_SENSOR_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTSensorComponent")
),
cv.GenerateID(): cv.declare_id(Sensor),
cv.Optional(
CONF_UNIT_OF_MEASUREMENT, visibility=cv.Visibility.ADVANCED
@@ -359,7 +362,7 @@ _SENSOR_SCHEMA = (
)
_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("sensor"))
_SENSOR_SCHEMA.add_extra(zigbee.validate_sensor)
_SENSOR_SCHEMA.add_extra(lazy_load_validator("zigbee", "validate_sensor"))
def sensor_schema(
@@ -980,6 +983,8 @@ async def setup_sensor_core_(var, config):
CORE.add_job(_build_sensor_automations, var, config)
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
from esphome.components import mqtt
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
@@ -992,9 +997,14 @@ async def setup_sensor_core_(var, config):
cg.add(mqtt_.set_expire_after(expire_after))
if web_server_config := config.get(CONF_WEB_SERVER):
from esphome.components import web_server
await web_server.add_entity_config(var, web_server_config)
await zigbee.setup_sensor(var, config)
if "zigbee" in CORE.loaded_integrations:
from esphome.components import zigbee
await zigbee.setup_sensor(var, config)
async def register_sensor(var, config):
+2 -2
View File
@@ -47,7 +47,7 @@ from .zigbee_esp32 import (
validate_sensor_esp32,
zigbee_require_vfs_select,
)
from .zigbee_zephyr import zephyr_number, zephyr_sensor, zephyr_switch
from .zigbee_zephyr import zephyr_number, zephyr_switch
_LOGGER = logging.getLogger(__name__)
@@ -60,7 +60,7 @@ CONFLICTS_WITH = ["openthread"]
# them without importing this package; re-exported here for existing consumers.
BASE_SCHEMA = entity_helpers.ZIGBEE_BASE_ENTITY_SCHEMA
BINARY_SENSOR_SCHEMA = entity_helpers.ZIGBEE_BINARY_SENSOR_SCHEMA
SENSOR_SCHEMA = cv.Schema({}).extend(BASE_SCHEMA).extend(zephyr_sensor)
SENSOR_SCHEMA = entity_helpers.ZIGBEE_SENSOR_SCHEMA
SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch)
NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number)
+1 -1
View File
@@ -3,10 +3,10 @@
from esphome.const import ( # noqa: F401 # pylint: disable=unused-import
CONF_ZIGBEE_BINARY_SENSOR,
CONF_ZIGBEE_ID,
CONF_ZIGBEE_SENSOR,
)
CONF_MAX_EP_NUMBER_ZEPHYR = 8
CONF_ZIGBEE_SENSOR = "zigbee_sensor"
CONF_ZIGBEE_SWITCH = "zigbee_switch"
CONF_ZIGBEE_NUMBER = "zigbee_number"
CONF_SLEEPY = "sleepy"
@@ -57,15 +57,6 @@ ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component)
ZigbeeSwitch = zigbee_ns.class_("ZigbeeSwitch", cg.Component)
ZigbeeNumber = zigbee_ns.class_("ZigbeeNumber", cg.Component)
zephyr_sensor = cv.Schema(
{
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent),
cv.OnlyWith(CONF_ZIGBEE_SENSOR, ["nrf52", "zigbee"]): cv.declare_id(
ZigbeeSensor
),
}
)
zephyr_switch = cv.Schema(
{
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent),
+1
View File
@@ -1151,6 +1151,7 @@ CONF_YEAR = "year"
CONF_ZERO = "zero"
CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor"
CONF_ZIGBEE_ID = "zigbee_id"
CONF_ZIGBEE_SENSOR = "zigbee_sensor"
TYPE_GIT = "git"
TYPE_LOCAL = "local"
+4
View File
@@ -27,6 +27,7 @@ from esphome.const import (
CONF_WEB_SERVER_ID,
CONF_ZIGBEE_BINARY_SENSOR,
CONF_ZIGBEE_ID,
CONF_ZIGBEE_SENSOR,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.config import (
@@ -743,6 +744,7 @@ ZIGBEE_BASE_ENTITY_SCHEMA = cv.Schema(
# class against the owning declaration in zigbee_zephyr.
_ZIGBEE_ENTITY_CLASSES = {
"binary_sensor": (CONF_ZIGBEE_BINARY_SENSOR, "ZigbeeBinarySensor"),
"sensor": (CONF_ZIGBEE_SENSOR, "ZigbeeSensor"),
}
@@ -762,6 +764,8 @@ def _zigbee_entity_schema(platform: str) -> cv.Schema:
ZIGBEE_BINARY_SENSOR_SCHEMA = _zigbee_entity_schema("binary_sensor")
ZIGBEE_SENSOR_SCHEMA = _zigbee_entity_schema("sensor")
def lazy_load_validator(
component: str, name: str
+1 -1
View File
@@ -557,7 +557,7 @@ def lint_constants_usage():
# Maximum allowed CONF_ constants in esphome/const.py.
# This file is frozen — new constants go in esphome/components/const/__init__.py.
# Decrease this number when constants are moved out of const.py.
CONST_PY_MAX_CONF = 1024
CONST_PY_MAX_CONF = 1025
@lint_content_check(include=["esphome/const.py"])
+8 -5
View File
@@ -19,6 +19,8 @@ from pathlib import Path
import subprocess
import sys
import pytest
# Modules that must only load for the commands that actually use them
# (compile/config validation, shell completion), never from a bare
# ``import esphome.__main__``.
@@ -190,21 +192,22 @@ def test_api_client_does_not_import_heavy_modules() -> None:
)
def test_binary_sensor_does_not_import_integrations() -> None:
@pytest.mark.parametrize("component", ["binary_sensor", "sensor"])
def test_entity_component_does_not_import_integrations(component: str) -> None:
"""An entity component must not drag in its optional integrations.
binary_sensor's mqtt/web_server/zigbee schema fragments live in
The mqtt/web_server/zigbee schema fragments live in
``esphome.core.entity_helpers``; the integration packages (and the
esp32/logger chains mqtt and zigbee pull in) must only load when the
user's config actually uses them.
"""
allowed = (
"esphome.components",
"esphome.components.binary_sensor",
f"esphome.components.{component}",
"esphome.components.const",
)
check = (
"import sys; import esphome.components.binary_sensor; "
f"import sys; import esphome.components.{component}; "
f"leaked = [m for m in sys.modules "
f"if m.startswith('esphome.components') and m not in {allowed!r}]; "
"print(','.join(leaked))"
@@ -217,7 +220,7 @@ def test_binary_sensor_does_not_import_integrations() -> None:
)
leaked = result.stdout.strip()
assert not leaked, (
f"esphome.components.binary_sensor imports integration packages at "
f"esphome.components.{component} imports integration packages at "
f"top level: {leaked}. Keep the shared schema fragments in "
"esphome.core.entity_helpers and import the integration inside "
"to_code instead."