[tests] Fail component test merge on conflicting duplicate IDs (#16849)

This commit is contained in:
J. Nick Koston
2026-06-08 12:49:25 -05:00
committed by GitHub
parent 54c73bf1bc
commit 36e043debb
125 changed files with 836 additions and 372 deletions
+1
View File
@@ -113,6 +113,7 @@ jobs:
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
script/generate-rp2040-boards.py --check
script/ci_check_duplicate_test_ids.py
import-time:
name: Check import esphome.__main__ time
+7 -2
View File
@@ -39,8 +39,13 @@ from helpers import BASE_BUS_COMPONENTS, is_validate_only_file
from esphome import yaml_util
from esphome.config_helpers import Extend, Remove
# Path to common bus configs
COMMON_BUS_PATH = Path("tests/test_build_components/common")
# Path to common bus configs (resolved relative to this file, not the CWD)
COMMON_BUS_PATH = (
Path(__file__).resolve().parent.parent
/ "tests"
/ "test_build_components"
/ "common"
)
# Package dependencies - maps packages to the packages they include
# When a component uses a package on the left, it automatically gets
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Fail when two component test fixtures define the same id with different content.
Component tests are merged and built in groups in CI (see
``script/merge_component_configs.py``). When two components declare the same id
under the same section but with different content, the merge keeps the first and
drops the rest, which can make a cross-reference resolve to an incompatible
entity (this is what broke the i2s_audio speaker tests). That only surfaces when
the two components happen to land in the same group, often in an unrelated PR
long after the duplicate was written.
This script is the complete, batch-independent guard: it scans every component's
``test.<platform>.yaml`` per platform and reports any id that is defined by more
than one component with differing content, so a collision fails the PR that
introduces it and names the exact id and components.
To stay byte-for-byte consistent with what the merge actually does (so the guard
never disagrees with the build), it reuses the merge's own helpers:
* ``prefix_substitutions_in_dict`` -- the merge prefixes every component's
substitution references with the component name before deduplicating, so e.g.
``pin: ${pin}`` in two components becomes ``${a_pin}`` and ``${b_pin}`` and
conflicts. We apply the same prefixing; otherwise a shared id whose only
difference is a substitution looks identical here but conflicts at merge time.
* ``deduplicate_by_id`` -- the actual merge comparison (including the
``INTENTIONALLY_SHARED_IDS`` allowlist for deliberately shared singletons such
as ``sntp_time``). We feed each shared id's prefixed items straight through it
and treat a raised ``ValueError`` as a conflict, so this check and the merge
can never diverge.
``packages:`` are left as opaque ``!include`` objects by the loader -- exactly as
the merge sees them at dedup time -- so package-provided bus ids (``i2c_bus`` ...)
are not compared here, matching the merge, which re-adds those packages once.
"""
from __future__ import annotations
from collections import defaultdict
from collections.abc import Iterator
from dataclasses import dataclass, field
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
from esphome.core import EsphomeError # noqa: E402
from script.merge_component_configs import ( # noqa: E402
deduplicate_by_id,
load_yaml_file,
prepare_component_body,
)
# Resolved relative to this file (not the CWD) so the scan cannot silently cover
# nothing when run from a different directory.
TESTS_DIR = Path(__file__).resolve().parent.parent / "tests" / "components"
def _collect_ids(
data: object,
path: tuple[str, ...],
out: dict[tuple[tuple[str, ...], object], object],
) -> None:
"""Record (dict_path, id) -> item for id-bearing items in dict-reachable lists.
Keyed by the full dict path (not just the immediate key) so items under
different paths that happen to share a list key name are never compared. Only
lists reached purely through dict keys are recorded: once the merge
concatenates a list, items from different components live in separate elements,
so anything deeper is never compared across components (matching how
``merge_config`` combines bodies). Ids keep their original type so ``5`` and
``"5"`` stay distinct, exactly as ``deduplicate_by_id`` treats them; an
unhashable id (rare) falls back to its ``repr`` so it can still be grouped.
"""
if not isinstance(data, dict):
return
for key, value in data.items():
new_path = path + (key,)
if isinstance(value, list):
for item in value:
if isinstance(item, dict) and "id" in item:
item_id = item["id"]
try:
hash(item_id)
except TypeError:
item_id = repr(item_id)
out[(new_path, item_id)] = item
elif isinstance(value, dict):
_collect_ids(value, new_path, out)
def _discover_platforms() -> set[str]:
platforms: set[str] = set()
for test_file in TESTS_DIR.glob("*/test.*.yaml"):
# test.<platform>.yaml -> platform is the middle dotted part
parts = test_file.name.split(".")
if len(parts) == 3:
platforms.add(parts[1])
return platforms
def _load_components(
platform: str, parse_errors: list[str]
) -> Iterator[tuple[str, object]]:
"""Yield (component, prefixed config) for each component testing this platform.
Each body is prepared with ``prepare_component_body`` (the same helper the
merge uses: it expands component-specific package includes and prefixes
substitutions), so the comparison sees what the build merges. Fixtures that
fail to parse are recorded in ``parse_errors`` so the run can fail rather than
silently skip them.
"""
for comp_dir in sorted(TESTS_DIR.iterdir()):
test_file = comp_dir / f"test.{platform}.yaml"
if not comp_dir.is_dir() or not test_file.exists():
continue
try:
data = load_yaml_file(test_file)
except EsphomeError as err:
parse_errors.append(str(test_file))
print(f"ERROR: could not parse {test_file}: {err}", file=sys.stderr)
continue
yield comp_dir.name, prepare_component_body(data, comp_dir.name, comp_dir)
@dataclass
class ScanResult:
"""Outcome of a scan. A caller cannot observe a clean result while files were
skipped or nothing was scanned -- all three fields are reported together."""
conflicts: list[str] = field(default_factory=list)
parse_errors: list[str] = field(default_factory=list)
components_scanned: int = 0
def scan() -> ScanResult:
"""Scan every component's base test fixture and report cross-component id conflicts.
Only base ``test.<platform>.yaml`` fixtures are scanned because only those are
combined by ``merge_component_configs`` in grouped CI builds; variant
(``test-*.yaml``) fixtures are built individually and never cross-merged.
"""
result = ScanResult()
for platform in sorted(_discover_platforms()):
# (dict_path, id) -> {component: prefixed_item}
groups: dict[tuple[tuple[str, ...], object], dict[str, object]] = defaultdict(
dict
)
for component, data in _load_components(platform, result.parse_errors):
result.components_scanned += 1
collected: dict[tuple[tuple[str, ...], object], object] = {}
_collect_ids(data, (), collected)
for key, item in collected.items():
groups[key][component] = item
for (path, id_), by_component in sorted(
groups.items(), key=lambda kv: (kv[0][0], str(kv[0][1]))
):
if len(by_component) < 2:
continue
# Delegate the decision to the merge's own deduplication so this guard
# can never disagree with what the build does.
try:
deduplicate_by_id({path[-1]: list(by_component.values())})
except ValueError:
result.conflicts.append(
f"[{platform}] id '{id_}' under '{'.'.join(path)}' is defined "
f"differently by: {', '.join(sorted(by_component))}"
)
return result
def main() -> int:
result = scan()
if result.conflicts:
print("Conflicting test component ids found:\n")
for line in result.conflicts:
print(f" - {line}")
print(
"\nGive each component a unique id (e.g. '<component>_<id>'), or add the "
"id to INTENTIONALLY_SHARED_IDS in script/merge_component_configs.py if "
"it is a deliberately shared singleton."
)
if result.parse_errors:
# A fixture we could not parse was never scanned, so the run is not a
# clean pass even if no conflicts were found among the rest.
print(
f"\n{len(result.parse_errors)} test fixture(s) could not be parsed and "
"were not checked:"
)
for path in result.parse_errors:
print(f" - {path}")
if result.components_scanned == 0:
# A scan that covered nothing is a false green -- the whole point of the
# guard is defeated. Fail loudly (wrong working directory or layout change).
print(
f"\nERROR: scanned 0 component test fixtures under {TESTS_DIR}; "
"the guard covered nothing.",
file=sys.stderr,
)
if result.conflicts or result.parse_errors or result.components_scanned == 0:
return 1
print(
f"No conflicting test component ids found "
f"({result.components_scanned} fixtures scanned)."
)
return 0
if __name__ == "__main__":
sys.exit(main())
+96 -68
View File
@@ -161,18 +161,46 @@ def prefix_substitutions_in_dict(
return data
# (section, id) pairs that several components intentionally share. ESPHome
# treats these as a single instance when merged, so duplicates with differing
# content are expected and must not be flagged as accidental collisions. Keyed on
# the section as well as the id so a generic name (e.g. `ldo_id`) is only exempt
# in its intended section -- an accidental collision on the same name elsewhere
# is still caught.
INTENTIONALLY_SHARED_IDS = frozenset(
{
# Several components each declare an `sntp_time` clock; ESPHome merges
# them into one time source.
("time", "sntp_time"),
# esp_ldo and mipi_dsi both configure the channel-3 internal LDO on the
# ESP32-P4; only one LDO per channel may exist, so the shared id lets the
# merge collapse them into a single LDO.
("esp_ldo", "ldo_id"),
}
)
def deduplicate_by_id(data: dict) -> dict:
"""Deduplicate list items with the same ID.
Keeps only the first occurrence of each ID. If items with the same ID
are identical, this silently deduplicates. If they differ, the first
one is kept (ESPHome's validation will catch if this causes issues).
Identical items sharing an ID (e.g. a shared bus from a common package pulled
in by several components) are collapsed to the first occurrence. Two items
that share an ID but differ in content are a real conflict: when merged, the
first silently wins and the others are dropped, which can make a
cross-reference resolve to an incompatible entity. Rather than defer that to
downstream validation (where it surfaces as a confusing, order-dependent
failure in an unrelated build), raise immediately so the offending ID is
named. Ids in ``INTENTIONALLY_SHARED_IDS`` are deliberately shared singletons
and keep their collapse behaviour.
Args:
data: Parsed config dictionary
Returns:
Config with deduplicated lists
Raises:
ValueError: If two items share an ID but have different content.
"""
if not isinstance(data, dict):
return data
@@ -181,16 +209,25 @@ def deduplicate_by_id(data: dict) -> dict:
for key, value in data.items():
if isinstance(value, list):
# Check for items with 'id' field
seen_ids = set()
seen_items: dict[str, Any] = {}
deduped_list = []
for item in value:
if isinstance(item, dict) and "id" in item:
item_id = item["id"]
if item_id not in seen_ids:
seen_ids.add(item_id)
if item_id not in seen_items:
seen_items[item_id] = item
deduped_list.append(item)
# else: skip duplicate ID (keep first occurrence)
elif (key, item_id) in INTENTIONALLY_SHARED_IDS:
# Deliberately shared singleton -> keep first occurrence.
pass
elif item != seen_items[item_id]:
raise ValueError(
f"Conflicting definitions for id '{item_id}' under "
f"'{key}' when merging test configs; give each "
f"component a unique id"
)
# else: identical duplicate (e.g. shared bus package) -> skip
else:
# No ID, just add it
deduped_list.append(item)
@@ -205,6 +242,55 @@ def deduplicate_by_id(data: dict) -> dict:
return result
def prepare_component_body(comp_data: dict, comp_name: str, comp_dir: Path) -> dict:
"""Return a component's test body as it enters the merge.
Expands component-specific package includes inline (common bus packages are
left for the merge to re-add once), applies ESPHome's top-level-substitutions
-override-package-substitutions rule, then prefixes every substitution
reference with the component name. Shared by ``merge_component_configs`` and
the duplicate-id guard (``script/ci_check_duplicate_test_ids.py``) so the
guard compares exactly what the build merges.
"""
# $component_dir resolves to the component's absolute path.
comp_abs_dir = str(comp_dir.absolute())
# Top-level substitutions override package substitutions, so capture them
# before expanding packages can introduce their own.
top_level_subs = (
comp_data["substitutions"].copy()
if isinstance(comp_data.get("substitutions"), dict)
else {}
)
packages_value = comp_data.get("packages")
if isinstance(packages_value, dict):
common_bus_packages = get_common_bus_packages()
for pkg_name, pkg_value in list(packages_value.items()):
if pkg_name in common_bus_packages:
continue
if isinstance(pkg_value, yaml_util.IncludeFile):
pkg_value = pkg_value.load()
if isinstance(pkg_value, dict):
comp_data = merge_config(comp_data, pkg_value)
elif isinstance(packages_value, list):
for pkg_value in packages_value:
if isinstance(pkg_value, yaml_util.IncludeFile):
pkg_value = pkg_value.load()
if isinstance(pkg_value, dict):
comp_data = merge_config(comp_data, pkg_value)
# Common bus packages are re-added once by the caller; drop them here.
comp_data.pop("packages", None)
subs = comp_data.get("substitutions") or {}
subs.update(top_level_subs)
prefixed_subs = {f"{comp_name}_{name}": value for name, value in subs.items()}
prefixed_subs[f"{comp_name}_component_dir"] = comp_abs_dir
comp_data["substitutions"] = prefixed_subs
return prefix_substitutions_in_dict(comp_data, comp_name)
def merge_component_configs(
component_names: list[str],
platform: str,
@@ -266,67 +352,9 @@ def merge_component_configs(
# New package type - add it
all_packages[pkg_name] = pkg_config
# Handle $component_dir by replacing with absolute path
# This allows components that use local file references to be grouped
comp_abs_dir = str(comp_dir.absolute())
# Save top-level substitutions BEFORE expanding packages
# In ESPHome, top-level substitutions override package substitutions
top_level_subs = (
comp_data["substitutions"].copy()
if "substitutions" in comp_data and comp_data["substitutions"] is not None
else {}
)
# Expand packages - but we'll restore substitution priority after
if "packages" in comp_data:
packages_value = comp_data["packages"]
if isinstance(packages_value, dict):
# Dict format - check each package
common_bus_packages = get_common_bus_packages()
for pkg_name, pkg_value in list(packages_value.items()):
if pkg_name in common_bus_packages:
continue
# Resolve deferred !include files before checking type
if isinstance(pkg_value, yaml_util.IncludeFile):
pkg_value = pkg_value.load()
if not isinstance(pkg_value, dict):
continue
# Component-specific package - expand its content into top level
comp_data = merge_config(comp_data, pkg_value)
elif isinstance(packages_value, list):
# List format - expand all package includes
for pkg_value in packages_value:
# Resolve deferred !include files before checking type
if isinstance(pkg_value, yaml_util.IncludeFile):
pkg_value = pkg_value.load()
if not isinstance(pkg_value, dict):
continue
comp_data = merge_config(comp_data, pkg_value)
# Remove all packages (common will be re-added at the end)
del comp_data["packages"]
# Restore top-level substitution priority
# Top-level substitutions override any from packages
if "substitutions" not in comp_data or comp_data["substitutions"] is None:
comp_data["substitutions"] = {}
# Merge: package subs as base, top-level subs override
comp_data["substitutions"].update(top_level_subs)
# Now prefix the final merged substitutions
comp_data["substitutions"] = {
f"{comp_name}_{sub_name}": sub_value
for sub_name, sub_value in comp_data["substitutions"].items()
}
# Add component_dir substitution with absolute path for this component
comp_data["substitutions"][f"{comp_name}_component_dir"] = comp_abs_dir
# Prefix substitution references throughout the config
comp_data = prefix_substitutions_in_dict(comp_data, comp_name)
# Expand component-specific packages and prefix substitutions, exactly as
# the duplicate-id guard does, so both see the same body.
comp_data = prepare_component_body(comp_data, comp_name, comp_dir)
# Use ESPHome's merge_config to merge this component into the result
# merge_config handles list merging with ID-based deduplication automatically
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: P23
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO1
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO1
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: A0
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO16
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO1
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO1
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: VCC
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: A5
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: VCC
name: ADC Test sensor
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: VCC
name: ADC Test sensor
@@ -1,6 +1,6 @@
binary_sensor:
- platform: gpio
id: bin1
id: alarm_control_panel_bin1
pin: 1
alarm_control_panel:
@@ -18,7 +18,7 @@ alarm_control_panel:
pending_time: 15s
trigger_time: 30s
binary_sensors:
- input: bin1
- input: alarm_control_panel_bin1
bypass_armed_home: true
bypass_armed_night: true
bypass_auto: true
@@ -39,7 +39,7 @@ alarm_control_panel:
pending_time: 15s
trigger_time: 30s
binary_sensors:
- input: bin1
- input: alarm_control_panel_bin1
bypass_armed_home: true
bypass_armed_night: true
bypass_auto: true
@@ -4,7 +4,7 @@ packages:
display:
- platform: ili9xxx
id: main_lcd
id: animation_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 12
@@ -4,7 +4,7 @@ packages:
display:
- platform: ili9xxx
id: main_lcd
id: animation_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 5
@@ -4,7 +4,7 @@ packages:
display:
- platform: ili9xxx
id: main_lcd
id: animation_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 20
+1 -1
View File
@@ -296,7 +296,7 @@ api:
event:
- platform: template
name: Test Event
id: test_event
id: api_test_event
event_types:
- single_click
- double_click
+1 -1
View File
@@ -1,5 +1,5 @@
audio_file:
- id: test_audio
- id: audio_file_test_audio
file:
type: local
path: $component_dir/test.wav
@@ -1,5 +1,5 @@
audio_file:
- id: test_audio
- id: audio_file_test_audio
file:
type: local
path: $component_dir/test.wav
+2 -2
View File
@@ -1,7 +1,7 @@
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_i2c_display
id: axs15231_ssd1306_i2c_display
model: SSD1306_128X64
reset_pin: 19
pages:
@@ -13,6 +13,6 @@ touchscreen:
- platform: axs15231
i2c_id: i2c_bus
id: axs15231_touchscreen
display: ssd1306_i2c_display
display: axs15231_ssd1306_i2c_display
interrupt_pin: 20
reset_pin: 18
@@ -4,7 +4,7 @@ packages:
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_display
id: axs15231_ssd1306_display
model: SSD1306_128X64
reset_pin: 13
pages:
@@ -15,5 +15,5 @@ display:
touchscreen:
- platform: axs15231
i2c_id: i2c_bus
display: ssd1306_display
display: axs15231_ssd1306_display
interrupt_pin: 12
+6 -6
View File
@@ -1,6 +1,6 @@
switch:
- platform: template
id: template_switch1
id: bang_bang_template_switch1
optimistic: true
- platform: template
id: template_switch2
@@ -8,7 +8,7 @@ switch:
sensor:
- platform: template
id: template_sensor1
id: bang_bang_template_sensor1
lambda: |-
if (millis() > 10000) {
return 42.0;
@@ -20,16 +20,16 @@ sensor:
climate:
- platform: bang_bang
name: Bang Bang Climate
sensor: template_sensor1
humidity_sensor: template_sensor1
sensor: bang_bang_template_sensor1
humidity_sensor: bang_bang_template_sensor1
default_target_temperature_low: 18°C
default_target_temperature_high: 24°C
idle_action:
- switch.turn_on: template_switch1
- switch.turn_on: bang_bang_template_switch1
cool_action:
- switch.turn_on: template_switch2
heat_action:
- switch.turn_on: template_switch1
- switch.turn_on: bang_bang_template_switch1
away_config:
default_target_temperature_low: 16°C
default_target_temperature_high: 20°C
+3 -3
View File
@@ -1,7 +1,7 @@
binary_sensor:
- platform: template
trigger_on_initial_state: true
id: some_binary_sensor
id: binary_sensor_some_binary_sensor
name: "Random binary"
lambda: return (random_uint32() & 1) == 0;
filters:
@@ -21,7 +21,7 @@ binary_sensor:
time_off: 100ms
time_on: 400ms
- lambda: |-
if (id(some_binary_sensor).state) {
if (id(binary_sensor_some_binary_sensor).state) {
return x;
}
return {};
@@ -36,7 +36,7 @@ binary_sensor:
- logger.log:
format: "New state is %s"
args: ['x.has_value() ? ONOFF(x) : "Unknown"']
- binary_sensor.invalidate_state: some_binary_sensor
- binary_sensor.invalidate_state: binary_sensor_some_binary_sensor
# Test autorepeat with default configuration (no timings)
- platform: template
+12 -12
View File
@@ -1,20 +1,20 @@
binary_sensor:
- platform: template
id: bin1
id: binary_sensor_map_bin1
lambda: |-
if (millis() > 10000) {
return true;
}
return false;
- platform: template
id: bin2
id: binary_sensor_map_bin2
lambda: |-
if (millis() > 20000) {
return true;
}
return false;
- platform: template
id: bin3
id: binary_sensor_map_bin3
lambda: |-
if (millis() > 30000) {
return true;
@@ -26,33 +26,33 @@ sensor:
name: Binary Sensor Map Group
type: group
channels:
- binary_sensor: bin1
- binary_sensor: binary_sensor_map_bin1
value: 10.0
- binary_sensor: bin2
- binary_sensor: binary_sensor_map_bin2
value: 15.0
- binary_sensor: bin3
- binary_sensor: binary_sensor_map_bin3
value: 100.0
- platform: binary_sensor_map
name: Binary Sensor Map Sum
type: sum
channels:
- binary_sensor: bin1
- binary_sensor: binary_sensor_map_bin1
value: 10.0
- binary_sensor: bin2
- binary_sensor: binary_sensor_map_bin2
value: 15.0
- binary_sensor: bin3
- binary_sensor: binary_sensor_map_bin3
value: 100.0
- platform: binary_sensor_map
name: Binary Sensor Map Bayesian
type: bayesian
prior: 0.4
observations:
- binary_sensor: bin1
- binary_sensor: binary_sensor_map_bin1
prob_given_true: 0.9
prob_given_false: 0.4
- binary_sensor: bin2
- binary_sensor: binary_sensor_map_bin2
prob_given_true: 0.7
prob_given_false: 0.05
- binary_sensor: bin3
- binary_sensor: binary_sensor_map_bin3
prob_given_true: 0.8
prob_given_false: 0.2
+2 -2
View File
@@ -56,7 +56,7 @@ sensor:
number:
- platform: template
name: "Test Number"
id: test_number
id: ble_client_test_number
optimistic: true
min_value: 0
max_value: 255
@@ -72,5 +72,5 @@ button:
service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678"
characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678"
value: !lambda |-
uint8_t val = (uint8_t)id(test_number).state;
uint8_t val = (uint8_t)id(ble_client_test_number).state;
return std::vector<uint8_t>{0xAA, val, 0xBB};
+3 -3
View File
@@ -1,6 +1,6 @@
canbus:
- platform: esp32_can
id: esp32_internal_can
id: canbus_esp32_internal_can
rx_pin: 4
tx_pin: 5
can_id: 4
@@ -40,7 +40,7 @@ canbus:
number:
- platform: template
name: "Test Number"
id: test_number
id: canbus_test_number
optimistic: true
min_value: 0
max_value: 255
@@ -62,5 +62,5 @@ button:
- canbus.send: !lambda return {0, 1, 2};
# Test canbus.send with lambda that references a component (function pointer)
- canbus.send: !lambda |-
uint8_t val = (uint8_t)id(test_number).state;
uint8_t val = (uint8_t)id(canbus_test_number).state;
return std::vector<uint8_t>{0xAA, val, 0xBB};
+3 -3
View File
@@ -6,13 +6,13 @@ cd74hc4067:
sensor:
- platform: adc
id: esp_adc_sensor
id: cd74hc4067_esp_adc_sensor
pin: ${pin}
- platform: cd74hc4067
id: cd74hc4067_adc_0
number: 0
sensor: esp_adc_sensor
sensor: cd74hc4067_esp_adc_sensor
- platform: cd74hc4067
id: cd74hc4067_adc_1
number: 1
sensor: esp_adc_sensor
sensor: cd74hc4067_esp_adc_sensor
+2 -2
View File
@@ -1,6 +1,6 @@
sensor:
- platform: template
id: temp_sensor
id: climate_ir_lg_temp_sensor
lambda: return 22.0;
update_interval: 60s
- platform: template
@@ -12,5 +12,5 @@ climate:
- platform: climate_ir_lg
name: LG Climate
transmitter_id: xmitr
sensor: temp_sensor
sensor: climate_ir_lg_temp_sensor
humidity_sensor: humidity_sensor
@@ -1,15 +1,15 @@
output:
- platform: ${light_platform}
id: light_output_1
id: color_temperature_light_output_1
pin: ${pin_o1}
- platform: ${light_platform}
id: light_output_2
id: color_temperature_light_output_2
pin: ${pin_o2}
light:
- platform: color_temperature
name: Lights
color_temperature: light_output_1
brightness: light_output_2
color_temperature: color_temperature_light_output_1
brightness: color_temperature_light_output_2
cold_white_color_temperature: 153 mireds
warm_white_color_temperature: 500 mireds
+4 -4
View File
@@ -1,17 +1,17 @@
output:
- platform: ${pwm_platform}
id: fan_output_1
id: copy_fan_output_1
pin: ${pin}
fan:
- platform: speed
id: fan_speed
output: fan_output_1
id: copy_fan_speed
output: copy_fan_output_1
preset_modes:
- Eco
- Turbo
- platform: copy
source_id: fan_speed
source_id: copy_fan_speed
name: Fan Speed Copy
select:
+2 -2
View File
@@ -1,9 +1,9 @@
sensor:
- platform: adc
id: esp_adc_sensor
id: ct_clamp_esp_adc_sensor
pin: ${pin}
- platform: ct_clamp
sensor: esp_adc_sensor
sensor: ct_clamp_esp_adc_sensor
name: CT Clamp
sample_duration: 500ms
update_interval: 5s
+3 -3
View File
@@ -31,7 +31,7 @@ sensor:
switch:
- platform: template
id: template_switch1
id: current_based_template_switch1
optimistic: true
- platform: template
id: template_switch2
@@ -46,7 +46,7 @@ cover:
open_obstacle_current_threshold: 0.8
open_duration: 12s
open_action:
- switch.turn_on: template_switch1
- switch.turn_on: current_based_template_switch1
close_sensor: ade7953_current_b
close_moving_current_threshold: 0.5
close_obstacle_current_threshold: 0.8
@@ -54,7 +54,7 @@ cover:
close_action:
- switch.turn_on: template_switch2
stop_action:
- switch.turn_off: template_switch1
- switch.turn_off: current_based_template_switch1
- switch.turn_off: template_switch2
obstacle_rollback: 30%
start_sensing_delay: 0.8s
+2 -2
View File
@@ -1,8 +1,8 @@
light:
- platform: cwww
name: CWWW Light
cold_white: light_output_1
warm_white: light_output_2
cold_white: cwww_light_output_1
warm_white: cwww_light_output_2
cold_white_color_temperature: 153 mireds
warm_white_color_temperature: 500 mireds
constant_brightness: true
+2 -2
View File
@@ -5,11 +5,11 @@ substitutions:
output:
- platform: ${light_platform}
id: light_output_1
id: cwww_light_output_1
pin: ${pin_o1}
channel: 0
- platform: ${light_platform}
id: light_output_2
id: cwww_light_output_2
pin: ${pin_o2}
channel: 1
phase_angle: 180°
+2 -2
View File
@@ -5,10 +5,10 @@ substitutions:
output:
- platform: ${light_platform}
id: light_output_1
id: cwww_light_output_1
pin: ${pin_o1}
- platform: ${light_platform}
id: light_output_2
id: cwww_light_output_2
pin: ${pin_o2}
<<: !include common.yaml
+2 -2
View File
@@ -5,10 +5,10 @@ substitutions:
output:
- platform: ${light_platform}
id: light_output_1
id: cwww_light_output_1
pin: ${pin_o1}
- platform: ${light_platform}
id: light_output_2
id: cwww_light_output_2
pin: ${pin_o2}
<<: !include common.yaml
+2 -2
View File
@@ -1,6 +1,6 @@
binary_sensor:
- platform: template
id: bin1
id: duty_time_bin1
lambda: |-
if (millis() > 10000) {
return true;
@@ -10,4 +10,4 @@ binary_sensor:
sensor:
- platform: duty_time
name: Duty Time
sensor: bin1
sensor: duty_time_bin1
+1 -1
View File
@@ -2,7 +2,7 @@
light:
- platform: rp2040_pio_led_strip
id: led_strip
id: e131_led_strip
pin: 2
pio: 0
num_leds: 256
+2 -2
View File
@@ -1,7 +1,7 @@
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_i2c_display
id: ektf2232_ssd1306_i2c_display
model: SSD1306_128X64
reset_pin: ${display_reset_pin}
pages:
@@ -15,7 +15,7 @@ touchscreen:
id: ektf2232_touchscreen
interrupt_pin: ${interrupt_pin}
reset_pin: ${touch_reset_pin}
display: ssd1306_i2c_display
display: ektf2232_ssd1306_i2c_display
on_touch:
- logger.log:
format: Touch at (%d, %d)
+6 -6
View File
@@ -1,6 +1,6 @@
binary_sensor:
- platform: template
id: bin1
id: endstop_bin1
lambda: |-
if (millis() > 10000) {
return true;
@@ -9,7 +9,7 @@ binary_sensor:
switch:
- platform: template
id: template_switch1
id: endstop_template_switch1
optimistic: true
- platform: template
id: template_switch2
@@ -20,12 +20,12 @@ cover:
id: endstop_cover
name: Endstop Cover
stop_action:
- switch.turn_on: template_switch1
open_endstop: bin1
- switch.turn_on: endstop_template_switch1
open_endstop: endstop_bin1
open_action:
- switch.turn_on: template_switch1
- switch.turn_on: endstop_template_switch1
open_duration: 5min
close_endstop: bin1
close_endstop: endstop_bin1
close_action:
- switch.turn_on: template_switch2
close_duration: 4.5min
+1 -1
View File
@@ -13,7 +13,7 @@ esphome:
canbus:
- platform: esp32_can
id: esp32_internal_can
id: esp32_can_esp32_internal_can
rx_pin: ${rx_pin}
tx_pin: ${tx_pin}
can_id: 4
@@ -3,20 +3,20 @@ esphome:
then:
- canbus.send:
# Extended ID explicit
canbus_id: esp32_internal_can
canbus_id: esp32_can_esp32_internal_can
use_extended_id: true
can_id: 0x100
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
- canbus.send:
# Standard ID by default
canbus_id: esp32_internal_can
canbus_id: esp32_can_esp32_internal_can
can_id: 0x100
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
# Note: esp32_internal_can_2 uses LISTENONLY mode, so no send actions
canbus:
- platform: esp32_can
id: esp32_internal_can
id: esp32_can_esp32_internal_can
rx_pin: GPIO8
tx_pin: GPIO7
can_id: 4
+3 -3
View File
@@ -62,7 +62,7 @@ packet_transport:
encryption:
key: "0123456789abcdef0123456789abcdef"
sensors:
- temp_sensor
- espnow_temp_sensor
providers:
- name: test-provider
encryption:
@@ -70,9 +70,9 @@ packet_transport:
sensor:
- platform: internal_temperature
id: temp_sensor
id: espnow_temp_sensor
- platform: packet_transport
provider: test-provider
remote_id: temp_sensor
remote_id: espnow_temp_sensor
id: remote_temp
@@ -1,6 +1,6 @@
light:
- platform: fastled_clockless
id: addr1
id: fastled_clockless_addr1
chipset: WS2811
pin: 13
num_leds: 100
@@ -59,13 +59,13 @@ light:
name: Custom Effect
sequence:
- light.addressable_set:
id: addr1
id: fastled_clockless_addr1
red: 100%
green: 100%
blue: 0%
- delay: 100ms
- light.addressable_set:
id: addr1
id: fastled_clockless_addr1
red: 0%
green: 100%
blue: 0%
+3 -3
View File
@@ -1,6 +1,6 @@
light:
- platform: fastled_spi
id: addr1
id: fastled_spi_addr1
chipset: WS2801
clock_pin: 22
data_pin: 23
@@ -59,13 +59,13 @@ light:
name: Custom Effect
sequence:
- light.addressable_set:
id: addr1
id: fastled_spi_addr1
red: 100%
green: 100%
blue: 0%
- delay: 100ms
- light.addressable_set:
id: addr1
id: fastled_spi_addr1
red: 0%
green: 100%
blue: 0%
+3 -3
View File
@@ -8,7 +8,7 @@ font:
id: roboto32
- file: "gfonts://Roboto"
id: roboto
id: font_roboto
size: 20
glyphs: "0123456789."
extras:
@@ -50,11 +50,11 @@ font:
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_display
id: font_ssd1306_display
model: SSD1306_128X64
reset_pin: ${display_reset_pin}
lambda: |-
it.print(0, 0, id(roboto), "Hello, World!");
it.print(0, 0, id(font_roboto), "Hello, World!");
it.print(0, 20, id(roboto_web), "Hello, World!");
it.print(0, 40, id(monocraft), "Hello, World!");
it.print(0, 60, id(monocraft2), "Hello, World!");
+3 -3
View File
@@ -8,7 +8,7 @@ font:
id: roboto32
- file: "gfonts://Roboto"
id: roboto
id: font_roboto
size: 20
glyphs: "0123456789."
extras:
@@ -44,12 +44,12 @@ font:
display:
- platform: sdl
id: sdl_display
id: font_sdl_display
dimensions:
width: 800
height: 600
lambda: |-
it.print(0, 0, id(roboto), "Hello, World!");
it.print(0, 0, id(font_roboto), "Hello, World!");
it.print(0, 20, id(roboto_web), "Hello, World!");
it.print(0, 40, id(roboto_greek), "Hello κόσμε!");
it.print(0, 60, id(monocraft), "Hello, World!");

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