393 files

This commit is contained in:
Home Assistant Version Control committed 2026-09-27 17:16:54 +00:00
1 parent b05b7a897e
commit 39d97ac2db
404 files changed
+54006 -11034

No files matched your search

@@ -31,7 +31,13 @@ Direction semantics:
Connect salt/rinse-aid/descale/clean events. Trigger: a state_change latch on
``present`` (not a numeric threshold); the task auto-completes when the event
clears. The appliance emitting the clearing event is required for auto-resolve
— otherwise the task waits for a manual completion.
— otherwise the task waits for a manual completion. Two variants:
``ok_state`` flips the latch for LEVEL enums whose alert is "anything but
OK" (homeconnect_ws salt: ``empty``/``nearly_empty``/``full`` → alert while
not ``full``); several ``keys`` in one signature make ONE latch watching
every matched entity with ``entity_logic: any`` (Home Connect's
``salt_nearly_empty`` + ``salt_lack`` + ``program_blocked_salt_lack``) —
see ``build_setup_trigger`` for when that is (and is not) appropriate.
* ``usage_delta`` — a LIFETIME counter with no reset anywhere (printer
usage hours, burner hours, car odometer). Trigger: a counter trigger in
delta mode — fires every N canonical units (hours for operating-time
@@ -98,6 +104,14 @@ class ConsumableSignature:
# then mean "the device's single entity of this domain".
entity_domain: str = "sensor"
on_states: tuple[str, ...] = ()
# event_present only: the entity's single HEALTHY state. Set, the latch is
# built from-only (``trigger_from_state`` without a To-state, #167): it
# fires on any transition AWAY from this state and recovers when the
# entity returns to it — for level enums that name several alert states
# but only one OK state (homeconnect_ws salt: empty / nearly_empty /
# full). Takes precedence over on_states; unavailable/unknown never count
# as leaving it (the state-change trigger skips them).
ok_state: str = ""
# runtime signatures may track an ATTRIBUTE instead of the state — a
# climate entity's hvac_action says whether it actually conditions.
attribute: str = ""
@@ -118,6 +132,11 @@ class ConsumableSignature:
# the entity's own registry name; a device matching a single entity keeps
# the plain catalog name (a mono printer has no colours to tell apart).
per_entity: bool = False
# 2.94: appliance types (the integration's own setting, see
# IntegrationSignature.appliance_type_key) this duty does not apply to —
# WashData offers "descale" for every appliance, a heat-pump dryer has
# no water circuit to descale.
exclude_appliance_types: tuple[str, ...] = ()
@dataclass(frozen=True)
@@ -130,6 +149,19 @@ class IntegrationSignature:
# date, not a pinned commit) — the audit trail for "verified against what".
verified: str = ""
tasks: tuple[ConsumableSignature, ...] = field(default_factory=tuple)
# Opt-in: this integration gives its entities their own translation_keys,
# so an entity whose translation_key is set and DIFFERS from a catalog key
# is a different entity, whatever its id ends with — the id-suffix /
# infix / object-id fallbacks then only apply to entities WITHOUT one.
# (A bike's 'odometer' key must not claim '…_next_service_odometer'.)
# Deliberately not the default: some integrations set a noisy
# translation_key and are matched by suffix on purpose — xiaomi_miot's
# 'filter-filter_life_level' for our 'filter_life_level'.
translation_keys_authoritative: bool = False
# 2.94: the option of the integration's OWN config entry that names the
# appliance type (WashData: "device_type" = washing_machine / dryer / …),
# read from options first, then data. None = the integration has none.
appliance_type_key: str | None = None
def task_name_variants(task_name: str) -> set[str]:
@@ -184,7 +216,7 @@ def entity_label(hass: HomeAssistant, entry: er.RegistryEntry, device_name: str)
return label.strip()[:60]
def _entity_matches(entry: er.RegistryEntry, key: str) -> bool:
def _entity_matches(entry: er.RegistryEntry, key: str, *, tk_authoritative: bool = False) -> bool:
"""translation_key match, with an entity_id-suffix fallback for custom
integrations that don't set translation_key on their descriptions.
@@ -192,9 +224,16 @@ def _entity_matches(entry: er.RegistryEntry, key: str) -> bool:
a ``_p_{siid}_{piid}`` tail (``..._filter_life_level_p_4_1``) and sets no
translation_key — matched via the distinctive ``_<key>_p_`` infix. Matching
is already scoped to the signature's integration (entry.platform), so this
cannot bleed across integrations."""
cannot bleed across integrations.
``tk_authoritative`` (IntegrationSignature.translation_keys_authoritative):
an entity carrying a DIFFERENT translation_key is never matched by the
fallbacks — they stay available for the integration's entities without
one."""
if entry.translation_key == key:
return True
if tk_authoritative and entry.translation_key:
return False
if entry.entity_id.endswith(f"_{key}"):
return True
if f"_{key}_p_" in entry.entity_id:
@@ -260,11 +299,16 @@ def _threshold_for(sig: ConsumableSignature, hass: HomeAssistant, entity_id: str
unit = (state.attributes.get("unit_of_measurement") if state else None) or "h"
# Canonical → display unit: time counters are stored in hours, odometers in
# kilometres; the entity may display s/min/d resp. miles.
# Every duration unit HA lets a user pick as display unit is listed —
# a missing one silently fell back to 1.0 (a "24 h" floor became "24 w").
factor = {
"μs": 3_600_000_000.0,
"ms": 3_600_000.0,
"s": 3600.0,
"min": 60.0,
"h": 1.0,
"d": 1 / 24,
"w": 1 / 168,
"km": 1.0,
"mi": 0.62137,
# energy counters are canonical in kWh (wallbox cable inspection)
@@ -288,22 +332,44 @@ def build_setup_trigger(sig: ConsumableSignature, hass: HomeAssistant, entity_id
counters, resetting the counter drops it back below the threshold), which
resolves the task just like a cleared problem sensor.
``event_present`` signatures build a single-entity state-change LATCH on the
``present`` state (Home Connect salt/rinse-aid/descale/clean events): the
task activates while the event is present and auto-completes when the
appliance clears it (to ``off``/``confirmed``).
``event_present`` signatures build a state-change LATCH on the ``present``
state (Home Connect salt/rinse-aid/descale/clean events): the task
activates while the event is present and auto-completes when the
appliance clears it (to ``off``/``confirmed``). With ``ok_state`` the
latch is from-only instead: alert == anything but the OK state.
Several matched entities (a signature with several keys) share ONE latch:
``create_triggers`` builds one state-change trigger per entry of
``entity_ids`` and the task sensor aggregates them with ``entity_logic:
any`` — so the newer Home Connect ``salt_lack`` event and the older
``salt_nearly_empty`` both fire "Refill Salt", whichever the appliance
emits. Chosen over a compound OR trigger (its aggregate deactivation never
auto-completes) and over "watch only the preferred key" (drops the other
signal). Caveat of the engine's per-entity recovery: the FIRST entity
leaving its alert state auto-completes the task and resets every latch,
so a sibling still in alert stays silent until its next transition. Put
keys into one signature only when they describe the SAME condition that
one action clears together (refilling salt clears nearly-empty, lack and
program-blocked alike); independently serviced parts (two i-Dos tanks)
use ``per_entity`` instead.
"""
if sig.direction == "event_present":
return {
latch: dict[str, Any] = {
"type": "state_change",
"entity_id": entity_ids[0], # state latch watches a single entity
"entity_id": entity_ids[0], # legacy mirror; entity_ids is authoritative
"entity_ids": list(entity_ids),
# Home Connect events use "present"; other integrations latch on
# their own alert state (Dolphin filter bag: "full").
"trigger_to_state": sig.on_states[0] if sig.on_states else "present",
"trigger_target_changes": 1,
"auto_complete_on_recovery": True,
}
if len(entity_ids) > 1:
latch["entity_logic"] = "any" # the default, spelled out for readers
if sig.ok_state:
latch["trigger_from_state"] = sig.ok_state
else:
# Home Connect events use "present"; other integrations latch on
# their own alert state (Dolphin filter bag: "full").
latch["trigger_to_state"] = sig.on_states[0] if sig.on_states else "present"
return latch
if sig.direction == "cycle_count":
# Engine-counted mechanical cycles: every transition into on_states[0]
# increments; the task fires at N and completing it resets the count.