mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 08:20:21 +00:00
Merge branch 'esp8266-native-library-backend' into esp8266-native-build-spec
This commit is contained in:
@@ -4,6 +4,6 @@ from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code(config):
|
||||
cg.add_build_flag("-DUSE_TIME_TIMEZONE")
|
||||
cg.add_define("USE_TIME_TIMEZONE")
|
||||
|
||||
manifest.to_code = to_code
|
||||
|
||||
@@ -57,6 +57,14 @@ def reset_core() -> Generator[None]:
|
||||
CORE.reset()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_full_config() -> Generator[None]:
|
||||
"""Give each test a clean final-validate config and restore it after."""
|
||||
token = final_validate.full_config.set({})
|
||||
yield
|
||||
final_validate.full_config.reset(token)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def set_core_config() -> Generator[SetCoreConfigCallable]:
|
||||
"""Fixture to set up the core configuration for tests."""
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
libraries:
|
||||
- NetworkClientSecure
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: arduino
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
use_full_certificate_bundle: true
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE: y
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
esp32_camera_web_server:
|
||||
port: 8080
|
||||
mode: stream
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_NVS_ENCRYPTION: y
|
||||
CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC: y
|
||||
CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID: "0"
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_NVS_ENCRYPTION: n
|
||||
@@ -17,6 +17,7 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANTS,
|
||||
NetworkSdkconfigData,
|
||||
RawSdkconfigValue,
|
||||
_ota_downgrade_protection_errors,
|
||||
_reconcile_network_sdkconfig,
|
||||
_reconcile_vfs_fatfs_sdkconfig,
|
||||
@@ -274,9 +275,24 @@ def test_esp32_configuration_errors(
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_web_server.yaml",
|
||||
("esp-tls",),
|
||||
("esp-tls", "esp_http_server"),
|
||||
id="web_server_idf",
|
||||
),
|
||||
pytest.param(
|
||||
"nvs_encryption_s3.yaml",
|
||||
("nvs_sec_provider",),
|
||||
id="nvs_encryption",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_nvs_sdkconfig.yaml",
|
||||
("nvs_sec_provider",),
|
||||
id="nvs_encryption_raw_sdkconfig",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_camera_web_server.yaml",
|
||||
("esp_http_server",),
|
||||
id="esp32_camera_web_server",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_nextion.yaml",
|
||||
("esp-tls", "esp_http_client"),
|
||||
@@ -304,6 +320,76 @@ def test_default_exclusions_reincluded_by_owning_components(
|
||||
# Components no part of this config touches stay excluded.
|
||||
assert "unity" in excluded
|
||||
assert "fatfs" in excluded
|
||||
# The HTTP server only comes back for configs that run one.
|
||||
assert ("esp_http_server" in excluded) == ("esp_http_server" not in reincluded)
|
||||
|
||||
|
||||
def test_nvs_sec_provider_stays_excluded_when_encryption_is_off(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""An explicit CONFIG_NVS_ENCRYPTION=n keeps nvs_sec_provider excluded."""
|
||||
from esphome.components.esp32.const import KEY_EXCLUDE_COMPONENTS
|
||||
|
||||
generate_main(component_config_path("exclusion_stays_nvs_sdkconfig_off.yaml"))
|
||||
assert "nvs_sec_provider" in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]
|
||||
|
||||
|
||||
_BUNDLE_OPTIONS = (
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE",
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN",
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "expected"),
|
||||
[
|
||||
pytest.param("exclusion_reincludes.yaml", (False, None, None), id="no_tls"),
|
||||
pytest.param(
|
||||
"certificate_bundle_http_request.yaml",
|
||||
(True, True, False),
|
||||
id="http_request",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_http_request.yaml",
|
||||
(False, None, None),
|
||||
id="http_request_no_verify",
|
||||
),
|
||||
pytest.param(
|
||||
"certificate_bundle_full.yaml", (True, None, True), id="full_option"
|
||||
),
|
||||
pytest.param(
|
||||
"certificate_bundle_arduino_tls.yaml",
|
||||
(True, True, False),
|
||||
id="arduino_network_client_secure",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_certificate_bundle_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
expected: tuple[bool | None, ...],
|
||||
) -> None:
|
||||
"""The bundle and its CMN/FULL variant are written only when requested."""
|
||||
generate_main(component_config_path(config_file))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert tuple(sdkconfig.get(name) for name in _BUNDLE_OPTIONS) == expected
|
||||
|
||||
|
||||
def test_user_sdkconfig_certificate_bundle_wins(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A raw sdkconfig_options bundle setting is kept and still pins CMN."""
|
||||
generate_main(component_config_path("certificate_bundle_sdkconfig.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
value = sdkconfig["CONFIG_MBEDTLS_CERTIFICATE_BUNDLE"]
|
||||
assert isinstance(value, RawSdkconfigValue)
|
||||
assert value.value == "y"
|
||||
assert sdkconfig.get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN") is True
|
||||
assert sdkconfig.get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL") is False
|
||||
|
||||
|
||||
def test_execute_from_psram_s3_sdkconfig(
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
interface: spi2
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
miso_pin: GPIO19
|
||||
|
||||
ethernet:
|
||||
id: eth_component
|
||||
type: W5500
|
||||
spi_id: spi_bus
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO36
|
||||
reset_pin: GPIO22
|
||||
clock_speed: 20MHz
|
||||
@@ -0,0 +1,16 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
ethernet:
|
||||
id: eth_component
|
||||
type: W5500
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
miso_pin: GPIO19
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO36
|
||||
reset_pin: GPIO22
|
||||
clock_speed: 20MHz
|
||||
@@ -27,14 +27,6 @@ _CH390_CONFIG = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_full_config():
|
||||
"""Reset fv.full_config so each test starts with a clean slate."""
|
||||
token = fv.full_config.set({})
|
||||
yield
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_rejects_wifi_and_ethernet_without_priority() -> None:
|
||||
"""Wi-Fi + ethernet without a network: priority: list must be rejected."""
|
||||
fv.full_config.set({"wifi": {}, "ethernet": {}})
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Tests for the ethernet `spi_id:` option (attach to a shared spi bus)."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.components.ethernet import CONF_INTERFACE, CONFIG_SCHEMA, _final_validate
|
||||
from esphome.components.rp2.const import KEY_BOARD as RP2_KEY_BOARD
|
||||
|
||||
# Registers the rp2 pin schema so RP2 configs can validate pins.
|
||||
import esphome.components.rp2.gpio # noqa: F401
|
||||
from esphome.components.spi import CONF_INTERFACE_INDEX
|
||||
from esphome.const import (
|
||||
CONF_CLK_PIN,
|
||||
CONF_ID,
|
||||
CONF_MISO_PIN,
|
||||
CONF_MOSI_PIN,
|
||||
CONF_SPI,
|
||||
CONF_SPI_ID,
|
||||
CONF_TYPE,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
import esphome.final_validate as fv
|
||||
|
||||
from ..types import SetCoreConfigCallable
|
||||
|
||||
_W5500_PIN_CONFIG = {
|
||||
"type": "W5500",
|
||||
"clk_pin": 47,
|
||||
"mosi_pin": 48,
|
||||
"miso_pin": 14,
|
||||
"cs_pin": 21,
|
||||
}
|
||||
|
||||
_W5500_SPI_ID_CONFIG = {
|
||||
"type": "W5500",
|
||||
"spi_id": "spi_bus",
|
||||
"cs_pin": 21,
|
||||
}
|
||||
|
||||
|
||||
def _set_esp32_s3(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={
|
||||
KEY_BOARD: "esp32-s3-devkitc-1",
|
||||
KEY_VARIANT: VARIANT_ESP32S3,
|
||||
KEY_IDF_VERSION: cv.Version(5, 3, 2),
|
||||
},
|
||||
)
|
||||
# _validate derives use_address from the node name, which has no default here.
|
||||
CORE.name = "spi-id-test"
|
||||
|
||||
|
||||
def test_spi_id_accepted_without_pins_or_interface(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""With spi_id set, the pin options are not required and no interface is defaulted."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
config = CONFIG_SCHEMA(dict(_W5500_SPI_ID_CONFIG))
|
||||
assert config[CONF_SPI_ID] == ID("spi_bus")
|
||||
# The interface comes from the referenced bus; no default may be injected.
|
||||
assert CONF_INTERFACE not in config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("key", "value"),
|
||||
[
|
||||
(CONF_CLK_PIN, 47),
|
||||
(CONF_MOSI_PIN, 48),
|
||||
(CONF_MISO_PIN, 14),
|
||||
(CONF_INTERFACE, "spi2"),
|
||||
],
|
||||
)
|
||||
def test_spi_id_rejects_bus_options(
|
||||
set_core_config: SetCoreConfigCallable, key: str, value: int | str
|
||||
) -> None:
|
||||
"""Options provided by the referenced bus must be rejected alongside spi_id."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
with pytest.raises(Invalid, match=f"'{key}' cannot be used together with 'spi_id'"):
|
||||
CONFIG_SCHEMA({**_W5500_SPI_ID_CONFIG, key: value})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", [CONF_CLK_PIN, CONF_MOSI_PIN, CONF_MISO_PIN])
|
||||
def test_bus_pins_still_required_without_spi_id(
|
||||
set_core_config: SetCoreConfigCallable, key: str
|
||||
) -> None:
|
||||
"""Without spi_id, the bus pin options stay required."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
config = {k: v for k, v in _W5500_PIN_CONFIG.items() if k != key}
|
||||
with pytest.raises(
|
||||
Invalid, match=f"'{key}' is a required option when 'spi_id' is not set"
|
||||
):
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
def test_spi_id_rejected_on_rp2(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""spi_id is ESP32-only; the RP2 path is unchanged."""
|
||||
set_core_config(
|
||||
PlatformFramework.RP2_ARDUINO, platform_data={RP2_KEY_BOARD: "rpipicow"}
|
||||
)
|
||||
CORE.name = "spi-id-test"
|
||||
config = {
|
||||
"type": "W5500",
|
||||
"spi_id": "spi_bus",
|
||||
"clk_pin": 18,
|
||||
"mosi_pin": 19,
|
||||
"miso_pin": 16,
|
||||
"cs_pin": 17,
|
||||
}
|
||||
with pytest.raises(Invalid, match="only available on"):
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
def _eth_spi_id_final_config() -> dict:
|
||||
return {CONF_TYPE: "W5500", CONF_SPI_ID: ID("spi_bus")}
|
||||
|
||||
|
||||
class _FakeFinalConfig(dict):
|
||||
"""Dict-backed FinalValidateConfig with just enough ID resolution for
|
||||
fv.id_declaration_match_schema to find an spi bus fragment."""
|
||||
|
||||
def get_path_for_id(self, id: ID) -> list:
|
||||
for index, conf in enumerate(self[CONF_SPI]):
|
||||
if conf[CONF_ID] == id:
|
||||
return [CONF_SPI, index, CONF_ID]
|
||||
raise KeyError(id)
|
||||
|
||||
def get_config_for_path(self, path: list) -> dict:
|
||||
return self[path[0]][path[1]]
|
||||
|
||||
|
||||
def _set_spi_buses(*buses: dict) -> None:
|
||||
fv.full_config.set(_FakeFinalConfig({CONF_SPI: list(buses)}))
|
||||
|
||||
|
||||
_SHAREABLE_BUS = {
|
||||
CONF_ID: ID("spi_bus"),
|
||||
CONF_INTERFACE_INDEX: 0,
|
||||
CONF_MISO_PIN: {},
|
||||
CONF_MOSI_PIN: {},
|
||||
}
|
||||
|
||||
|
||||
def test_final_validate_accepts_hardware_bus_with_data_pins(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A hardware spi bus that declares miso_pin and mosi_pin may be shared."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
# An unrelated bus first: the ID lookup must skip past it.
|
||||
_set_spi_buses({CONF_ID: ID("other_bus"), CONF_INTERFACE_INDEX: 1}, _SHAREABLE_BUS)
|
||||
_final_validate(_eth_spi_id_final_config())
|
||||
|
||||
|
||||
def test_final_validate_rejects_software_bus(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A software spi bus (no hardware interface index) cannot be shared."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
bus = {k: v for k, v in _SHAREABLE_BUS.items() if k != CONF_INTERFACE_INDEX}
|
||||
_set_spi_buses(bus)
|
||||
with pytest.raises(Invalid, match="requires this spi bus to use a hardware"):
|
||||
_final_validate(_eth_spi_id_final_config())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pin_key", [CONF_MISO_PIN, CONF_MOSI_PIN])
|
||||
def test_final_validate_rejects_bus_without_data_pin(
|
||||
set_core_config: SetCoreConfigCallable, pin_key: str
|
||||
) -> None:
|
||||
"""The shared bus must declare both data pins to drive the ethernet chip."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
bus = {k: v for k, v in _SHAREABLE_BUS.items() if k != pin_key}
|
||||
_set_spi_buses(bus)
|
||||
with pytest.raises(Invalid, match=f"requires this spi bus to declare a {pin_key}"):
|
||||
_final_validate(_eth_spi_id_final_config())
|
||||
|
||||
|
||||
def test_final_validate_rejects_colliding_host_without_spi_id(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Without spi_id, claiming the same host as an spi bus stays an error."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
fv.full_config.set({CONF_SPI: [{CONF_ID: ID("spi_bus"), CONF_INTERFACE_INDEX: 0}]})
|
||||
config = {CONF_TYPE: "W5500", CONF_INTERFACE: "spi2"}
|
||||
with pytest.raises(Invalid, match="both using interface 'SPI2_HOST'"):
|
||||
_final_validate(config)
|
||||
|
||||
|
||||
def test_final_validate_accepts_distinct_host_without_spi_id(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Without spi_id, a different host than the spi bus is accepted."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
fv.full_config.set({CONF_SPI: [{CONF_ID: ID("spi_bus"), CONF_INTERFACE_INDEX: 0}]})
|
||||
_final_validate({CONF_TYPE: "W5500", CONF_INTERFACE: "spi3"})
|
||||
|
||||
|
||||
def test_generated_code_uses_spi_parent(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""With spi_id, codegen wires the spi parent and skips the bus options."""
|
||||
main_cpp = generate_main(component_config_path("spi_id_shared_bus.yaml"))
|
||||
|
||||
assert "eth_component->set_spi_parent(spi_bus);" in main_cpp
|
||||
assert "eth_component->set_cs_pin(5);" in main_cpp
|
||||
assert "eth_component->set_clk_pin(" not in main_cpp
|
||||
assert "eth_component->set_miso_pin(" not in main_cpp
|
||||
assert "eth_component->set_mosi_pin(" not in main_cpp
|
||||
assert "eth_component->set_interface(" not in main_cpp
|
||||
|
||||
|
||||
def test_generated_code_without_spi_id_initializes_own_bus(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without spi_id, codegen still emits the pin and interface setters."""
|
||||
main_cpp = generate_main(component_config_path("spi_own_bus.yaml"))
|
||||
|
||||
assert "eth_component->set_spi_parent(" not in main_cpp
|
||||
assert "eth_component->set_clk_pin(18);" in main_cpp
|
||||
assert "eth_component->set_miso_pin(19);" in main_cpp
|
||||
assert "eth_component->set_mosi_pin(23);" in main_cpp
|
||||
assert "eth_component->set_cs_pin(5);" in main_cpp
|
||||
assert "eth_component->set_interface(::SPI3_HOST);" in main_cpp
|
||||
@@ -24,11 +24,9 @@ from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_core_data():
|
||||
"""Wipe CORE.data and reset fv.full_config so each test starts clean."""
|
||||
"""Wipe CORE.data so each test starts clean."""
|
||||
CORE.data.clear()
|
||||
token = fv.full_config.set({})
|
||||
yield
|
||||
fv.full_config.reset(token)
|
||||
CORE.data.clear()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
esphome:
|
||||
name: rp2-trigger-codegen
|
||||
on_boot:
|
||||
then:
|
||||
- rp2_ble_tracker.start_scan:
|
||||
continuous: true
|
||||
# Bare form: restores the configured scan_parameters mode — no
|
||||
# set_continuous emitted (asserted in the codegen test).
|
||||
- rp2_ble_tracker.start_scan:
|
||||
- rp2_ble_tracker.stop_scan
|
||||
|
||||
rp2:
|
||||
board: rpipicow
|
||||
|
||||
rp2_ble_tracker:
|
||||
scan_parameters:
|
||||
continuous: false
|
||||
active: false
|
||||
on_ble_advertise:
|
||||
- mac_address:
|
||||
- AC:37:43:77:5F:4C
|
||||
- 11:22:33:44:55:66
|
||||
then:
|
||||
- lambda: 'char addr[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGD("t", "%s", x.address_str_to(addr));'
|
||||
on_ble_service_data_advertise:
|
||||
- service_uuid: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
|
||||
mac_address: AC:37:43:77:5F:4C
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("t", "%zu", x.size());'
|
||||
- service_uuid: ABCDABCD
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("t", "%zu", x.size());'
|
||||
on_ble_manufacturer_data_advertise:
|
||||
- manufacturer_id: ABCD
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("t", "%zu", x.size());'
|
||||
- manufacturer_id: ABCDABCD
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("t", "%zu", x.size());'
|
||||
- manufacturer_id: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("t", "%zu", x.size());'
|
||||
on_scan_end:
|
||||
- then:
|
||||
- lambda: 'ESP_LOGD("t", "end");'
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Codegen tests for the tracker automations.
|
||||
|
||||
The shared trigger classes (ble_device_base/automation.h) are compiled by the
|
||||
rp2040 compile fixtures, but the codegen accounting — the getattr-built setter
|
||||
spellings, the single set_continuous pin and the listener-count define — is
|
||||
only checkable from the generated main, mirroring the bk72xx/ln882h tests."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from esphome.components import ble_device_base
|
||||
from tests.component_tests.helpers import get_define_value
|
||||
|
||||
|
||||
def test_trigger_codegen(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("test_automations.yaml"))
|
||||
|
||||
# on_ble_advertise: multi-mac filter (two addresses in one initializer list)
|
||||
assert "set_addresses({0xAC3743775F4CULL, 0x112233445566ULL})" in main_cpp
|
||||
# 128-bit service uuid goes out reversed (BLE wire order); single-mac filter
|
||||
assert (
|
||||
"set_service_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB,"
|
||||
"0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB})" in main_cpp
|
||||
)
|
||||
assert "set_address(0xAC3743775F4CULL)" in main_cpp
|
||||
# 32-bit middle branch of the width dispatch
|
||||
assert "set_service_uuid32(0xABCDABCDULL)" in main_cpp
|
||||
# All three manufacturer widths: getattr() builds these names as strings,
|
||||
# so a misspelling only ever fails here.
|
||||
assert "set_manufacturer_uuid16(0xABCDULL)" in main_cpp
|
||||
assert "set_manufacturer_uuid32(0xABCDABCDULL)" in main_cpp
|
||||
assert (
|
||||
"set_manufacturer_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB,"
|
||||
"0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB})" in main_cpp
|
||||
)
|
||||
# scan-control actions: templatable continuous lambda + parented actions.
|
||||
# Exactly one set_continuous: the bare start_scan emits none, pinning the
|
||||
# restore-configured-mode divergence from esp32 against a future default=.
|
||||
assert main_cpp.count("->set_continuous(") == 1
|
||||
assert "startscanaction_id->set_continuous(" in main_cpp
|
||||
assert "stopscanaction_id->set_parent(" in main_cpp
|
||||
# scan_parameters continuous: false reaches the YAML-mode setter, not the
|
||||
# runtime override.
|
||||
assert "->set_configured_continuous(false)" in main_cpp
|
||||
# active: false (non-default) flows through to the setter.
|
||||
assert "->set_scan_active(false)" in main_cpp
|
||||
# Constructor call, not just the declaration: the parent argument is what
|
||||
# registers the trigger as a listener.
|
||||
assert re.search(
|
||||
r"new\(\w+\) ble_device_base::BLEEndOfScanTrigger\(\w+\)", main_cpp
|
||||
)
|
||||
|
||||
# Seven triggers register as listeners; an undercount silently drops the
|
||||
# last trigger at runtime (StaticVector::push_back past capacity), so the
|
||||
# define is the assertion that matters most.
|
||||
assert get_define_value(ble_device_base.LISTENER_COUNT_DEFINE) == "7"
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
@@ -0,0 +1,21 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
logger:
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
on_time:
|
||||
- seconds: 0
|
||||
then:
|
||||
- logger.log: tick
|
||||
@@ -0,0 +1,20 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
logger:
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
on_time_sync:
|
||||
then:
|
||||
- logger.log: synced
|
||||
@@ -0,0 +1,28 @@
|
||||
"""automation.cpp (CronTrigger and SyncTrigger) is only compiled when an
|
||||
on_time or on_time_sync automation exists, so the define must follow them."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fixture", "emits"),
|
||||
[
|
||||
("no_triggers.yaml", False),
|
||||
("on_time.yaml", True),
|
||||
("on_time_sync.yaml", True),
|
||||
],
|
||||
)
|
||||
def test_triggers_define_follows_automations(
|
||||
fixture: str,
|
||||
emits: bool,
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path(fixture))
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert ("USE_TIME_TRIGGERS" in defines) is emits
|
||||
@@ -0,0 +1,20 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
|
||||
sensor:
|
||||
- platform: uptime
|
||||
name: Uptime Seconds
|
||||
type: seconds
|
||||
@@ -0,0 +1,20 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
|
||||
sensor:
|
||||
- platform: uptime
|
||||
name: Uptime Timestamp
|
||||
type: timestamp
|
||||
@@ -0,0 +1,27 @@
|
||||
"""The timestamp uptime sensor source is only compiled when that type is used,
|
||||
so the define must follow the configured sensor type rather than time: alone."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fixture", "emits"),
|
||||
[
|
||||
("seconds.yaml", False),
|
||||
("timestamp.yaml", True),
|
||||
],
|
||||
)
|
||||
def test_timestamp_define_follows_sensor_type(
|
||||
fixture: str,
|
||||
emits: bool,
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path(fixture))
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert ("USE_UPTIME_TIMESTAMP" in defines) is emits
|
||||
@@ -7,7 +7,7 @@ esp32:
|
||||
enable_lwip_mdns_queries: true
|
||||
enable_lwip_bridge_interface: true
|
||||
disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization
|
||||
use_full_certificate_bundle: false # Test CMN bundle (default)
|
||||
use_full_certificate_bundle: false # Bundle stays off without a component that needs it
|
||||
include_builtin_idf_components:
|
||||
- freertos # Test escape hatch (freertos is always included anyway)
|
||||
enable_full_printf: false
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
ethernet:
|
||||
type: W5500
|
||||
spi_id: spi_bus
|
||||
cs_pin: 5
|
||||
interrupt_pin: 36
|
||||
reset_pin: 22
|
||||
clock_speed: 10Mhz
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
subnet: 255.255.255.0
|
||||
domain: .local
|
||||
mac_address: "02:AA:BB:CC:DD:01"
|
||||
on_connect:
|
||||
- logger.log: "Ethernet connected!"
|
||||
on_disconnect:
|
||||
- logger.log: "Ethernet disconnected!"
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
ethernet: !include common-w5500-spi-id.yaml
|
||||
@@ -0,0 +1,54 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- rp2_ble_tracker.start_scan
|
||||
- rp2_ble_tracker.start_scan:
|
||||
continuous: true
|
||||
# Lambda arm of the templatable value — different codegen instantiation.
|
||||
- rp2_ble_tracker.start_scan:
|
||||
continuous: !lambda return false;
|
||||
- rp2_ble_tracker.stop_scan
|
||||
- rp2_ble_tracker.stop_scan: ble_tracker
|
||||
|
||||
rp2_ble_tracker:
|
||||
on_ble_advertise:
|
||||
- mac_address: AC:37:43:77:5F:4C
|
||||
then:
|
||||
- lambda: |-
|
||||
char addr[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGD("main", "The device address is %s", x.address_str_to(addr));
|
||||
- mac_address:
|
||||
- AC:37:43:77:5F:4C
|
||||
- AC:37:43:77:5F:4D
|
||||
then:
|
||||
- lambda: |-
|
||||
char addr[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGD("main", "The device address is %s", x.address_str_to(addr));
|
||||
on_ble_service_data_advertise:
|
||||
- service_uuid: ABCD
|
||||
# mac_address exercises the UUID triggers' set_address() codegen branch.
|
||||
mac_address: AC:37:43:77:5F:4C
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "Length of service data is %zu", x.size());
|
||||
- service_uuid: ABCDABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "32-bit service data is %zu", x.size());
|
||||
- service_uuid: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "128-bit service data is %zu", x.size());
|
||||
on_ble_manufacturer_data_advertise:
|
||||
- manufacturer_id: ABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "Length of manufacturer data is %zu", x.size());
|
||||
- manufacturer_id: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "128-bit manufacturer data is %zu", x.size());
|
||||
on_scan_end:
|
||||
- then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "Scan ended");
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
rp2_ble_tracker: !include common.yaml
|
||||
automations: !include common-automations.yaml
|
||||
@@ -0,0 +1,12 @@
|
||||
sensor:
|
||||
- platform: sfa40
|
||||
i2c_id: i2c_bus
|
||||
wait_for_ready: false
|
||||
formaldehyde:
|
||||
name: SFA40 formaldehyde
|
||||
temperature:
|
||||
name: SFA40 temperature
|
||||
humidity:
|
||||
name: SFA40 humidity
|
||||
address: 0x5D
|
||||
update_interval: 30s
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
sfa40: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
sfa40: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
sfa40: !include common.yaml
|
||||
@@ -0,0 +1,9 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
sensor:
|
||||
- platform: sfa40
|
||||
i2c_id: i2c_bus
|
||||
wait_for_ready: true
|
||||
formaldehyde:
|
||||
name: SFA40 formaldehyde
|
||||
@@ -4,6 +4,6 @@ from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code(config):
|
||||
cg.add_build_flag("-DUSE_TIME_TIMEZONE")
|
||||
cg.add_define("USE_TIME_TIMEZONE")
|
||||
|
||||
manifest.to_code = to_code
|
||||
|
||||
Reference in New Issue
Block a user