456 files

This commit is contained in:
Home Assistant Version Control committed 2026-10-01 05:38:21 +00:00
1 parent 4192ecbaae
commit 5d6d845ebf
456 files changed
+64951 -3828

No files matched your search

@@ -9,6 +9,7 @@ from homeassistant.helpers import area_registry as ar
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from ..task_origin import origin_is_duty, task_origin
from ._model import (
_entity_matches,
_entity_unit,
@@ -42,6 +43,11 @@ def _entity_watchers(hass: HomeAssistant) -> dict[str, set[str]]:
base = catalog_base_name(name)
if base != name and base in known:
name = base
# 2.95: a task adopted from a duty claims as that duty,
# whatever it is called now (its fingerprint).
origin = task_origin(task)
if origin is not None and origin.get("kind") == "integration" and origin.get("duty"):
name = origin["duty"].lower()
for eid in normalize_entity_ids(tc):
out.setdefault(eid, set()).add(name)
return out
@@ -80,6 +86,42 @@ def _matches_catalog_key(entry: er.RegistryEntry, key: str, catalog_keys: set[st
)
def reset_button_for(
hass: HomeAssistant,
sig: Any,
matched: list[er.RegistryEntry],
device_id: str,
integration: str,
catalog_keys: set[str],
*,
tk_authoritative: bool = False,
) -> dict[str, Any] | None:
"""The integration's own reset button for this duty's counter on the same
device (2.95, helpers/reset_wiring.py) — ``{entity_id, name, disabled}``,
or None when the duty has none or the button is missing / user-disabled."""
if not sig.resets or not matched:
return None
from ..reset_wiring import pressable_reset_buttons
buttons = pressable_reset_buttons(hass, device_id, integration)
for entry in matched:
for sensor_key, button_key in sig.resets:
if not _matches_catalog_key(entry, sensor_key, catalog_keys, tk_authoritative=tk_authoritative):
continue
hits = [b for b in buttons if _entity_matches(b, button_key, tk_authoritative=tk_authoritative)]
# Several buttons under one key (a Matter purifier's HEPA and
# carbon filter both reset via 'reset_filter_condition') cannot be
# told apart — wiring a guess could reset the wrong filter.
if len(hits) == 1:
button = hits[0]
return {
"entity_id": button.entity_id,
"name": button.name or button.original_name or button.entity_id,
"disabled": button.disabled_by is not None,
}
return None
def _appliance_type(hass: HomeAssistant, device: Any, integration: str, key: str | None) -> str | None:
"""The appliance type the integration's own config entry names for this
device (WashData: ``device_type``) — options first, then data."""
@@ -93,15 +135,32 @@ def _appliance_type(hass: HomeAssistant, device: Any, integration: str, key: str
return None
def _adopted_as(tasks: dict[str, Any], integration: str | None, proposal: dict[str, Any]) -> str | None:
"""The name of the target's task adopted from this very duty (and entity
label, for per-entity duties) — found by its fingerprint, not its name."""
if not integration:
return None
for task in tasks.values():
origin = task_origin(task)
if (
origin_is_duty(task, integration, proposal["catalog_task_name"], proposal["direction"])
and origin is not None
and (origin.get("entity_label") or None) == (proposal.get("entity_label") or None)
):
return str(task.get("name", ""))
return None
def annotate_for_target(
hass: HomeAssistant, proposals: list[dict[str, Any]], entry: Any, *, device_name: str = ""
hass: HomeAssistant, proposals: list[dict[str, Any]], entry: Any, *, device_name: str = "", integration: str | None = None
) -> tuple[list[dict[str, Any]], list[dict[str, str]]]:
"""Split proposals against the target object's tasks (2.94).
A duty the target already has BY NAME (any language) goes to ``already``
with the existing task's name — shown, never adopted. The rest keeps
``covered_by`` (helpers.adopt_match.covering_task): a likely duplicate
under another name, which the dialog shows unticked.
under another name, which the dialog shows unticked. A task adopted from
the same duty counts as present under any name (2.95 fingerprint).
"""
from ...const import CONF_TASKS
from ..adopt_match import covering_task
@@ -113,8 +172,9 @@ def annotate_for_target(
already: list[dict[str, str]] = []
for proposal in proposals:
hit = set(by_lower) & proposal_name_variants(proposal["catalog_task_name"], proposal["entity_label"])
if hit:
already.append({"task_name": proposal["task_name"], "task_name_localized": proposal["task_name_localized"], "existing_name": by_lower[sorted(hit)[0]]})
existing = by_lower[sorted(hit)[0]] if hit else _adopted_as(tasks, integration, proposal)
if existing is not None:
already.append({"task_name": proposal["task_name"], "task_name_localized": proposal["task_name_localized"], "existing_name": existing})
continue
covered = (
covering_task(hass, (proposal["task_name_localized"], proposal["task_name"]), proposal["entity_ids"], tasks, ignore=ignore)
@@ -252,6 +312,7 @@ def discover_integration_setups(hass: HomeAssistant, *, targets: dict[str, str]
else:
target_id = suggested["entry_id"] if suggested else (candidate["entry_id"] if candidate else None)
target = hass.config_entries.async_get_entry(target_id) if target_id else None
device_catalog_keys = {key for s in catalog.tasks for key in s.keys}
raw: list[dict[str, Any]] = []
for (_integ, task_name, direction), group in sig_map.items():
entity_ids = sorted(group["entity_ids"])
@@ -283,13 +344,23 @@ def discover_integration_setups(hass: HomeAssistant, *, targets: dict[str, str]
"entity_ids": ids,
"threshold": _threshold_for(sig, hass, ids[0]),
"direction": direction,
# 2.95: completing it also presses this reset button
"reset": reset_button_for(
hass,
sig,
[group["entries"][eid] for eid in ids],
device_id,
integration,
device_catalog_keys,
tk_authoritative=catalog.translation_keys_authoritative,
),
}
)
# Duties already present on the target BY NAME (any language) are not
# proposed — covers manually created calendar tasks whose trigger
# watches no entity (the entity-watched exclusion misses them); since
# 2.94 they are listed as ``already`` so the dialog can say so.
tasks, already = annotate_for_target(hass, raw, target, device_name=device_name)
tasks, already = annotate_for_target(hass, raw, target, device_name=device_name, integration=integration)
if not tasks and targets is None:
continue
tasks.sort(key=lambda t: (t["task_name"], t["direction"]))