1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-30 06:33:51 +00:00

Merge branch 'dev' into api_size_limits

This commit is contained in:
J. Nick Koston
2025-10-07 16:18:23 -05:00
committed by GitHub
103 changed files with 1006 additions and 532 deletions

View File

@@ -0,0 +1,9 @@
i2c:
- id: i2c_lm75b
scl: ${scl_pin}
sda: ${sda_pin}
sensor:
- platform: lm75b
name: LM75B Temperature
update_interval: 30s

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO15
sda_pin: GPIO13
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO15
sda_pin: GPIO13
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View File

@@ -17,5 +17,7 @@ sensor:
temperature:
name: QMC5883L Temperature
range: 800uT
data_rate: 200Hz
oversampling: 256x
update_interval: 15s
drdy_pin: ${drdy_pin}

View File

@@ -1,5 +1,6 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
drdy_pin: GPIO18
<<: !include common.yaml

View File

@@ -1,5 +1,6 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
drdy_pin: GPIO6
<<: !include common.yaml

View File

@@ -1,5 +1,6 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
drdy_pin: GPIO6
<<: !include common.yaml

View File

@@ -1,5 +1,6 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
drdy_pin: GPIO18
<<: !include common.yaml

View File

@@ -1,5 +1,6 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
drdy_pin: GPIO2
<<: !include common.yaml

View File

@@ -1,5 +1,6 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
drdy_pin: GPIO2
<<: !include common.yaml

View File

@@ -1,6 +1,8 @@
substitutions:
pin: GPIO2
clock_resolution: "2000000"
carrier_duty_percent: "25"
carrier_frequency: "30000"
filter_symbols: "2"
receive_symbols: "4"
rmt_symbols: "64"

View File

