mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
"""Read-only projections of the stored plan (#256).
|
|
|
|
A full configuration is 70 KB on a real installation: three spaces and 139
|
|
markers. Every diagnostic question — "which space does this marker point at",
|
|
"how many markers are hidden", "what is on the first floor" — used to require
|
|
downloading all of it, because `houseplan/config/get` had no way to ask for
|
|
less.
|
|
|
|
The functions here are pure and deliberately unaware of Home Assistant: the
|
|
websocket handlers stay thin, and the interesting part is covered by tests that
|
|
run without the HA harness.
|
|
|
|
Two rules shape everything below.
|
|
|
|
* Absent parameter means *no projection*. The response must stay byte-for-byte
|
|
what it was before this module existed; no existing client may notice it.
|
|
* A projection never invents or repairs data. An unknown field name simply adds
|
|
nothing, and an unknown space yields an empty list rather than an error — the
|
|
caller distinguishes "no such thing" from "broken" by content, not by an
|
|
error code.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Iterable
|
|
|
|
|
|
def _names(value: Any) -> list[str] | None:
|
|
"""Normalise a field list; anything unusable means "no projection"."""
|
|
if not isinstance(value, (list, tuple)):
|
|
return None
|
|
names = [str(item) for item in value if isinstance(item, str) and item]
|
|
return names or None
|
|
|
|
|
|
def project_markers(markers: Any, marker_fields: Iterable[str] | None) -> Any:
|
|
"""Keep only the requested marker fields, plus `id`.
|
|
|
|
`id` is added unconditionally: a marker without it cannot be matched to
|
|
anything, so a projection that drops it produces an answer nobody can use.
|
|
"""
|
|
names = _names(marker_fields)
|
|
if names is None or not isinstance(markers, list):
|
|
return markers
|
|
keep = {"id", *names}
|
|
out = []
|
|
for marker in markers:
|
|
if not isinstance(marker, dict):
|
|
out.append(marker)
|
|
continue
|
|
out.append({key: value for key, value in marker.items() if key in keep})
|
|
return out
|
|
|
|
|
|
def project_config(
|
|
config: Any,
|
|
*,
|
|
space_id: str | None = None,
|
|
fields: Iterable[str] | None = None,
|
|
marker_fields: Iterable[str] | None = None,
|
|
) -> Any:
|
|
"""Return a narrowed copy of the configuration.
|
|
|
|
The original object is never mutated: the caller hands us the store's
|
|
document, and a projection that edited it in place would corrupt the very
|
|
thing it was asked to read.
|
|
"""
|
|
if not isinstance(config, dict):
|
|
return config
|
|
field_names = _names(fields)
|
|
if space_id is None and field_names is None and _names(marker_fields) is None:
|
|
return config
|
|
|
|
projected: dict[str, Any] = dict(config)
|
|
if space_id is not None:
|
|
spaces = projected.get("spaces")
|
|
projected["spaces"] = [
|
|
space for space in spaces
|
|
if isinstance(space, dict) and str(space.get("id", "")) == str(space_id)
|
|
] if isinstance(spaces, list) else spaces
|
|
if marker_fields is not None:
|
|
projected["markers"] = project_markers(projected.get("markers"), marker_fields)
|
|
if field_names is not None:
|
|
projected = {key: value for key, value in projected.items() if key in set(field_names)}
|
|
return projected
|
|
|
|
|
|
def project_layout(layout: Any, *, space_id: str | None = None) -> Any:
|
|
"""Keep only the positions of one space."""
|
|
if space_id is None or not isinstance(layout, dict):
|
|
return layout
|
|
wanted = str(space_id)
|
|
return {
|
|
key: position for key, position in layout.items()
|
|
if isinstance(position, dict) and str(position.get("s", "")) == wanted
|
|
}
|