Merge branch 'dev' into esp8266-arduino-toolchain

This commit is contained in:
J. Nick Koston
2026-08-20 09:06:15 -05:00
32 changed files with 624 additions and 133 deletions
+1 -1
View File
@@ -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 \
+37 -4
View File
@@ -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()),
)
+10 -2
View File
@@ -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)
+26 -10
View File
@@ -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])
+13 -6
View File
@@ -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)}",
)
+23 -7
View File
@@ -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)
+10 -4
View File
@@ -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()
+16 -11
View File
@@ -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)
+9
View File
@@ -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
+14 -6
View File
@@ -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))
+7 -6
View File
@@ -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)
+5 -3
View File
@@ -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 -1
View File
@@ -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],
+23 -5
View File
@@ -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]
+21 -10
View File
@@ -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");
+18 -8
View File
@@ -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")
+7
View File
@@ -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
+11 -3
View File
@@ -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
+5 -2
View File
@@ -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])
+22 -4
View File
@@ -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
+22 -4
View File
@@ -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) {
+23 -1
View File
@@ -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 --
+101 -14
View File
@@ -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()
+69
View File
@@ -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)
+89 -1
View File
@@ -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