93 files
This commit is contained in:
@@ -261,8 +261,33 @@ def _build_task_summary(
|
||||
}
|
||||
|
||||
|
||||
def _build_object_response(hass: HomeAssistant, entry: ConfigEntry, coordinator_data: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""Build a full object response dict."""
|
||||
_EMPTY_LIST: list[Any] = []
|
||||
_EMPTY_DICT: dict[str, Any] = {}
|
||||
|
||||
|
||||
def _strip_empty(d: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Drop keys whose value is None, [] or {} (compact mode, perf wave 2 #3).
|
||||
|
||||
Measured on a 121-task instance: 52 % of the objects payload was keys
|
||||
carrying one of these three empties. Only clients that OPT IN via
|
||||
``compact: true`` get stripped responses — they hydrate the handful of
|
||||
list/dict-typed keys back client-side (helpers/hydrate-objects.ts; the
|
||||
two lists are pinned against each other by test_ws_compact_mode).
|
||||
Scalars stay droppable without a table because absent and null read the
|
||||
same through JS ``== null`` / ``||`` access. False, 0 and "" are kept —
|
||||
they are meaningful values, not absences.
|
||||
"""
|
||||
return {k: v for k, v in d.items() if not (v is None or v in (_EMPTY_LIST, _EMPTY_DICT))}
|
||||
|
||||
|
||||
def _build_object_response(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
coordinator_data: dict[str, Any] | None,
|
||||
*,
|
||||
compact: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a full object response dict (compact: empty keys stripped)."""
|
||||
from ..const import slugify_object_name
|
||||
|
||||
obj_data = entry.data.get(CONF_OBJECT, {})
|
||||
@@ -311,7 +336,7 @@ def _build_object_response(hass: HomeAssistant, entry: ConfigEntry, coordinator_
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
resp: dict[str, Any] = {
|
||||
"entry_id": entry.entry_id,
|
||||
"object": {
|
||||
"id": obj_data.get("id", ""),
|
||||
@@ -344,6 +369,12 @@ def _build_object_response(hass: HomeAssistant, entry: ConfigEntry, coordinator_
|
||||
# v2.20 (N1) replace-flow lineage, both directions.
|
||||
"predecessor_entry_id": obj_data.get("predecessor_entry_id"),
|
||||
"replaced_by_entry_id": obj_data.get("replaced_by_entry_id"),
|
||||
# Battery-fleet markers (field-completeness audit, #50 class):
|
||||
# the fleet flag existed only task-level in the response while
|
||||
# the OBJECT flag drives find_fleet_entry — consumers (and our
|
||||
# own visual harness) had to detect the fleet via a task.
|
||||
"battery_fleet": obj_data.get("battery_fleet", False),
|
||||
"battery_fleet_excluded": obj_data.get("battery_fleet_excluded", []),
|
||||
# (roadmap P2) count of attached documents (files + web-links) for
|
||||
# the objects-table paperclip badge; computed, not persisted.
|
||||
"document_count": document_count,
|
||||
@@ -365,6 +396,11 @@ def _build_object_response(hass: HomeAssistant, entry: ConfigEntry, coordinator_
|
||||
"tasks": tasks,
|
||||
"parts": parts_payload,
|
||||
}
|
||||
if compact:
|
||||
resp["object"] = _strip_empty(resp["object"])
|
||||
resp["tasks"] = [_strip_empty(t) for t in tasks]
|
||||
resp = _strip_empty(resp)
|
||||
return resp
|
||||
|
||||
|
||||
def _get_global_entry(hass: HomeAssistant) -> ConfigEntry | None:
|
||||
|
||||
@@ -292,6 +292,10 @@ async def ws_get_statistics(
|
||||
vol.Required("type"): "maintenance_supporter/subscribe",
|
||||
# 2.52 delta protocol opt-in — see the handler docstring.
|
||||
vol.Optional("deltas", default=False): bool,
|
||||
# Compact payloads (perf wave 2 #3): same opt-in + hydration contract
|
||||
# as the `objects` read — empty keys stripped from every snapshot and
|
||||
# delta this subscription ships.
|
||||
vol.Optional("compact", default=False): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@@ -320,6 +324,7 @@ async def ws_subscribe(
|
||||
import json
|
||||
|
||||
deltas: bool = msg.get("deltas", False)
|
||||
compact: bool = msg.get("compact", False)
|
||||
attached_entry_ids: set[str] = set()
|
||||
unsub_callbacks: list[Callable[[], None]] = []
|
||||
dirty: set[str] = set()
|
||||
@@ -330,7 +335,7 @@ async def ws_subscribe(
|
||||
def _build(entry: Any) -> dict[str, Any]:
|
||||
rd = _get_runtime_data(hass, entry.entry_id)
|
||||
coord_data = rd.coordinator.data if rd and rd.coordinator else None
|
||||
return _build_object_response(hass, entry, coord_data)
|
||||
return _build_object_response(hass, entry, coord_data, compact=compact)
|
||||
|
||||
def _hash(resp: dict[str, Any]) -> int:
|
||||
return hash(json.dumps(resp, sort_keys=True, default=str))
|
||||
|
||||
@@ -128,7 +128,16 @@ def _validate_device_link(
|
||||
return True
|
||||
|
||||
|
||||
@websocket_api.websocket_command({vol.Required("type"): "maintenance_supporter/objects"})
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "maintenance_supporter/objects",
|
||||
# Opt-in (perf wave 2 #3): strip keys whose value is None/[]/{} from
|
||||
# the object + task summaries. Own panel/card pass this and hydrate
|
||||
# the list/dict keys back; consumers that don't ask keep the full,
|
||||
# every-field shape (#50 contract untouched).
|
||||
vol.Optional("compact", default=False): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def ws_get_objects(
|
||||
hass: HomeAssistant,
|
||||
@@ -136,12 +145,13 @@ async def ws_get_objects(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return all maintenance objects with tasks and computed status."""
|
||||
compact = bool(msg.get("compact", False))
|
||||
entries = _get_object_entries(hass)
|
||||
result = []
|
||||
for entry in entries:
|
||||
rd = _get_runtime_data(hass, entry.entry_id)
|
||||
coord_data = rd.coordinator.data if rd and rd.coordinator else None
|
||||
result.append(_build_object_response(hass, entry, coord_data))
|
||||
result.append(_build_object_response(hass, entry, coord_data, compact=compact))
|
||||
|
||||
connection.send_result(msg["id"], {"objects": result})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user