189 files

This commit is contained in:
Home Assistant Version Control committed 2026-09-08 18:05:32 +00:00
1 parent 79fefc0a7c
commit 303886f212
189 files changed
+21402 -3514

No files matched your search

@@ -58,6 +58,25 @@ def _catalog_name_variants() -> set[str]:
return variants
def _matches_catalog_key(entry: er.RegistryEntry, key: str, catalog_keys: set[str]) -> bool:
"""``_entity_matches`` where a LONGER key of the same catalog that ends
in ``key`` owns the entity.
The entity_id-suffix fallback made dreame's ``secondary_filter_left``
sensor a second source of "Replace Filter" (``…_filter_left``) next to
its own "Replace Secondary Filter" duty — the secondary filter's wear
then fired the main-filter task (bug review 2026-09-04). An exact
translation_key match is the integration's own word and needs no guard.
"""
if entry.translation_key == key:
return True
if not _entity_matches(entry, key):
return False
return not any(
other != key and other.endswith(f"_{key}") and _entity_matches(entry, other) for other in catalog_keys
)
def discover_integration_setups(hass: HomeAssistant) -> list[dict[str, Any]]:
"""Devices of catalogued integrations with their matchable task wiring.
@@ -107,6 +126,7 @@ def discover_integration_setups(hass: HomeAssistant) -> list[dict[str, Any]]:
if device is not None and getattr(device, "parent_device_id", None) is None
else ""
).lower()
catalog_keys = {key for s in catalog.tasks for key in s.keys}
for sig in catalog.tasks:
# Device-type gates: registry model substring and/or a
# type-identifying sibling entity (watched siblings still count —
@@ -127,7 +147,7 @@ def discover_integration_setups(hass: HomeAssistant) -> list[dict[str, Any]]:
continue
# Empty keys (non-sensor domains only, tripwire-enforced) match
# the device's single entity of that domain — THE lawn_mower.
if sig.keys and not any(_entity_matches(entry, key) for key in sig.keys):
if sig.keys and not any(_matches_catalog_key(entry, key, catalog_keys) for key in sig.keys):
continue
group = matched.setdefault(device_id, {}).setdefault(
(integration, sig.task_name, sig.direction),
@@ -75,8 +75,12 @@ _DEFAULT_ABOVE_HOURS = 100
# Canonical units between services for lifetime counters (usage_delta mode):
# hours for operating-time counters, kilometres for odometers.
_DEFAULT_DELTA_UNITS = 500
# Percent floor for percent-remaining consumables (ink, toner, drum, brush %).
_DEFAULT_BELOW_PERCENT = 10
# Percent floor for percent-remaining consumables (ink, toner, drum, brush %)
# — the catalog default is "follow the household setting" (#146), so the
# field is None unless a signature pins its own floor. A sentinel value
# (the old 10) could not tell "pinned at 10" from "default" (bug review
# 2026-09-04).
_DEFAULT_BELOW_PERCENT: int | None = None
@dataclass(frozen=True)
@@ -87,7 +91,7 @@ class ConsumableSignature:
task_name: str # EN task name; localized through templates_i18n
direction: str # duration_left | percent_left | usage_above | event_present | usage_delta | runtime_hours
below_hours: int = _DEFAULT_BELOW_HOURS
below_percent: int = _DEFAULT_BELOW_PERCENT
below_percent: int | None = _DEFAULT_BELOW_PERCENT
above_hours: int = _DEFAULT_ABOVE_HOURS
delta_units: int = _DEFAULT_DELTA_UNITS
# runtime_hours signatures target a non-sensor STATE entity; empty keys
@@ -246,8 +250,8 @@ def _threshold_for(sig: ConsumableSignature, hass: HomeAssistant, entity_id: str
return float(sig.delta_units)
if sig.direction == "percent_left":
# #146: catalog-default floors follow the household setting; a
# signature that pins its own floor keeps it.
if sig.below_percent == _DEFAULT_BELOW_PERCENT:
# signature that pins its own floor (any value, 10 included) keeps it.
if sig.below_percent is None:
from ..global_options import get_consumable_threshold
return float(get_consumable_threshold(hass))