mirror of
https://github.com/esphome/esphome.git
synced 2026-08-30 09:36:03 +00:00
Merge branch 'dev' into esp8266-arduino-toolchain
This commit is contained in:
+1
-1
@@ -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 \
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2127,13 +2137,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)}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -580,7 +580,14 @@ bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &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) {
|
||||
|
||||
@@ -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 --
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}):
|
||||
@@ -533,6 +579,48 @@ def testget_tools_path_default_is_global_cache(
|
||||
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")
|
||||
|
||||
|
||||
def test_resolve_toolchain_rejects_unsupported() -> None:
|
||||
"""A --toolchain nRF52 cannot serve fails instead of degrading silently."""
|
||||
from esphome.components.nrf52 import _resolve_toolchain
|
||||
|
||||
Reference in New Issue
Block a user