@@ -44,37 +44,53 @@ def test_get_clang_tidy_version_from_requirements(
assert result == expected
def test_calculate_clang_tidy_hash() -> None:
"""Test calculating hash from all configuration sources."""
def test_calculate_clang_tidy_hash_with_sdkconfig(tmp_path: Path) -> None:
"""Test calculating hash from all configuration sources including sdkconfig.defaults."""
clang_tidy_content = b"Checks: '-*,readability-*'\n"
requirements_version = "clang-tidy==18.1.5"
platformio_content = b"[env:esp32]\nplatform = espressif32\n"
sdkconfig_content = b"CONFIG_AUTOSTART_ARDUINO=y\n"
requirements_content = "clang-tidy==18.1.5\n"
# Create temporary files
(tmp_path / ".clang-tidy").write_bytes(clang_tidy_content)
(tmp_path / "platformio.ini").write_bytes(platformio_content)
(tmp_path / "sdkconfig.defaults").write_bytes(sdkconfig_content)
(tmp_path / "requirements_dev.txt").write_text(requirements_content)
# Expected hash calculation
expected_hasher = hashlib.sha256()
expected_hasher.update(clang_tidy_content)
expected_hasher.update(requirements_version.encode())
expected_hasher.update(platformio_content)
expected_hasher.update(sdkconfig_content)
expected_hash = expected_hasher.hexdigest()
# Mock the dependencies
with (
patch("clang_tidy_hash.read_file_bytes") as mock_read_bytes,
patch(
"clang_tidy_hash.get_clang_tidy_version_from_requirements",
return_value=requirements_version,
),
):
# Set up mock to return different content based on the file being read
def read_file_mock(path: Path) -> bytes:
if ".clang-tidy" in str(path):
return clang_tidy_content
if "platformio.ini" in str(path):
return platformio_content
return b""
result = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path)
mock_read_bytes.side_effect = read_file_mock
result = clang_tidy_hash.calculate_clang_tidy_hash()
assert result == expected_hash
def test_calculate_clang_tidy_hash_without_sdkconfig(tmp_path: Path) -> None:
"""Test calculating hash without sdkconfig.defaults file."""
clang_tidy_content = b"Checks: '-*,readability-*'\n"
requirements_version = "clang-tidy==18.1.5"
platformio_content = b"[env:esp32]\nplatform = espressif32\n"
requirements_content = "clang-tidy==18.1.5\n"
# Create temporary files (without sdkconfig.defaults)
(tmp_path / ".clang-tidy").write_bytes(clang_tidy_content)
(tmp_path / "platformio.ini").write_bytes(platformio_content)
(tmp_path / "requirements_dev.txt").write_text(requirements_content)
# Expected hash calculation (no sdkconfig)
expected_hasher = hashlib.sha256()
expected_hasher.update(clang_tidy_content)
expected_hasher.update(requirements_version.encode())
expected_hasher.update(platformio_content)
expected_hash = expected_hasher.hexdigest()
result = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path)
assert result == expected_hash
@@ -85,67 +101,63 @@ def test_read_stored_hash_exists(tmp_path: Path) -> None:
hash_file = tmp_path / ".clang-tidy.hash"
hash_file.write_text(f"{stored_hash}\n")
with (
patch("clang_tidy_hash.Path") as mock_path_class,
patch("clang_tidy_hash.read_file_lines", return_value=[f"{stored_hash}\n"]),
):
# Mock the path calculation and exists check
mock_hash_file = Mock()
mock_hash_file.exists.return_value = True
mock_path_class.return_value.parent.parent.__truediv__.return_value = (
mock_hash_file
)
result = clang_tidy_hash.read_stored_hash()
result = clang_tidy_hash.read_stored_hash(repo_root=tmp_path)
assert result == stored_hash
def test_read_stored_hash_not_exists() -> None:
def test_read_stored_hash_not_exists(tmp_path: Path) -> None:
"""Test reading hash when file doesn't exist."""
with patch("clang_tidy_hash.Path") as mock_path_class:
# Mock the path calculation and exists check
mock_hash_file = Mock()
mock_hash_file.exists.return_value = False
mock_path_class.return_value.parent.parent.__truediv__.return_value = (
mock_hash_file
)
result = clang_tidy_hash.read_stored_hash()
result = clang_tidy_hash.read_stored_hash(repo_root=tmp_path)
assert result is None
def test_write_hash() -> None:
def test_write_hash(tmp_path: Path) -> None:
"""Test writing hash to file."""
hash_value = "abc123def456"
hash_file = tmp_path / ".clang-tidy.hash"
with patch("clang_tidy_hash.write_file_content") as mock_write:
clang_tidy_hash.write_hash(hash_value)
clang_tidy_hash.write_hash(hash_value, repo_root=tmp_path)
# Verify write_file_content was called with correct parameters
mock_write.assert_called_once()
args = mock_write.call_args[0]
assert str(args[0]).endswith(".clang-tidy.hash")
assert args[1] == hash_value.strip() + "\n"
assert hash_file.exists()
assert hash_file.read_text() == hash_value.strip() + "\n"
@pytest.mark.parametrize(
("args", "current_hash", "stored_hash", "expected_exit"),
("args", "current_hash", "stored_hash", "hash_file_in_changed", "expected_exit"),
[
(["--check"], "abc123", "abc123", 1), # Hashes match, no scan needed
(["--check"], "abc123", "def456", 0), # Hashes differ, scan needed
(["--check"], "abc123", None, 0), # No stored hash, scan needed
(["--check"], "abc123", "abc123", False, 1), # Hashes match, no scan needed
(["--check"], "abc123", "def456", False, 0), # Hashes differ, scan needed
(["--check"], "abc123", None, False, 0), # No stored hash, scan needed
(
["--check"],
"abc123",
"abc123",
True,
0,
), # Hash file updated in PR, scan needed
],
)
def test_main_check_mode(
args: list[str], current_hash: str, stored_hash: str | None, expected_exit: int
args: list[str],
current_hash: str,
stored_hash: str | None,
hash_file_in_changed: bool,
expected_exit: int,
) -> None:
"""Test main function in check mode."""
changed = [".clang-tidy.hash"] if hash_file_in_changed else []
# Create a mock module that can be imported
mock_helpers = Mock()
mock_helpers.changed_files = Mock(return_value=changed)
with (
patch("sys.argv", ["clang_tidy_hash.py"] + args),
patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash),
patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash),
patch.dict("sys.modules", {"helpers": mock_helpers}),
pytest.raises(SystemExit) as exc_info,
):
clang_tidy_hash.main()

View File

@@ -101,3 +101,10 @@ def mock_get_idedata() -> Generator[Mock, None, None]:
"""Mock get_idedata for platformio_api."""
with patch("esphome.platformio_api.get_idedata") as mock:
yield mock
@pytest.fixture
def mock_get_component() -> Generator[Mock, None, None]:
"""Mock get_component for config module."""
with patch("esphome.config.get_component") as mock:
yield mock

View File

