393 files
This commit is contained in:
@@ -3,9 +3,12 @@
|
||||
``integration_setups/discover`` lists devices of catalogued integrations
|
||||
(helpers/integration_signatures — every signature verified against the
|
||||
integration's source) whose consumable entities can back maintenance tasks;
|
||||
``integration_setups/adopt`` creates the object (or extends the existing one on
|
||||
that device) with the tasks and their sensor triggers PRE-WIRED. Discovery is
|
||||
read; adoption is write, mirroring problem-sensor adoption.
|
||||
``integration_setups/preview`` (2.94) recomputes one device's proposals against
|
||||
another target object — the dialog's before/after when the user picks a
|
||||
different object; ``integration_setups/adopt`` creates the object (or extends
|
||||
the existing one on that device) with the tasks and their sensor triggers
|
||||
PRE-WIRED. Discovery and preview are read; adoption is write, mirroring
|
||||
problem-sensor adoption.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,14 +20,15 @@ import voluptuous as vol
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from ..const import CONF_OBJECT, CONF_TASKS, DOMAIN, MAX_ID_LENGTH, MAX_NAME_LENGTH
|
||||
from ..helpers.aggregate import is_object_entry
|
||||
from ..const import CONF_OBJECT, CONF_TASKS, DOMAIN, MAX_NAME_LENGTH
|
||||
from ..helpers.integration_signatures import (
|
||||
SIGNATURES,
|
||||
build_setup_trigger,
|
||||
discover_integration_setups,
|
||||
)
|
||||
from ..helpers.permissions import require_write
|
||||
from . import ID_FIELD
|
||||
from .adopt_batch import AdoptBatch
|
||||
|
||||
|
||||
@websocket_api.websocket_command({vol.Required("type"): f"{DOMAIN}/integration_setups/discover"})
|
||||
@@ -38,11 +42,45 @@ async def ws_discover_integration_setups(
|
||||
connection.send_result(msg["id"], {"setups": discover_integration_setups(hass)})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): f"{DOMAIN}/integration_setups/preview",
|
||||
vol.Required("device_id"): ID_FIELD,
|
||||
# An existing object as the target; omitted / null = a new object.
|
||||
vol.Optional("entry_id"): vol.Any(ID_FIELD, None),
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def ws_preview_integration_setup(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""One device's proposals judged against the chosen target (2.94): what
|
||||
the target already has (``already``), what it likely has under another
|
||||
name (``covered_by``) and how many tasks it holds now — the dialog's
|
||||
before/after. Read-only; adopt re-runs discovery itself."""
|
||||
from ..helpers.aggregate import is_object_entry
|
||||
|
||||
entry_id = msg.get("entry_id") or ""
|
||||
if entry_id and not is_object_entry(hass.config_entries.async_get_entry(entry_id)):
|
||||
connection.send_error(msg["id"], "not_found", "Target object not found")
|
||||
return
|
||||
setup = next(
|
||||
(s for s in discover_integration_setups(hass, targets={msg["device_id"]: entry_id}) if s["device_id"] == msg["device_id"]),
|
||||
None,
|
||||
)
|
||||
if setup is None:
|
||||
connection.send_error(msg["id"], "not_found", "No suggestion for this device")
|
||||
return
|
||||
connection.send_result(msg["id"], setup)
|
||||
|
||||
|
||||
_SELECTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("device_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
|
||||
vol.Required("device_id"): ID_FIELD,
|
||||
# Existing target object; omit to create a fresh object for the device.
|
||||
vol.Optional("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
|
||||
vol.Optional("entry_id"): ID_FIELD,
|
||||
vol.Optional("object_name"): vol.All(str, vol.Length(min=1, max=MAX_NAME_LENGTH)),
|
||||
# Subset of suggested task names to adopt; omit = all suggested.
|
||||
vol.Optional("task_names"): vol.All(
|
||||
@@ -75,26 +113,21 @@ async def ws_adopt_integration_setups(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Create sensor-wired maintenance tasks for the selected suggestions."""
|
||||
from ..export import object_entries
|
||||
from ..helpers.i18n import normalize_language
|
||||
from ..templates import localize_template_text
|
||||
from .objects import async_create_object
|
||||
from .tasks_persist import async_persist_task
|
||||
|
||||
lang = normalize_language(hass)
|
||||
# Re-run discovery server-side: the wiring (entities, thresholds) comes
|
||||
# from the verified catalog, never from the client.
|
||||
setups = {s["device_id"]: s for s in discover_integration_setups(hass)}
|
||||
|
||||
tasks_created = 0
|
||||
objects_created = 0
|
||||
errors: list[dict[str, str]] = []
|
||||
batch = AdoptBatch(hass)
|
||||
|
||||
for sel in msg["selections"]:
|
||||
device_id = sel["device_id"]
|
||||
setup = setups.get(device_id)
|
||||
if setup is None:
|
||||
errors.append({"device_id": device_id, "reason": "no suggestion for this device"})
|
||||
batch.errors.append({"device_id": device_id, "reason": "no suggestion for this device"})
|
||||
continue
|
||||
wanted = set(sel.get("task_names") or [t["task_name"] for t in setup["tasks"]])
|
||||
# Keyed by (task_name, direction): one integration can ship a task name
|
||||
@@ -103,25 +136,18 @@ async def ws_adopt_integration_setups(
|
||||
sig_by_key = {
|
||||
(s.task_name, s.direction): s for s in SIGNATURES[setup["integration"]].tasks
|
||||
}
|
||||
created_entry_id: str | None = None
|
||||
batch.begin()
|
||||
try:
|
||||
entry_id = sel.get("entry_id") or setup["suggested_entry_id"]
|
||||
if not entry_id:
|
||||
entry_id = await async_create_object(
|
||||
hass,
|
||||
entry_id = await batch.create_object(
|
||||
name=sel.get("object_name") or setup["suggested_object_name"],
|
||||
ha_device_id=device_id,
|
||||
)
|
||||
created_entry_id = entry_id
|
||||
objects_created += 1
|
||||
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
# Same guard as websocket._load_object_entry: the global settings
|
||||
# entry is NOT a valid adoption target — async_persist_task writes
|
||||
# CONF_TASKS + CONF_OBJECT["task_ids"] into whatever entry it is
|
||||
# handed, so a client-supplied global entry_id would corrupt it.
|
||||
if not is_object_entry(entry):
|
||||
errors.append({"device_id": device_id, "reason": "target object not found"})
|
||||
entry = batch.target_entry(entry_id)
|
||||
if entry is None:
|
||||
batch.errors.append({"device_id": device_id, "reason": "target object not found"})
|
||||
continue
|
||||
|
||||
# #105: adopting into a user-picked existing object that isn't
|
||||
@@ -165,8 +191,7 @@ async def ws_adopt_integration_setups(
|
||||
baseline = baselines.get(task["task_name"])
|
||||
if baseline is not None and sig.direction == "usage_delta":
|
||||
trigger["trigger_baseline_value"] = float(baseline)
|
||||
await async_persist_task(
|
||||
hass,
|
||||
await batch.persist_task(
|
||||
entry,
|
||||
{
|
||||
"id": uuid4().hex,
|
||||
@@ -180,19 +205,10 @@ async def ws_adopt_integration_setups(
|
||||
"trigger_config": trigger,
|
||||
},
|
||||
)
|
||||
tasks_created += 1
|
||||
except (ValueError, KeyError) as err:
|
||||
errors.append({"device_id": device_id, "reason": str(err)})
|
||||
if created_entry_id is not None:
|
||||
objects_created -= 1
|
||||
if hass.config_entries.async_get_entry(created_entry_id) is not None:
|
||||
await hass.config_entries.async_remove(created_entry_id)
|
||||
# Removes an object created for this device together with the
|
||||
# tasks already persisted into it — and un-counts both (the
|
||||
# tasks were over-reported before the DRY audit 2026-09-26).
|
||||
await batch.fail({"device_id": device_id, "reason": str(err)})
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"tasks_created": tasks_created,
|
||||
"objects_created": objects_created,
|
||||
"total": len(object_entries(hass)),
|
||||
}
|
||||
if errors:
|
||||
result["errors"] = errors
|
||||
connection.send_result(msg["id"], result)
|
||||
connection.send_result(msg["id"], batch.result())
|
||||
|
||||
Reference in New Issue
Block a user