429 files

This commit is contained in:
Home Assistant Version Control committed 2026-08-30 02:43:31 +00:00
1 parent 472c19bceb
commit 963cce81a7
566 files changed
+50311 -47616

No files matched your search

@@ -14,6 +14,11 @@ from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant
from ..const import (
BATTERY_FLEET_EXCLUDED,
BATTERY_FLEET_INCLUDED,
BATTERY_FLEET_OBJECT_FLAG,
BATTERY_FLEET_TASK_FLAG,
BATTERY_FLEET_TRACK_SELF_CHARGING,
CONF_OBJECT,
CONF_OBJECT_MANUFACTURER,
CONF_OBJECT_MODEL,
@@ -89,6 +94,57 @@ def _sanitize_history(history: Any) -> list[dict[str, Any]]:
return out
def _import_fleet_identity(
hass: HomeAssistant,
obj_data: dict[str, Any],
import_obj: dict[str, Any],
obj_name: str,
) -> bool:
"""Restore the battery-fleet markers onto ``import_obj``; True if it is the fleet.
The exported flag is honoured only while this instance has NO fleet yet
(``find_fleet_entry`` returns the FIRST flagged entry, so a second flagged
object would silently shadow or be shadowed by the existing one). The
exclude/include lists are re-validated like the live WS writes: entity
ids only, deduped + sorted, capped at ``FLEET_LIST_CAP``.
"""
if obj_data.get(BATTERY_FLEET_OBJECT_FLAG) is not True:
return False
from homeassistant.core import valid_entity_id
from ..helpers.battery_fleet_setup import FLEET_LIST_CAP, find_fleet_entry
if find_fleet_entry(hass) is not None:
_LOGGER.warning(
"JSON import: %r is flagged as the Battery Fleet but this instance already has one — importing it as a plain object",
obj_name,
)
return False
import_obj[BATTERY_FLEET_OBJECT_FLAG] = True
for key in (BATTERY_FLEET_EXCLUDED, BATTERY_FLEET_INCLUDED):
raw_list = obj_data.get(key)
if not isinstance(raw_list, list):
continue
cleaned = sorted({e.strip() for e in raw_list if isinstance(e, str) and valid_entity_id(e.strip())})
if cleaned:
import_obj[key] = cleaned[:FLEET_LIST_CAP]
if obj_data.get(BATTERY_FLEET_TRACK_SELF_CHARGING) is True:
import_obj[BATTERY_FLEET_TRACK_SELF_CHARGING] = True
return True
def _keep_fleet_part_id(is_fleet: bool, old_id: str) -> bool:
"""Whether an imported part keeps its id instead of getting a fresh uuid.
Only the fleet's deterministic type-part ids (``batt_<type>``, minted by
battery_fleet_setup._type_part) — every other part id is re-minted so an
import can never collide with or impersonate an existing part.
"""
return is_fleet and old_id.startswith("batt_") and len(old_id) <= MAX_ID_LENGTH
@websocket_api.websocket_command({vol.Required("type"): f"{DOMAIN}/version"})
@websocket_api.async_response
async def ws_version(hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]) -> None:
@@ -557,12 +613,26 @@ async def ws_import_json(
"paused_until": _iso_marker(obj_data.get("paused_until")),
"predecessor_entry_id": obj_data.get("predecessor_entry_id"),
"replaced_by_entry_id": obj_data.get("replaced_by_entry_id"),
# Object-level archive marker — same presence-means-archived
# semantics as paused_at, so it gets the same ISO validation. Its
# tasks carry their own archived_* pair (mirrored below).
"archived_at": _iso_marker(obj_data.get("archived_at")),
"task_ids": [],
}
# Battery fleet identity (object flag + exclude/include lists + the
# self-charging opt-in). The fleet is ONE object by invariant
# (find_fleet_entry returns the first flagged entry), so the flag is
# only restored when this instance has no fleet yet — otherwise the
# payload imports as a plain object (fresh part ids, no task flag).
is_fleet = _import_fleet_identity(hass, obj_data, import_obj, obj_name)
# Spare parts: regenerate ids (like tasks) and remember the mapping so
# task-side links (consumes_parts / part_ref) can be rewritten below.
# Stock is dynamic Store state — collected here, written after setup.
# Fleet type-parts keep their deterministic ``batt_<type>`` ids: the
# fleet reconcile / mark-replaced paths key on them, so a re-minted
# uuid would orphan the whole battery-type ↔ part mapping.
from uuid import uuid4 as _uuid4
part_id_map: dict[str, str] = {}
@@ -574,7 +644,7 @@ async def ws_import_json(
if not isinstance(part_entry, dict) or not (part_entry.get("name") or "").strip():
continue
old_id = str(part_entry.get("id") or "")
new_id = _uuid4().hex
new_id = old_id if _keep_fleet_part_id(is_fleet, old_id) else _uuid4().hex
pdata = {k: v for k, v in part_entry.items() if k != "stock"}
pdata["id"] = new_id
# Drop a non-http(s) product_url — the WS write path validates it
@@ -597,6 +667,7 @@ async def ws_import_json(
# old task id → new id, so document task-links (task_ids) can be
# remapped onto the freshly generated tasks (mirrors part_id_map).
task_id_map: dict[str, str] = {}
fleet_task_seen = False
tasks_list = obj_entry.get("tasks", [])
if not isinstance(tasks_list, list):
tasks_list = []
@@ -642,6 +713,7 @@ async def ws_import_json(
"documentation_url",
"custom_icon",
"nfc_tag_id",
"require_tag_scan",
"responsible_user_id",
"entity_slug",
"trigger_config",
@@ -667,6 +739,13 @@ async def ws_import_json(
if val is not None:
task_data[key] = val
# The fleet's single aggregate task keeps its marker (detail view
# renders the battery section; the fleet reconcile repairs its
# trigger). Only ONE task may carry it, and only on the fleet.
if is_fleet and task_entry.get(BATTERY_FLEET_TASK_FLAG) is True and not fleet_task_seen:
task_data[BATTERY_FLEET_TASK_FLAG] = True
fleet_task_seen = True
# In-cycle checklist ticks: keyed by item TEXT so they survive the
# id regeneration; keys are filtered against the imported checklist
# exactly like the live checklist_progress WS write. Rides