@@ -0,0 +1,10 @@
esphome:
name: test-device
esp32:
board: esp32dev
# Test component with dynamic AUTO_LOAD
test_component:
enable_logger: true
enable_api: false

View File

@@ -0,0 +1,8 @@
esphome:
name: test-device
esp32:
board: esp32dev
# Test component with static AUTO_LOAD
test_component:

View File

@@ -0,0 +1,131 @@
"""Tests for AUTO_LOAD functionality including dynamic AUTO_LOAD."""
from pathlib import Path
from typing import Any
from unittest.mock import Mock
import pytest
from esphome import config, config_validation as cv, yaml_util
from esphome.core import CORE
@pytest.fixture
def fixtures_dir() -> Path:
"""Get the fixtures directory."""
return Path(__file__).parent / "fixtures"
@pytest.fixture
def default_component() -> Mock:
"""Create a default mock component for unmocked components."""
return Mock(
auto_load=[],
is_platform_component=False,
is_platform=False,
multi_conf=False,
multi_conf_no_default=False,
dependencies=[],
conflicts_with=[],
config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA),
)
@pytest.fixture
def static_auto_load_component() -> Mock:
"""Create a mock component with static AUTO_LOAD."""
return Mock(
auto_load=["logger"],
is_platform_component=False,
is_platform=False,
multi_conf=False,
multi_conf_no_default=False,
dependencies=[],
conflicts_with=[],
config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA),
)
def test_static_auto_load_adds_components(
mock_get_component: Mock,
fixtures_dir: Path,
static_auto_load_component: Mock,
default_component: Mock,
) -> None:
"""Test that static AUTO_LOAD triggers loading of specified components."""
CORE.config_path = fixtures_dir / "auto_load_static.yaml"
config_file = fixtures_dir / "auto_load_static.yaml"
raw_config = yaml_util.load_yaml(config_file)
component_mocks = {"test_component": static_auto_load_component}
mock_get_component.side_effect = lambda name: component_mocks.get(
name, default_component
)
result = config.validate_config(raw_config, {})
# Check for validation errors
assert not result.errors, f"Validation errors: {result.errors}"
# Logger should have been auto-loaded by test_component
assert "logger" in result
assert "test_component" in result
def test_dynamic_auto_load_with_config_param(
mock_get_component: Mock,
fixtures_dir: Path,
default_component: Mock,
) -> None:
"""Test that dynamic AUTO_LOAD evaluates based on configuration."""
CORE.config_path = fixtures_dir / "auto_load_dynamic.yaml"
config_file = fixtures_dir / "auto_load_dynamic.yaml"
raw_config = yaml_util.load_yaml(config_file)
# Track if auto_load was called with config
auto_load_calls = []
def dynamic_auto_load(conf: dict[str, Any]) -> list[str]:
"""Dynamically load components based on config."""
auto_load_calls.append(conf)
component_map = {
"enable_logger": "logger",
"enable_api": "api",
}
return [comp for key, comp in component_map.items() if conf.get(key)]
dynamic_component = Mock(
auto_load=dynamic_auto_load,
is_platform_component=False,
is_platform=False,
multi_conf=False,
multi_conf_no_default=False,
dependencies=[],
conflicts_with=[],
config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA),
)
component_mocks = {"test_component": dynamic_component}
mock_get_component.side_effect = lambda name: component_mocks.get(
name, default_component
)
result = config.validate_config(raw_config, {})
# Check for validation errors
assert not result.errors, f"Validation errors: {result.errors}"
# Verify auto_load was called with the validated config
assert len(auto_load_calls) == 1, "auto_load should be called exactly once"
assert auto_load_calls[0].get("enable_logger") is True
assert auto_load_calls[0].get("enable_api") is False
# Only logger should be auto-loaded (enable_logger=true in YAML)
assert "logger" in result, (
f"Logger not found in result. Result keys: {list(result.keys())}"
)
# API should NOT be auto-loaded (enable_api=false in YAML)
assert "api" not in result
assert "test_component" in result

View File

@@ -10,13 +10,6 @@ from esphome import config, yaml_util
from esphome.core import CORE
@pytest.fixture
def mock_get_component() -> Generator[Mock, None, None]:
"""Fixture for mocking get_component."""
with patch("esphome.config.get_component") as mock_get_component:
yield mock_get_component
@pytest.fixture
def mock_get_platform() -> Generator[Mock, None, None]:
"""Fixture for mocking get_platform."""