[ci] Merge components with different buses to reduce CI time (#11251)
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.14) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Split components for intelligent grouping (40 weighted per batch) (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / CI Status (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04-arm) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04-arm) (push) Has been cancelled
Synchronise Device Classes from Home Assistant / Sync Device Classes (push) Has been cancelled

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
J. Nick Koston
2025-10-15 17:36:03 -10:00
committed by GitHub
parent f2e0a412db
commit 14d76e9e4e
78 changed files with 954 additions and 266 deletions
+47 -15
View File
@@ -379,7 +379,16 @@ jobs:
# Use intelligent splitter that groups components with same bus configs # Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}' components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed='[]'
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
fi
echo "Splitting components intelligently..." echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github) output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
@@ -396,7 +405,7 @@ jobs:
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }} max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix: matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
steps: steps:
@@ -424,18 +433,31 @@ jobs:
- name: Validate and compile components with intelligent grouping - name: Validate and compile components with intelligent grouping
run: | run: |
. venv/bin/activate . venv/bin/activate
# Use /mnt for build files (70GB available vs ~29GB on /)
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
sudo mkdir -p /mnt/platformio
sudo chown $USER:$USER /mnt/platformio
mkdir -p ~/.platformio
sudo mount --bind /mnt/platformio ~/.platformio
# Bind mount test build directory to /mnt # Check if /mnt has more free space than / before bind mounting
sudo mkdir -p /mnt/test_build_components_build # Extract available space in KB for comparison
sudo chown $USER:$USER /mnt/test_build_components_build root_avail=$(df -k / | awk 'NR==2 {print $4}')
mkdir -p tests/test_build_components/build mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
# Only use /mnt if it has more space than /
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
echo "Using /mnt for build files (more space available)"
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
sudo mkdir -p /mnt/platformio
sudo chown $USER:$USER /mnt/platformio
mkdir -p ~/.platformio
sudo mount --bind /mnt/platformio ~/.platformio
# Bind mount test build directory to /mnt
sudo mkdir -p /mnt/test_build_components_build
sudo chown $USER:$USER /mnt/test_build_components_build
mkdir -p tests/test_build_components/build
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
else
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
fi
# Convert space-separated components to comma-separated for Python script # Convert space-separated components to comma-separated for Python script
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',') components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
@@ -448,7 +470,7 @@ jobs:
# - This catches pin conflicts and other issues in directly changed code # - This catches pin conflicts and other issues in directly changed code
# - Grouped tests use --testing-mode to allow config merging (disables some checks) # - Grouped tests use --testing-mode to allow config merging (disables some checks)
# - Dependencies are safe to group since they weren't modified in this PR # - Dependencies are safe to group since they weren't modified in this PR
if [ "${{ github.base_ref }}" = "beta" ] || [ "${{ github.base_ref }}" = "release" ]; then if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed_csv="" directly_changed_csv=""
echo "Testing components: $components_csv" echo "Testing components: $components_csv"
echo "Target branch: ${{ github.base_ref }} - grouping all components" echo "Target branch: ${{ github.base_ref }} - grouping all components"
@@ -459,6 +481,11 @@ jobs:
fi fi
echo "" echo ""
# Show disk space before validation (after bind mounts setup)
echo "Disk space before config validation:"
df -h
echo ""
# Run config validation with grouping and isolation # Run config validation with grouping and isolation
python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv" python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv"
@@ -466,6 +493,11 @@ jobs:
echo "Config validation passed! Starting compilation..." echo "Config validation passed! Starting compilation..."
echo "" echo ""
# Show disk space before compilation
echo "Disk space before compilation:"
df -h
echo ""
# Run compilation with grouping and isolation # Run compilation with grouping and isolation
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv" python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
@@ -474,7 +506,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- common - common
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release' if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+12 -1
View File
@@ -190,7 +190,7 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define("ESPHOME_VARIANT", "ESP8266")
cg.add_define(ThreadModel.SINGLE) cg.add_define(ThreadModel.SINGLE)
cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
conf = config[CONF_FRAMEWORK] conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino") cg.add_platformio_option("framework", "arduino")
@@ -230,6 +230,12 @@ async def to_code(config):
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;` # For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
cg.add_build_flag("-DNEW_OOM_ABORT") cg.add_build_flag("-DNEW_OOM_ABORT")
# In testing mode, fake a larger IRAM to allow linking grouped component tests
# Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
# This is done via a pre-build script that generates a custom linker script
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
@@ -265,3 +271,8 @@ def copy_files():
post_build_file, post_build_file,
CORE.relative_build_path("post_build.py"), CORE.relative_build_path("post_build.py"),
) )
iram_fix_file = dir / "iram_fix.py.script"
copy_file_if_changed(
iram_fix_file,
CORE.relative_build_path("iram_fix.py"),
)
@@ -0,0 +1,44 @@
import os
import re
# pylint: disable=E0602
Import("env") # noqa
def patch_linker_script_after_preprocess(source, target, env):
"""Patch the local linker script after PlatformIO preprocesses it."""
# Check if we're in testing mode by looking for the define
build_flags = env.get("BUILD_FLAGS", [])
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
if not testing_mode:
return
# Get the local linker script path
build_dir = env.subst("$BUILD_DIR")
local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
if not os.path.exists(local_ld):
return
# Read the linker script
with open(local_ld, "r") as f:
content = f.read()
# Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
# The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
updated = re.sub(
r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
r"\g<1>0x200000",
content,
)
if updated != content:
with open(local_ld, "w") as f:
f.write(updated)
print("ESPHome: Patched IRAM size to 2MB for testing mode")
# Hook into the build process right before linking
# This runs after PlatformIO has already preprocessed the linker scripts
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)
+142
View File
@@ -56,6 +56,10 @@ DIRECT_BUS_TYPES = ("i2c", "spi", "uart", "modbus")
# These components can be merged with any other group # These components can be merged with any other group
NO_BUSES_SIGNATURE = "no_buses" NO_BUSES_SIGNATURE = "no_buses"
# Prefix for isolated component signatures
# Isolated components have unique signatures and cannot be merged with others
ISOLATED_SIGNATURE_PREFIX = "isolated_"
# Base bus components - these ARE the bus implementations and should not # Base bus components - these ARE the bus implementations and should not
# be flagged as needing migration since they are the platform/base components # be flagged as needing migration since they are the platform/base components
BASE_BUS_COMPONENTS = { BASE_BUS_COMPONENTS = {
@@ -75,6 +79,7 @@ ISOLATED_COMPONENTS = {
"ethernet": "Defines ethernet: which conflicts with wifi: used by most components", "ethernet": "Defines ethernet: which conflicts with wifi: used by most components",
"ethernet_info": "Related to ethernet component which conflicts with wifi", "ethernet_info": "Related to ethernet component which conflicts with wifi",
"lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs", "lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs",
"mapping": "Uses dict format for image/display sections incompatible with standard list format - ESPHome merge_config cannot handle",
"openthread": "Conflicts with wifi: used by most components", "openthread": "Conflicts with wifi: used by most components",
"openthread_info": "Conflicts with wifi: used by most components", "openthread_info": "Conflicts with wifi: used by most components",
"matrix_keypad": "Needs isolation due to keypad", "matrix_keypad": "Needs isolation due to keypad",
@@ -368,6 +373,143 @@ def analyze_all_components(
return components, non_groupable, direct_bus_components return components, non_groupable, direct_bus_components
@lru_cache(maxsize=256)
def _get_bus_configs(buses: tuple[str, ...]) -> frozenset[tuple[str, str]]:
"""Map bus type to set of configs for that type.
Args:
buses: Tuple of bus package names (e.g., ("uart_9600", "i2c"))
Returns:
Frozenset of (base_type, full_config) tuples
Example: frozenset({("uart", "uart_9600"), ("i2c", "i2c")})
"""
# Split on underscore to get base type: "uart_9600" -> "uart", "i2c" -> "i2c"
return frozenset((bus.split("_", 1)[0], bus) for bus in buses)
@lru_cache(maxsize=1024)
def are_buses_compatible(buses1: tuple[str, ...], buses2: tuple[str, ...]) -> bool:
"""Check if two bus tuples are compatible for merging.
Two bus lists are compatible if they don't have conflicting configurations
for the same bus type. For example:
- ("ble", "uart") and ("i2c",) are compatible (different buses)
- ("uart_9600",) and ("uart_19200",) are NOT compatible (same bus, different configs)
- ("uart_9600",) and ("uart_9600",) are compatible (same bus, same config)
Args:
buses1: First tuple of bus package names
buses2: Second tuple of bus package names
Returns:
True if buses can be merged without conflicts
"""
configs1 = _get_bus_configs(buses1)
configs2 = _get_bus_configs(buses2)
# Group configs by base type
bus_types1: dict[str, set[str]] = {}
for base_type, full_config in configs1:
if base_type not in bus_types1:
bus_types1[base_type] = set()
bus_types1[base_type].add(full_config)
bus_types2: dict[str, set[str]] = {}
for base_type, full_config in configs2:
if base_type not in bus_types2:
bus_types2[base_type] = set()
bus_types2[base_type].add(full_config)
# Check for conflicts: same bus type with different configs
for bus_type, configs in bus_types1.items():
if bus_type not in bus_types2:
continue # No conflict - different bus types
# Same bus type - check if configs match
if configs != bus_types2[bus_type]:
return False # Conflict - same bus type, different configs
return True # No conflicts found
def merge_compatible_bus_groups(
grouped_components: dict[tuple[str, str], list[str]],
) -> dict[tuple[str, str], list[str]]:
"""Merge groups with compatible (non-conflicting) buses.
This function takes groups keyed by (platform, bus_signature) and merges
groups that share the same platform and have compatible bus configurations.
Two groups can be merged if their buses don't conflict - meaning they don't
have different configurations for the same bus type.
For example:
- ["ble"] + ["uart"] = compatible (different buses)
- ["uart_9600"] + ["uart_19200"] = incompatible (same bus, different configs)
- ["uart_9600"] + ["uart_9600"] = compatible (same bus, same config)
Args:
grouped_components: Dictionary mapping (platform, signature) to list of component names
Returns:
Dictionary with same structure but with compatible groups merged
"""
merged_groups: dict[tuple[str, str], list[str]] = {}
processed_keys: set[tuple[str, str]] = set()
for (platform1, sig1), comps1 in sorted(grouped_components.items()):
if (platform1, sig1) in processed_keys:
continue
# Skip NO_BUSES_SIGNATURE - kept separate for flexible batch distribution
# These components have no bus requirements and can be added to any batch
# as "fillers" for load balancing across CI runners
if sig1 == NO_BUSES_SIGNATURE:
merged_groups[(platform1, sig1)] = comps1
processed_keys.add((platform1, sig1))
continue
# Skip isolated components - they can't be merged with others
if sig1.startswith(ISOLATED_SIGNATURE_PREFIX):
merged_groups[(platform1, sig1)] = comps1
processed_keys.add((platform1, sig1))
continue
# Start with this group's components
merged_comps: list[str] = list(comps1)
merged_sig: str = sig1
processed_keys.add((platform1, sig1))
# Get buses for this group as tuple for caching
buses1: tuple[str, ...] = tuple(sorted(sig1.split("+")))
# Try to merge with other groups on same platform
for (platform2, sig2), comps2 in sorted(grouped_components.items()):
if (platform2, sig2) in processed_keys:
continue
if platform2 != platform1:
continue # Different platforms can't be merged
if sig2 == NO_BUSES_SIGNATURE:
continue # Keep separate for flexible batch distribution
if sig2.startswith(ISOLATED_SIGNATURE_PREFIX):
continue # Isolated components can't be merged
# Check if buses are compatible
buses2: tuple[str, ...] = tuple(sorted(sig2.split("+")))
if are_buses_compatible(buses1, buses2):
# Compatible! Merge this group
merged_comps.extend(comps2)
processed_keys.add((platform2, sig2))
# Update merged signature to include all unique buses
all_buses: set[str] = set(buses1) | set(buses2)
merged_sig = "+".join(sorted(all_buses))
buses1 = tuple(sorted(all_buses)) # Update for next iteration
# Store merged group
merged_groups[(platform1, merged_sig)] = merged_comps
return merged_groups
def create_grouping_signature( def create_grouping_signature(
platform_buses: dict[str, list[str]], platform: str platform_buses: dict[str, list[str]], platform: str
) -> str: ) -> str:
+32 -7
View File
@@ -185,17 +185,20 @@ def main():
"-c", "-c",
"--changed", "--changed",
action="store_true", action="store_true",
help="List all components required for testing based on changes (includes dependencies)", help="List all components with dependencies (used by clang-tidy). "
"When base test infrastructure changes, returns ALL components.",
) )
parser.add_argument( parser.add_argument(
"--changed-direct", "--changed-direct",
action="store_true", action="store_true",
help="List only directly changed components (without dependencies)", help="List only directly changed components, ignoring infrastructure changes "
"(used by CI for isolation decisions)",
) )
parser.add_argument( parser.add_argument(
"--changed-with-deps", "--changed-with-deps",
action="store_true", action="store_true",
help="Output JSON with both directly changed and all changed components", help="Output JSON with both directly changed and all changed components "
"(with dependencies), ignoring infrastructure changes (used by CI for test determination)",
) )
parser.add_argument( parser.add_argument(
"-b", "--branch", help="Branch to compare changed files against" "-b", "--branch", help="Branch to compare changed files against"
@@ -213,12 +216,34 @@ def main():
# When --changed* is passed, only get the changed files # When --changed* is passed, only get the changed files
changed = changed_files(args.branch) changed = changed_files(args.branch)
# If any base test file(s) changed, there's no need to filter out components # If any base test file(s) changed, we need to check all components
if any("tests/test_build_components" in file for file in changed): # BUT only for --changed (used by clang-tidy for comprehensive checking)
# Need to get all component files # NOT for --changed-direct or --changed-with-deps (used by CI for targeted testing)
#
# Flag usage:
# - --changed: Used by clang-tidy (script/helpers.py get_changed_components)
# Returns: All components with dependencies when base test files change
# Reason: Test infrastructure changes may affect any component
#
# - --changed-direct: Used by CI isolation (script/determine-jobs.py)
# Returns: Only components with actual code changes (not infrastructure)
# Reason: Only directly changed components need isolated testing
#
# - --changed-with-deps: Used by CI test determination (script/determine-jobs.py)
# Returns: Components with code changes + their dependencies (not infrastructure)
# Reason: CI needs to test changed components and their dependents
base_test_changed = any(
"tests/test_build_components" in file for file in changed
)
if base_test_changed and not args.changed_direct and not args.changed_with_deps:
# Base test infrastructure changed - load all component files
# This is for --changed (clang-tidy) which needs comprehensive checking
files = get_all_component_files() files = get_all_component_files()
else: else:
# Only look at changed component files # Only look at changed component files (ignore infrastructure changes)
# For --changed-direct: only actual component code changes matter (for isolation)
# For --changed-with-deps: only actual component code changes matter (for testing)
files = [f for f in changed if filter_component_files(f)] files = [f for f in changed if filter_component_files(f)]
else: else:
# Get all component files # Get all component files
+100 -36
View File
@@ -16,6 +16,7 @@ The merger handles:
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from functools import lru_cache
from pathlib import Path from pathlib import Path
import re import re
import sys import sys
@@ -28,6 +29,10 @@ from esphome import yaml_util
from esphome.config_helpers import merge_config from esphome.config_helpers import merge_config
from script.analyze_component_buses import PACKAGE_DEPENDENCIES, get_common_bus_packages from script.analyze_component_buses import PACKAGE_DEPENDENCIES, get_common_bus_packages
# Prefix for dependency markers in package tracking
# Used to mark packages that are included transitively (e.g., uart via modbus)
DEPENDENCY_MARKER_PREFIX = "_dep_"
def load_yaml_file(yaml_file: Path) -> dict: def load_yaml_file(yaml_file: Path) -> dict:
"""Load YAML file using ESPHome's YAML loader. """Load YAML file using ESPHome's YAML loader.
@@ -44,6 +49,34 @@ def load_yaml_file(yaml_file: Path) -> dict:
return yaml_util.load_yaml(yaml_file) return yaml_util.load_yaml(yaml_file)
@lru_cache(maxsize=256)
def get_component_packages(
component_name: str, platform: str, tests_dir_str: str
) -> dict:
"""Get packages dict from a component's test file with caching.
This function is cached to avoid re-loading and re-parsing the same file
multiple times when extracting packages during cross-bus merging.
Args:
component_name: Name of the component
platform: Platform name (e.g., "esp32-idf")
tests_dir_str: String path to tests/components directory (must be string for cache hashability)
Returns:
Dictionary with 'packages' key containing the raw packages dict from the YAML,
or empty dict if no packages section exists
"""
tests_dir = Path(tests_dir_str)
test_file = tests_dir / component_name / f"test.{platform}.yaml"
comp_data = load_yaml_file(test_file)
if "packages" not in comp_data or not isinstance(comp_data["packages"], dict):
return {}
return comp_data["packages"]
def extract_packages_from_yaml(data: dict) -> dict[str, str]: def extract_packages_from_yaml(data: dict) -> dict[str, str]:
"""Extract COMMON BUS package includes from parsed YAML. """Extract COMMON BUS package includes from parsed YAML.
@@ -82,7 +115,7 @@ def extract_packages_from_yaml(data: dict) -> dict[str, str]:
if dep not in common_bus_packages: if dep not in common_bus_packages:
continue continue
# Mark as included via dependency # Mark as included via dependency
packages[f"_dep_{dep}"] = f"(included via {name})" packages[f"{DEPENDENCY_MARKER_PREFIX}{dep}"] = f"(included via {name})"
return packages return packages
@@ -195,6 +228,9 @@ def merge_component_configs(
# Start with empty config # Start with empty config
merged_config_data = {} merged_config_data = {}
# Convert tests_dir to string for caching
tests_dir_str = str(tests_dir)
# Process each component # Process each component
for comp_name in component_names: for comp_name in component_names:
comp_dir = tests_dir / comp_name comp_dir = tests_dir / comp_name
@@ -206,26 +242,29 @@ def merge_component_configs(
# Load the component's test file # Load the component's test file
comp_data = load_yaml_file(test_file) comp_data = load_yaml_file(test_file)
# Validate packages are compatible # Merge packages from all components (cross-bus merging)
# Components with no packages (no_buses) can merge with any group # Components can have different packages (e.g., one with ble, another with uart)
# as long as they don't conflict (checked by are_buses_compatible before calling this)
comp_packages = extract_packages_from_yaml(comp_data) comp_packages = extract_packages_from_yaml(comp_data)
if all_packages is None: if all_packages is None:
# First component - set the baseline # First component - initialize package dict
all_packages = comp_packages all_packages = comp_packages if comp_packages else {}
elif not comp_packages: elif comp_packages:
# This component has no packages (no_buses) - it can merge with any group # Merge packages - combine all unique package types
pass # If both have the same package type, verify they're identical
elif not all_packages: for pkg_name, pkg_config in comp_packages.items():
# Previous components had no packages, but this one does - adopt these packages if pkg_name in all_packages:
all_packages = comp_packages # Same package type - verify config matches
elif comp_packages != all_packages: if all_packages[pkg_name] != pkg_config:
# Both have packages but they differ - this is an error raise ValueError(
raise ValueError( f"Component {comp_name} has conflicting config for package '{pkg_name}'. "
f"Component {comp_name} has different packages than previous components. " f"Expected: {all_packages[pkg_name]}, Got: {pkg_config}. "
f"Expected: {all_packages}, Got: {comp_packages}. " f"Components with conflicting bus configs cannot be merged."
f"All components must use the same common bus configs to be merged." )
) else:
# New package type - add it
all_packages[pkg_name] = pkg_config
# Handle $component_dir by replacing with absolute path # Handle $component_dir by replacing with absolute path
# This allows components that use local file references to be grouped # This allows components that use local file references to be grouped
@@ -287,26 +326,51 @@ def merge_component_configs(
# merge_config handles list merging with ID-based deduplication automatically # merge_config handles list merging with ID-based deduplication automatically
merged_config_data = merge_config(merged_config_data, comp_data) merged_config_data = merge_config(merged_config_data, comp_data)
# Add packages back (only once, since they're identical) # Add merged packages back (union of all component packages)
# IMPORTANT: Only re-add common bus packages (spi, i2c, uart, etc.) # IMPORTANT: Only include common bus packages (spi, i2c, uart, etc.)
# Do NOT re-add component-specific packages as they contain unprefixed $component_dir refs # Do NOT re-add component-specific packages as they contain unprefixed $component_dir refs
if all_packages: if all_packages:
first_comp_data = load_yaml_file( # Build packages dict from merged all_packages
tests_dir / component_names[0] / f"test.{platform}.yaml" # all_packages is a dict mapping package_name -> str(package_value)
) # We need to reconstruct the actual package values by loading them from any component
if "packages" in first_comp_data and isinstance( # Since packages with the same name must have identical configs (verified above),
first_comp_data["packages"], dict # we can load the package value from the first component that has each package
): common_bus_packages = get_common_bus_packages()
# Filter to only include common bus packages merged_packages: dict[str, Any] = {}
# Only dict format can contain common bus packages
common_bus_packages = get_common_bus_packages() # Collect packages that are included as dependencies
filtered_packages = { # If modbus is present, uart is included via modbus.packages.uart
name: value packages_to_skip: set[str] = set()
for name, value in first_comp_data["packages"].items() for pkg_name in all_packages:
if name in common_bus_packages if pkg_name.startswith(DEPENDENCY_MARKER_PREFIX):
} # Extract the actual package name (remove _dep_ prefix)
if filtered_packages: dep_name = pkg_name[len(DEPENDENCY_MARKER_PREFIX) :]
merged_config_data["packages"] = filtered_packages packages_to_skip.add(dep_name)
for pkg_name in all_packages:
# Skip dependency markers
if pkg_name.startswith(DEPENDENCY_MARKER_PREFIX):
continue
# Skip non-common-bus packages
if pkg_name not in common_bus_packages:
continue
# Skip packages that are included as dependencies of other packages
# This prevents duplicate definitions (e.g., uart via modbus + uart separately)
if pkg_name in packages_to_skip:
continue
# Find a component that has this package and extract its value
# Uses cached lookup to avoid re-loading the same files
for comp_name in component_names:
comp_packages = get_component_packages(
comp_name, platform, tests_dir_str
)
if pkg_name in comp_packages:
merged_packages[pkg_name] = comp_packages[pkg_name]
break
if merged_packages:
merged_config_data["packages"] = merged_packages
# Deduplicate items with same ID (keeps first occurrence) # Deduplicate items with same ID (keeps first occurrence)
merged_config_data = deduplicate_by_id(merged_config_data) merged_config_data = deduplicate_by_id(merged_config_data)
+82 -13
View File
@@ -22,9 +22,11 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
from script.analyze_component_buses import ( from script.analyze_component_buses import (
ISOLATED_COMPONENTS, ISOLATED_COMPONENTS,
ISOLATED_SIGNATURE_PREFIX,
NO_BUSES_SIGNATURE, NO_BUSES_SIGNATURE,
analyze_all_components, analyze_all_components,
create_grouping_signature, create_grouping_signature,
merge_compatible_bus_groups,
) )
# Weighting for batch creation # Weighting for batch creation
@@ -33,6 +35,10 @@ from script.analyze_component_buses import (
ISOLATED_WEIGHT = 10 ISOLATED_WEIGHT = 10
GROUPABLE_WEIGHT = 1 GROUPABLE_WEIGHT = 1
# Platform used for batching (platform-agnostic batching)
# Batches are split across CI runners and each runner tests all platforms
ALL_PLATFORMS = "all"
def has_test_files(component_name: str, tests_dir: Path) -> bool: def has_test_files(component_name: str, tests_dir: Path) -> bool:
"""Check if a component has test files. """Check if a component has test files.
@@ -57,7 +63,7 @@ def create_intelligent_batches(
tests_dir: Path, tests_dir: Path,
batch_size: int = 40, batch_size: int = 40,
directly_changed: set[str] | None = None, directly_changed: set[str] | None = None,
) -> list[list[str]]: ) -> tuple[list[list[str]], dict[tuple[str, str], list[str]]]:
"""Create batches optimized for component grouping. """Create batches optimized for component grouping.
Args: Args:
@@ -67,7 +73,9 @@ def create_intelligent_batches(
directly_changed: Set of directly changed components (for logging only) directly_changed: Set of directly changed components (for logging only)
Returns: Returns:
List of component batches (lists of component names) Tuple of (batches, signature_groups) where:
- batches: List of component batches (lists of component names)
- signature_groups: Dict mapping (platform, signature) to component lists
""" """
# Filter out components without test files # Filter out components without test files
# Platform components like 'climate' and 'climate_ir' don't have test files # Platform components like 'climate' and 'climate_ir' don't have test files
@@ -91,8 +99,9 @@ def create_intelligent_batches(
# Group components by their bus signature ONLY (ignore platform) # Group components by their bus signature ONLY (ignore platform)
# All platforms will be tested by test_build_components.py for each batch # All platforms will be tested by test_build_components.py for each batch
# Key: signature, Value: list of components # Key: (platform, signature), Value: list of components
signature_groups: dict[str, list[str]] = defaultdict(list) # We use ALL_PLATFORMS since batching is platform-agnostic
signature_groups: dict[tuple[str, str], list[str]] = defaultdict(list)
for component in components_with_tests: for component in components_with_tests:
# Components that can't be grouped get unique signatures # Components that can't be grouped get unique signatures
@@ -107,7 +116,9 @@ def create_intelligent_batches(
or (directly_changed and component in directly_changed) or (directly_changed and component in directly_changed)
) )
if is_isolated: if is_isolated:
signature_groups[f"isolated_{component}"].append(component) signature_groups[
(ALL_PLATFORMS, f"{ISOLATED_SIGNATURE_PREFIX}{component}")
].append(component)
continue continue
# Get signature from any platform (they should all have the same buses) # Get signature from any platform (they should all have the same buses)
@@ -117,11 +128,17 @@ def create_intelligent_batches(
if buses: if buses:
signature = create_grouping_signature({platform: buses}, platform) signature = create_grouping_signature({platform: buses}, platform)
# Group by signature only - platform doesn't matter for batching # Group by signature only - platform doesn't matter for batching
signature_groups[signature].append(component) # Use ALL_PLATFORMS since we're batching across all platforms
signature_groups[(ALL_PLATFORMS, signature)].append(component)
break # Only use first platform for grouping break # Only use first platform for grouping
else: else:
# No buses found for any platform - can be grouped together # No buses found for any platform - can be grouped together
signature_groups[NO_BUSES_SIGNATURE].append(component) signature_groups[(ALL_PLATFORMS, NO_BUSES_SIGNATURE)].append(component)
# Merge compatible bus groups (cross-bus optimization)
# This allows components with different buses (ble + uart) to be batched together
# improving the efficiency of test_build_components.py grouping
signature_groups = merge_compatible_bus_groups(signature_groups)
# Create batches by keeping signature groups together # Create batches by keeping signature groups together
# Components with the same signature stay in the same batches # Components with the same signature stay in the same batches
@@ -132,8 +149,8 @@ def create_intelligent_batches(
# 2. Sort groupable signatures by size (largest first) # 2. Sort groupable signatures by size (largest first)
# 3. "no_buses" components CAN be grouped together # 3. "no_buses" components CAN be grouped together
def sort_key(item): def sort_key(item):
signature, components = item (_platform, signature), components = item
is_isolated = signature.startswith("isolated_") is_isolated = signature.startswith(ISOLATED_SIGNATURE_PREFIX)
# Put "isolated_*" last (1), groupable first (0) # Put "isolated_*" last (1), groupable first (0)
# Within each category, sort by size (largest first) # Within each category, sort by size (largest first)
return (is_isolated, -len(components)) return (is_isolated, -len(components))
@@ -149,8 +166,8 @@ def create_intelligent_batches(
current_batch = [] current_batch = []
current_weight = 0 current_weight = 0
for signature, group_components in sorted_groups: for (_platform, signature), group_components in sorted_groups:
is_isolated = signature.startswith("isolated_") is_isolated = signature.startswith(ISOLATED_SIGNATURE_PREFIX)
weight_per_component = ISOLATED_WEIGHT if is_isolated else GROUPABLE_WEIGHT weight_per_component = ISOLATED_WEIGHT if is_isolated else GROUPABLE_WEIGHT
for component in group_components: for component in group_components:
@@ -169,7 +186,7 @@ def create_intelligent_batches(
if current_batch: if current_batch:
batches.append(current_batch) batches.append(current_batch)
return batches return batches, signature_groups
def main() -> int: def main() -> int:
@@ -231,7 +248,7 @@ def main() -> int:
return 1 return 1
# Create intelligent batches # Create intelligent batches
batches = create_intelligent_batches( batches, signature_groups = create_intelligent_batches(
components=components, components=components,
tests_dir=args.tests_dir, tests_dir=args.tests_dir,
batch_size=args.batch_size, batch_size=args.batch_size,
@@ -256,6 +273,58 @@ def main() -> int:
# Re-analyze to get isolated component counts for summary # Re-analyze to get isolated component counts for summary
_, non_groupable, _ = analyze_all_components(args.tests_dir) _, non_groupable, _ = analyze_all_components(args.tests_dir)
# Show grouping details
print("\n=== Component Grouping Details ===", file=sys.stderr)
# Sort groups by signature for readability
groupable_groups = []
isolated_groups = []
for (platform, signature), group_comps in sorted(signature_groups.items()):
if signature.startswith(ISOLATED_SIGNATURE_PREFIX):
isolated_groups.append((signature, group_comps))
else:
groupable_groups.append((signature, group_comps))
if groupable_groups:
print(
f"\nGroupable signatures ({len(groupable_groups)} merged groups after cross-bus optimization):",
file=sys.stderr,
)
for signature, group_comps in sorted(
groupable_groups, key=lambda x: (-len(x[1]), x[0])
):
# Check if this is a merged signature (contains +)
is_merged = "+" in signature and signature != NO_BUSES_SIGNATURE
# Special handling for no_buses components
if signature == NO_BUSES_SIGNATURE:
print(
f" [{signature}]: {len(group_comps)} components (used as fillers across batches)",
file=sys.stderr,
)
else:
merge_indicator = " [MERGED]" if is_merged else ""
print(
f" [{signature}]{merge_indicator}: {len(group_comps)} components",
file=sys.stderr,
)
# Show first few components as examples
examples = ", ".join(sorted(group_comps)[:8])
if len(group_comps) > 8:
examples += f", ... (+{len(group_comps) - 8} more)"
print(f"{examples}", file=sys.stderr)
if isolated_groups:
print(
f"\nIsolated components ({len(isolated_groups)} components - tested individually):",
file=sys.stderr,
)
isolated_names = sorted(
[comp for _, comps in isolated_groups for comp in comps]
)
# Group isolated components for compact display
for i in range(0, len(isolated_names), 10):
chunk = isolated_names[i : i + 10]
print(f" {', '.join(chunk)}", file=sys.stderr)
# Count isolated vs groupable components # Count isolated vs groupable components
all_batched_components = [comp for batch in batches for comp in batch] all_batched_components = [comp for batch in batches for comp in batch]
isolated_count = sum( isolated_count = sum(
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
substitutions: substitutions:
irq0_pin: GPIO13 irq0_pin: GPIO0
irq1_pin: GPIO15 irq1_pin: GPIO15
reset_pin: GPIO16 reset_pin: GPIO16
+3
View File
@@ -4,10 +4,13 @@ sensor:
irq_pin: ${irq_pin} irq_pin: ${irq_pin}
voltage: voltage:
name: ADE7953 Voltage name: ADE7953 Voltage
id: ade7953_i2c_voltage
current_a: current_a:
name: ADE7953 Current A name: ADE7953 Current A
id: ade7953_i2c_current_a
current_b: current_b:
name: ADE7953 Current B name: ADE7953 Current B
id: ade7953_i2c_current_b
power_factor_a: power_factor_a:
name: ADE7953 Power Factor A name: ADE7953 Power Factor A
power_factor_b: power_factor_b:
+3 -3
View File
@@ -4,13 +4,13 @@ sensor:
irq_pin: ${irq_pin} irq_pin: ${irq_pin}
voltage: voltage:
name: ADE7953 Voltage name: ADE7953 Voltage
id: ade7953_voltage id: ade7953_spi_voltage
current_a: current_a:
name: ADE7953 Current A name: ADE7953 Current A
id: ade7953_current_a id: ade7953_spi_current_a
current_b: current_b:
name: ADE7953 Current B name: ADE7953 Current B
id: ade7953_current_b id: ade7953_spi_current_b
power_factor_a: power_factor_a:
name: ADE7953 Power Factor A name: ADE7953 Power Factor A
power_factor_b: power_factor_b:
+3
View File
@@ -1,13 +1,16 @@
as3935_i2c: as3935_i2c:
id: as3935_i2c_id
i2c_id: i2c_bus i2c_id: i2c_bus
irq_pin: ${irq_pin} irq_pin: ${irq_pin}
binary_sensor: binary_sensor:
- platform: as3935 - platform: as3935
as3935_id: as3935_i2c_id
name: Storm Alert name: Storm Alert
sensor: sensor:
- platform: as3935 - platform: as3935
as3935_id: as3935_i2c_id
lightning_energy: lightning_energy:
name: Lightning Energy name: Lightning Energy
distance: distance:
+3
View File
@@ -1,13 +1,16 @@
as3935_spi: as3935_spi:
id: as3935_spi_id
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
irq_pin: ${irq_pin} irq_pin: ${irq_pin}
binary_sensor: binary_sensor:
- platform: as3935 - platform: as3935
as3935_id: as3935_spi_id
name: Storm Alert name: Storm Alert
sensor: sensor:
- platform: as3935 - platform: as3935
as3935_id: as3935_spi_id
lightning_energy: lightning_energy:
name: Lightning Energy name: Lightning Energy
distance: distance:
+2 -2
View File
@@ -1,7 +1,7 @@
display: display:
- platform: ssd1306_i2c - platform: ssd1306_i2c
i2c_id: i2c_bus i2c_id: i2c_bus
id: ssd1306_display id: ssd1306_i2c_display
model: SSD1306_128X64 model: SSD1306_128X64
reset_pin: 19 reset_pin: 19
pages: pages:
@@ -13,6 +13,6 @@ touchscreen:
- platform: axs15231 - platform: axs15231
i2c_id: i2c_bus i2c_id: i2c_bus
id: axs15231_touchscreen id: axs15231_touchscreen
display: ssd1306_display display: ssd1306_i2c_display
interrupt_pin: 20 interrupt_pin: 20
reset_pin: 18 reset_pin: 18
+3 -3
View File
@@ -3,12 +3,12 @@ sensor:
i2c_id: i2c_bus i2c_id: i2c_bus
address: 0x76 address: 0x76
temperature: temperature:
id: bme280_temperature id: bme280_i2c_temperature
name: BME280 Temperature name: BME280 Temperature
humidity: humidity:
id: bme280_humidity id: bme280_i2c_humidity
name: BME280 Humidity name: BME280 Humidity
pressure: pressure:
id: bme280_pressure id: bme280_i2c_pressure
name: BME280 Pressure name: BME280 Pressure
update_interval: 15s update_interval: 15s
+3 -3
View File
@@ -2,12 +2,12 @@ sensor:
- platform: bme280_spi - platform: bme280_spi
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
temperature: temperature:
id: bme280_temperature id: bme280_spi_temperature
name: BME280 Temperature name: BME280 Temperature
humidity: humidity:
id: bme280_humidity id: bme280_spi_humidity
name: BME280 Humidity name: BME280 Humidity
pressure: pressure:
id: bme280_pressure id: bme280_spi_pressure
name: BME280 Pressure name: BME280 Pressure
update_interval: 15s update_interval: 15s
+2 -2
View File
@@ -3,10 +3,10 @@ sensor:
i2c_id: i2c_bus i2c_id: i2c_bus
address: 0x77 address: 0x77
temperature: temperature:
id: bmp280_temperature id: bmp280_i2c_temperature
name: Outside Temperature name: Outside Temperature
pressure: pressure:
name: Outside Pressure name: Outside Pressure
id: bmp280_pressure id: bmp280_i2c_pressure
iir_filter: 16x iir_filter: 16x
update_interval: 15s update_interval: 15s
+2 -2
View File
@@ -2,10 +2,10 @@ sensor:
- platform: bmp280_spi - platform: bmp280_spi
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
temperature: temperature:
id: bmp280_temperature id: bmp280_spi_temperature
name: Outside Temperature name: Outside Temperature
pressure: pressure:
name: Outside Pressure name: Outside Pressure
id: bmp280_pressure id: bmp280_spi_pressure
iir_filter: 16x iir_filter: 16x
update_interval: 15s update_interval: 15s
+2
View File
@@ -3,8 +3,10 @@ sensor:
i2c_id: i2c_bus i2c_id: i2c_bus
address: 0x77 address: 0x77
temperature: temperature:
id: bmp3xx_i2c_temperature
name: BMP Temperature name: BMP Temperature
oversampling: 16x oversampling: 16x
pressure: pressure:
id: bmp3xx_i2c_pressure
name: BMP Pressure name: BMP Pressure
iir_filter: 2X iir_filter: 2X
+2
View File
@@ -2,8 +2,10 @@ sensor:
- platform: bmp3xx_spi - platform: bmp3xx_spi
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
temperature: temperature:
id: bmp3xx_spi_temperature
name: BMP Temperature name: BMP Temperature
oversampling: 16x oversampling: 16x
pressure: pressure:
id: bmp3xx_spi_pressure
name: BMP Pressure name: BMP Pressure
iir_filter: 2X iir_filter: 2X
+1 -1
View File
@@ -1,4 +1,4 @@
packages: packages:
camera: !include ../../test_build_components/common/camera/esp32-idf.yaml i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml
<<: !include common.yaml <<: !include common.yaml
@@ -1,4 +1,4 @@
packages: packages:
camera: !include ../../test_build_components/common/camera/esp32-idf.yaml i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml
<<: !include common.yaml <<: !include common.yaml
@@ -4,6 +4,7 @@ packages:
display: display:
- platform: ili9xxx - platform: ili9xxx
spi_id: spi_bus
id: ili9xxx_display id: ili9xxx_display
model: GC9A01A model: GC9A01A
invert_colors: True invert_colors: True
@@ -16,5 +17,6 @@ display:
touchscreen: touchscreen:
- platform: chsc6x - platform: chsc6x
i2c_id: i2c_bus
display: ili9xxx_display display: ili9xxx_display
interrupt_pin: 20 interrupt_pin: 20
+2 -2
View File
@@ -1,7 +1,7 @@
display: display:
- platform: ssd1306_i2c - platform: ssd1306_i2c
i2c_id: i2c_bus i2c_id: i2c_bus
id: ssd1306_display id: ssd1306_i2c_display
model: SSD1306_128X64 model: SSD1306_128X64
reset_pin: ${display_reset_pin} reset_pin: ${display_reset_pin}
pages: pages:
@@ -15,7 +15,7 @@ touchscreen:
id: ektf2232_touchscreen id: ektf2232_touchscreen
interrupt_pin: ${interrupt_pin} interrupt_pin: ${interrupt_pin}
reset_pin: ${touch_reset_pin} reset_pin: ${touch_reset_pin}
display: ssd1306_display display: ssd1306_i2c_display
on_touch: on_touch:
- logger.log: - logger.log:
format: Touch at (%d, %d) format: Touch at (%d, %d)
+3
View File
@@ -3,8 +3,11 @@ sensor:
i2c_id: i2c_bus i2c_id: i2c_bus
address: 0x53 address: 0x53
eco2: eco2:
id: ens160_i2c_eco2
name: "ENS160 eCO2" name: "ENS160 eCO2"
tvoc: tvoc:
id: ens160_i2c_tvoc
name: "ENS160 Total Volatile Organic Compounds" name: "ENS160 Total Volatile Organic Compounds"
aqi: aqi:
id: ens160_i2c_aqi
name: "ENS160 Air Quality Index" name: "ENS160 Air Quality Index"
+3
View File
@@ -2,8 +2,11 @@ sensor:
- platform: ens160_spi - platform: ens160_spi
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
eco2: eco2:
id: ens160_spi_eco2
name: "ENS160 eCO2" name: "ENS160 eCO2"
tvoc: tvoc:
id: ens160_spi_tvoc
name: "ENS160 Total Volatile Organic Compounds" name: "ENS160 Total Volatile Organic Compounds"
aqi: aqi:
id: ens160_spi_aqi
name: "ENS160 Air Quality Index" name: "ENS160 Air Quality Index"
@@ -1,4 +1,4 @@
packages: packages:
camera: !include ../../test_build_components/common/camera/esp32-idf.yaml i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml
<<: !include common.yaml <<: !include common.yaml
@@ -1,4 +1,4 @@
packages: packages:
camera: !include ../../test_build_components/common/camera/esp32-idf.yaml i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml
<<: !include common.yaml <<: !include common.yaml
+1
View File
@@ -49,6 +49,7 @@ font:
display: display:
- platform: ssd1306_i2c - platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_display id: ssd1306_display
model: SSD1306_128X64 model: SSD1306_128X64
reset_pin: ${display_reset_pin} reset_pin: ${display_reset_pin}
@@ -1,5 +1,5 @@
substitutions: substitutions:
interrupt_pin: GPIO12 interrupt_pin: GPIO0
reset_pin: GPIO16 reset_pin: GPIO16
packages: packages:
+1
View File
@@ -11,6 +11,7 @@ graph:
display: display:
- platform: ssd1306_i2c - platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_display id: ssd1306_display
model: SSD1306_128X64 model: SSD1306_128X64
reset_pin: ${reset_pin} reset_pin: ${reset_pin}
@@ -1,6 +1,6 @@
display: display:
- platform: ssd1306_i2c - platform: ssd1306_i2c
id: ssd1306_display id: ssd1306_i2c_display
model: SSD1306_128X64 model: SSD1306_128X64
reset_pin: ${reset_pin} reset_pin: ${reset_pin}
pages: pages:
@@ -36,7 +36,7 @@ switch:
graphical_display_menu: graphical_display_menu:
id: test_graphical_display_menu id: test_graphical_display_menu
display: ssd1306_display display: ssd1306_i2c_display
font: roboto font: roboto
active: false active: false
mode: rotary mode: rotary
+2 -2
View File
@@ -1,7 +1,7 @@
display: display:
- platform: ssd1306_i2c - platform: ssd1306_i2c
i2c_id: i2c_bus i2c_id: i2c_bus
id: ssd1306_display id: ssd1306_i2c_display
model: SSD1306_128X64 model: SSD1306_128X64
reset_pin: ${display_reset_pin} reset_pin: ${display_reset_pin}
pages: pages:
@@ -13,7 +13,7 @@ touchscreen:
- platform: gt911 - platform: gt911
i2c_id: i2c_bus i2c_id: i2c_bus
id: gt911_touchscreen id: gt911_touchscreen
display: ssd1306_display display: ssd1306_i2c_display
interrupt_pin: ${interrupt_pin} interrupt_pin: ${interrupt_pin}
reset_pin: ${reset_pin} reset_pin: ${reset_pin}
+2 -2
View File
@@ -1,5 +1,5 @@
substitutions: substitutions:
clk_pin: GPIO4 clk_pin: GPIO0
dout_pin: GPIO5 dout_pin: GPIO2
<<: !include common.yaml <<: !include common.yaml
+18 -6
View File
@@ -7,9 +7,21 @@ sensor:
max_current: 40 A max_current: 40 A
adc_range: 1 adc_range: 1
temperature_coefficient: 50 temperature_coefficient: 50
shunt_voltage: "INA2xx Shunt Voltage" shunt_voltage:
bus_voltage: "INA2xx Bus Voltage" id: ina2xx_i2c_shunt_voltage
current: "INA2xx Current" name: "INA2xx Shunt Voltage"
power: "INA2xx Power" bus_voltage:
energy: "INA2xx Energy" id: ina2xx_i2c_bus_voltage
charge: "INA2xx Charge" name: "INA2xx Bus Voltage"
current:
id: ina2xx_i2c_current
name: "INA2xx Current"
power:
id: ina2xx_i2c_power
name: "INA2xx Power"
energy:
id: ina2xx_i2c_energy
name: "INA2xx Energy"
charge:
id: ina2xx_i2c_charge
name: "INA2xx Charge"
+18 -6
View File
@@ -6,9 +6,21 @@ sensor:
max_current: 40 A max_current: 40 A
adc_range: 1 adc_range: 1
temperature_coefficient: 50 temperature_coefficient: 50
shunt_voltage: "INA2xx Shunt Voltage" shunt_voltage:
bus_voltage: "INA2xx Bus Voltage" id: ina2xx_spi_shunt_voltage
current: "INA2xx Current" name: "INA2xx Shunt Voltage"
power: "INA2xx Power" bus_voltage:
energy: "INA2xx Energy" id: ina2xx_spi_bus_voltage
charge: "INA2xx Charge" name: "INA2xx Bus Voltage"
current:
id: ina2xx_spi_current
name: "INA2xx Current"
power:
id: ina2xx_spi_power
name: "INA2xx Power"
energy:
id: ina2xx_spi_energy
name: "INA2xx Energy"
charge:
id: ina2xx_spi_charge
name: "INA2xx Charge"
+2 -2
View File
@@ -1,7 +1,7 @@
display: display:
- platform: ssd1306_i2c - platform: ssd1306_i2c
i2c_id: i2c_bus i2c_id: i2c_bus
id: ssd1306_display id: ssd1306_i2c_display
model: SSD1306_128X64 model: SSD1306_128X64
reset_pin: ${reset_pin} reset_pin: ${reset_pin}
pages: pages:
@@ -14,7 +14,7 @@ touchscreen:
i2c_id: i2c_bus i2c_id: i2c_bus
id: lilygo_touchscreen id: lilygo_touchscreen
interrupt_pin: ${interrupt_pin} interrupt_pin: ${interrupt_pin}
display: ssd1306_display display: ssd1306_i2c_display
on_touch: on_touch:
- logger.log: - logger.log:
format: Touch at (%d, %d) format: Touch at (%d, %d)
+2 -2
View File
@@ -1,9 +1,9 @@
pn532_i2c: pn532_i2c:
i2c_id: i2c_bus i2c_id: i2c_bus
id: pn532_nfcc id: pn532_nfcc_i2c
binary_sensor: binary_sensor:
- platform: pn532 - platform: pn532
pn532_id: pn532_nfcc pn532_id: pn532_nfcc_i2c
name: PN532 NFC Tag name: PN532 NFC Tag
uid: 74-10-37-94 uid: 74-10-37-94
+2 -2
View File
@@ -1,9 +1,9 @@
pn532_spi: pn532_spi:
id: pn532_nfcc id: pn532_nfcc_spi
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
binary_sensor: binary_sensor:
- platform: pn532 - platform: pn532
pn532_id: pn532_nfcc pn532_id: pn532_nfcc_spi
name: PN532 NFC Tag name: PN532 NFC Tag
uid: 74-10-37-94 uid: 74-10-37-94
+9 -9
View File
@@ -1,23 +1,23 @@
esphome: esphome:
on_boot: on_boot:
then: then:
- tag.set_clean_mode: nfcc_pn7160 - tag.set_clean_mode: nfcc_pn7160_i2c
- tag.set_format_mode: nfcc_pn7160 - tag.set_format_mode: nfcc_pn7160_i2c
- tag.set_read_mode: nfcc_pn7160 - tag.set_read_mode: nfcc_pn7160_i2c
- tag.set_write_message: - tag.set_write_message:
message: https://www.home-assistant.io/tag/pulse message: https://www.home-assistant.io/tag/pulse
include_android_app_record: false include_android_app_record: false
- tag.set_write_mode: nfcc_pn7160 - tag.set_write_mode: nfcc_pn7160_i2c
- tag.set_emulation_message: - tag.set_emulation_message:
message: https://www.home-assistant.io/tag/pulse message: https://www.home-assistant.io/tag/pulse
include_android_app_record: false include_android_app_record: false
- tag.emulation_off: nfcc_pn7160 - tag.emulation_off: nfcc_pn7160_i2c
- tag.emulation_on: nfcc_pn7160 - tag.emulation_on: nfcc_pn7160_i2c
- tag.polling_off: nfcc_pn7160 - tag.polling_off: nfcc_pn7160_i2c
- tag.polling_on: nfcc_pn7160 - tag.polling_on: nfcc_pn7160_i2c
pn7150_i2c: pn7150_i2c:
id: nfcc_pn7160 id: nfcc_pn7160_i2c
i2c_id: i2c_bus i2c_id: i2c_bus
irq_pin: ${irq_pin} irq_pin: ${irq_pin}
ven_pin: ${ven_pin} ven_pin: ${ven_pin}
+9 -9
View File
@@ -1,23 +1,23 @@
esphome: esphome:
on_boot: on_boot:
then: then:
- tag.set_clean_mode: nfcc_pn7160 - tag.set_clean_mode: nfcc_pn7160_spi
- tag.set_format_mode: nfcc_pn7160 - tag.set_format_mode: nfcc_pn7160_spi
- tag.set_read_mode: nfcc_pn7160 - tag.set_read_mode: nfcc_pn7160_spi
- tag.set_write_message: - tag.set_write_message:
message: https://www.home-assistant.io/tag/pulse message: https://www.home-assistant.io/tag/pulse
include_android_app_record: false include_android_app_record: false
- tag.set_write_mode: nfcc_pn7160 - tag.set_write_mode: nfcc_pn7160_spi
- tag.set_emulation_message: - tag.set_emulation_message:
message: https://www.home-assistant.io/tag/pulse message: https://www.home-assistant.io/tag/pulse
include_android_app_record: false include_android_app_record: false
- tag.emulation_off: nfcc_pn7160 - tag.emulation_off: nfcc_pn7160_spi
- tag.emulation_on: nfcc_pn7160 - tag.emulation_on: nfcc_pn7160_spi
- tag.polling_off: nfcc_pn7160 - tag.polling_off: nfcc_pn7160_spi
- tag.polling_on: nfcc_pn7160 - tag.polling_on: nfcc_pn7160_spi
pn7160_spi: pn7160_spi:
id: nfcc_pn7160 id: nfcc_pn7160_spi
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
irq_pin: ${irq_pin} irq_pin: ${irq_pin}
ven_pin: ${ven_pin} ven_pin: ${ven_pin}
+2 -2
View File
@@ -1,5 +1,5 @@
rc522_i2c: rc522_i2c:
- id: rc522_nfcc - id: rc522_nfcc_i2c
i2c_id: i2c_bus i2c_id: i2c_bus
update_interval: 1s update_interval: 1s
on_tag: on_tag:
@@ -8,6 +8,6 @@ rc522_i2c:
binary_sensor: binary_sensor:
- platform: rc522 - platform: rc522
rc522_id: rc522_nfcc rc522_id: rc522_nfcc_i2c
name: RC522 NFC Tag name: RC522 NFC Tag
uid: 74-10-37-94 uid: 74-10-37-94
+3 -3
View File
@@ -1,9 +1,9 @@
rc522_spi: rc522_spi:
id: rc522_nfcc id: rc522_nfcc_spi
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
binary_sensor: binary_sensor:
- platform: rc522 - platform: rc522
rc522_id: rc522_nfcc rc522_id: rc522_nfcc_spi
name: PN532 NFC Tag name: RC522 NFC Tag
uid: 74-10-37-94 uid: 74-10-37-94
@@ -1,7 +1,7 @@
substitutions: substitutions:
tx_pin: GPIO0 tx_pin: GPIO0
rx_pin: GPIO2 rx_pin: GPIO2
flow_control_pin: GPIO4 flow_control_pin: GPIO15
packages: packages:
modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml
@@ -2,8 +2,8 @@ packages:
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
substitutions: substitutions:
clock_pin: GPIO5 clock_pin: GPIO15
data_pin: GPIO4 data_pin: GPIO16
latch_pin1: GPIO2 latch_pin1: GPIO2
oe_pin1: GPIO0 oe_pin1: GPIO0
latch_pin2: GPIO3 latch_pin2: GPIO3
+1 -1
View File
@@ -4,7 +4,7 @@ display:
model: SSD1306_128X64 model: SSD1306_128X64
reset_pin: ${reset_pin} reset_pin: ${reset_pin}
address: 0x3C address: 0x3C
id: display1 id: ssd1306_i2c_display
contrast: 60% contrast: 60%
pages: pages:
- id: ssd1306_i2c_page1 - id: ssd1306_i2c_page1
+1
View File
@@ -1,5 +1,6 @@
display: display:
- platform: ssd1306_spi - platform: ssd1306_spi
id: ssd1306_spi_display
model: SSD1306 128x64 model: SSD1306 128x64
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
dc_pin: ${dc_pin} dc_pin: ${dc_pin}
+1 -1
View File
@@ -4,7 +4,7 @@ display:
model: SSD1327_128x128 model: SSD1327_128x128
reset_pin: ${reset_pin} reset_pin: ${reset_pin}
address: 0x3C address: 0x3C
id: display1 id: ssd1327_i2c_display
pages: pages:
- id: ssd1327_i2c_page1 - id: ssd1327_i2c_page1
lambda: |- lambda: |-
+1
View File
@@ -1,5 +1,6 @@
display: display:
- platform: ssd1327_spi - platform: ssd1327_spi
id: ssd1327_spi_display
model: SSD1327 128x128 model: SSD1327 128x128
cs_pin: ${cs_pin} cs_pin: ${cs_pin}
dc_pin: ${dc_pin} dc_pin: ${dc_pin}
+1 -1
View File
@@ -3,7 +3,7 @@ display:
i2c_id: i2c_bus i2c_id: i2c_bus
reset_pin: ${reset_pin} reset_pin: ${reset_pin}
address: 0x3C address: 0x3C
id: display1 id: st7567_i2c_display
pages: pages:
- id: st7567_i2c_page1 - id: st7567_i2c_page1
lambda: |- lambda: |-

Some files were not shown because too many files have changed in this diff Show More