Updated apps
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""Device discovery over the assembled signature catalog."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
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 ._model import (
|
||||
_entity_matches,
|
||||
_entity_unit,
|
||||
_threshold_for,
|
||||
_unit_compatible,
|
||||
task_name_variants,
|
||||
)
|
||||
from ._registry import SIGNATURES
|
||||
|
||||
|
||||
def discover_integration_setups(hass: HomeAssistant) -> list[dict[str, Any]]:
|
||||
"""Devices of catalogued integrations with their matchable task wiring.
|
||||
|
||||
Groups matched entities per device; carries the maintenance object already
|
||||
attached to the device (if any) so adoption can extend it instead of
|
||||
creating a duplicate. Entities already watched by some task's trigger are
|
||||
skipped — re-running discovery never proposes what is already wired.
|
||||
"""
|
||||
from ...templates import localize_template_text
|
||||
from ..i18n import normalize_language
|
||||
from ..problem_sensors import _adopted_entity_ids, _object_by_device
|
||||
|
||||
lang = normalize_language(hass)
|
||||
ent_reg = er.async_get(hass)
|
||||
dev_reg = dr.async_get(hass)
|
||||
area_reg = ar.async_get(hass)
|
||||
already_watched = _adopted_entity_ids(hass)
|
||||
by_device = _object_by_device(hass)
|
||||
|
||||
# Collect the enabled registry entities of cataloged integrations per
|
||||
# (device, integration) first — the device-type gates need the device's
|
||||
# FULL entity list (siblings identify the appliance type).
|
||||
by_device_integration: dict[tuple[str, str], list[er.RegistryEntry]] = {}
|
||||
for entry in ent_reg.entities.values():
|
||||
if SIGNATURES.get(entry.platform) is None or not entry.device_id:
|
||||
continue
|
||||
if entry.disabled_by is not None:
|
||||
continue
|
||||
by_device_integration.setdefault((entry.device_id, entry.platform), []).append(entry)
|
||||
|
||||
# device_id → {(integration, task_name, direction): {sig, entity_ids}}.
|
||||
# The direction is part of the key so an integration that ships one task
|
||||
# name in two directions (LG ThinQ filter: hours vs percent) stays split.
|
||||
matched: dict[str, dict[tuple[str, str, str], dict[str, Any]]] = {}
|
||||
for (device_id, integration), entries in by_device_integration.items():
|
||||
catalog = SIGNATURES[integration]
|
||||
device = dev_reg.async_get(device_id)
|
||||
model = ((device.model or "") if device else "").lower()
|
||||
for sig in catalog.tasks:
|
||||
# Device-type gates: registry model substring and/or a
|
||||
# type-identifying sibling entity (watched siblings still count —
|
||||
# only the match TARGET must be unwatched).
|
||||
if sig.models and not any(m.lower() in model for m in sig.models):
|
||||
continue
|
||||
if sig.models_exclude and any(m.lower() in model for m in sig.models_exclude):
|
||||
continue
|
||||
if sig.require_sibling_keys and not any(
|
||||
any(_entity_matches(e, key) for key in sig.require_sibling_keys) for e in entries
|
||||
):
|
||||
continue
|
||||
for entry in entries:
|
||||
if entry.domain != sig.entity_domain:
|
||||
continue
|
||||
if entry.entity_id in already_watched:
|
||||
continue
|
||||
if not _unit_compatible(sig.direction, _entity_unit(hass, entry)):
|
||||
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):
|
||||
continue
|
||||
group = matched.setdefault(device_id, {}).setdefault(
|
||||
(integration, sig.task_name, sig.direction),
|
||||
{"sig": sig, "entity_ids": []},
|
||||
)
|
||||
group["entity_ids"].append(entry.entity_id)
|
||||
# One source entity may back SEVERAL duties (a mower's hours
|
||||
# counter drives blades AND undercarriage). Adopting any duty
|
||||
# marks the entity watched — adopt-all is the default,
|
||||
# deselecting a duty forfeits its later proposal.
|
||||
|
||||
out: list[dict[str, Any]] = []
|
||||
for device_id, sig_map in matched.items():
|
||||
device = dev_reg.async_get(device_id)
|
||||
if device is None:
|
||||
continue
|
||||
device_name = device.name_by_user or device.name or device_id
|
||||
area_name = ""
|
||||
if device.area_id and (area := area_reg.async_get_area(device.area_id)):
|
||||
area_name = area.name
|
||||
integration = next(iter(sig_map))[0]
|
||||
catalog = SIGNATURES[integration]
|
||||
suggested = by_device.get(device_id)
|
||||
# Duties already present on the bound object BY NAME (any language) are
|
||||
# not re-proposed — covers manually created calendar tasks whose
|
||||
# trigger watches no entity (the entity-watched exclusion misses them).
|
||||
existing_names: set[str] = set()
|
||||
if suggested and (target := hass.config_entries.async_get_entry(suggested["entry_id"])):
|
||||
from ...const import CONF_TASKS
|
||||
|
||||
existing_names = {str(t.get("name", "")).lower() for t in target.data.get(CONF_TASKS, {}).values()}
|
||||
tasks = []
|
||||
for (_integ, task_name, direction), group in sig_map.items():
|
||||
entity_ids = sorted(group["entity_ids"])
|
||||
if not entity_ids:
|
||||
continue
|
||||
if existing_names and existing_names & task_name_variants(task_name):
|
||||
continue
|
||||
tasks.append(
|
||||
{
|
||||
# task_name stays the EN catalog key (adopt selections
|
||||
# match on it); the dialog renders the localized twin.
|
||||
"task_name": task_name,
|
||||
"task_name_localized": localize_template_text(task_name, lang) or task_name,
|
||||
"entity_ids": entity_ids,
|
||||
"threshold": _threshold_for(group["sig"], hass, entity_ids[0]),
|
||||
"direction": direction,
|
||||
}
|
||||
)
|
||||
if not tasks:
|
||||
continue
|
||||
tasks.sort(key=lambda t: (t["task_name"], t["direction"]))
|
||||
out.append(
|
||||
{
|
||||
"device_id": device_id,
|
||||
"device_name": device_name,
|
||||
"area_name": area_name,
|
||||
"integration": integration,
|
||||
"integration_name": catalog.name,
|
||||
"suggested_entry_id": suggested["entry_id"] if suggested else None,
|
||||
"suggested_object_name": suggested["name"] if suggested else device_name,
|
||||
"tasks": tasks,
|
||||
}
|
||||
)
|
||||
out.sort(key=lambda s: (s["integration_name"], s["device_name"]))
|
||||
return out
|
||||
Reference in New Issue
Block a user