Merge Home & Phone into a three-tab Home that knows your house
Home is now Overview | My Home | Phone. Overview leads with quick-action tiles (focus, Do Not Disturb, health, snapshots, storage), keeps the findings card — updates fold in, the reclaim-space prompt is gone on purpose — and adds glance cards, the next calendar event, and weather. My Home groups every light by Home Assistant area: the helper gained an `areas` command (one REST template render, no websocket), and the rooms degrade to a flat list on setups without areas. The favorites editor and connection card moved intact. Phone gains a vitals strip — battery and cell signal read from KDE Connect's plugin D-Bus objects, where absence is data, not an error — beside ring, clipboard, send-a-file, and the BlueBubbles handoff. The retired home-phone id resolves to my-home forever via a new alias map in SettingsRoutes (with a hasOwnProperty guard so prototype names cannot leak into settingsPage). Storage no longer claims 0 B free — the old page read a field the disks helper never emitted. Contracts updated alongside; per the new workflow, the full suite runs once at the end of the redesign (see the test backlog note). Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -16,6 +16,7 @@ Singleton {
|
||||
// "loading" | "ready" | "degraded" | "unavailable"
|
||||
property string phase: "loading"
|
||||
property var catalog: []
|
||||
property var areas: []
|
||||
property var selectedEntities: []
|
||||
property bool stale: false
|
||||
property string lastError: ""
|
||||
@@ -38,6 +39,10 @@ Singleton {
|
||||
property var pendingFixtureTarget: null
|
||||
property int delayedFixtureExitCode: 0
|
||||
|
||||
// Catalog lights grouped by Home Assistant area, recomputed whenever either
|
||||
// half changes. Both halves are read here so the binding tracks them.
|
||||
readonly property var rooms: root.composeRooms(root.catalog, root.areas)
|
||||
|
||||
readonly property var visibleEntities: root.selectedEntities.slice(0, 4)
|
||||
readonly property int discoveredCount: root.catalog.length
|
||||
readonly property int configuredCount: root.selectedEntities.length
|
||||
@@ -56,6 +61,56 @@ Singleton {
|
||||
if (root.catalog.length === 0)
|
||||
root.phase = "loading";
|
||||
refreshProc.running = true;
|
||||
if (!areasProc.running)
|
||||
areasProc.running = true;
|
||||
}
|
||||
|
||||
// Rooms hold the SAME entity objects as the catalog, so a tile bound to a
|
||||
// room light sees exactly what the catalog says. Areas keep the order Home
|
||||
// Assistant reports them in; catalog lights no area claims fall into a
|
||||
// trailing "Other" bucket. With no usable areas the whole catalog becomes a
|
||||
// single unnamed bucket, which the UI renders as a flat list with no header.
|
||||
function composeRooms(catalog: var, areas: var): var {
|
||||
const lights = Array.isArray(catalog) ? catalog : [];
|
||||
const flat = [{ id: "", name: "", lights: lights.slice() }];
|
||||
if (!Array.isArray(areas) || areas.length === 0)
|
||||
return flat;
|
||||
|
||||
const catalogById = {};
|
||||
for (let index = 0; index < lights.length; index++)
|
||||
catalogById[lights[index].id] = lights[index];
|
||||
|
||||
const claimed = {};
|
||||
const grouped = [];
|
||||
for (let index = 0; index < areas.length; index++) {
|
||||
const area = areas[index];
|
||||
const entityIds = area && Array.isArray(area.entities) ? area.entities : [];
|
||||
const roomLights = [];
|
||||
for (let inner = 0; inner < entityIds.length; inner++) {
|
||||
const entity = catalogById[entityIds[inner]];
|
||||
if (!entity || claimed[entity.id] === true)
|
||||
continue;
|
||||
claimed[entity.id] = true;
|
||||
roomLights.push(entity);
|
||||
}
|
||||
if (roomLights.length === 0)
|
||||
continue;
|
||||
grouped.push({
|
||||
id: String(area.id || ""),
|
||||
name: String(area.name || ""),
|
||||
lights: roomLights
|
||||
});
|
||||
}
|
||||
|
||||
// Areas that name nothing in the catalog leave every light an orphan;
|
||||
// one "Other" heading over the whole list is worse than no heading.
|
||||
if (grouped.length === 0)
|
||||
return flat;
|
||||
|
||||
const orphans = lights.filter(entity => claimed[entity.id] !== true);
|
||||
if (orphans.length > 0)
|
||||
grouped.push({ id: "", name: "Other", lights: orphans });
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function consumeCatalog(text: string): void {
|
||||
@@ -91,6 +146,26 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Rooms are a nicety layered over the catalog: a Home Assistant whose
|
||||
// template endpoint is unavailable still gets its lights. A failure here
|
||||
// only empties `areas` -- it never touches `phase`, `stale`, or `lastError`.
|
||||
function consumeAreas(text: string): void {
|
||||
if (root.fixtureMode)
|
||||
return;
|
||||
|
||||
let result = null;
|
||||
try {
|
||||
result = JSON.parse(text);
|
||||
} catch (error) {
|
||||
root.areas = [];
|
||||
return;
|
||||
}
|
||||
|
||||
root.areas = result && result.ok === true && Array.isArray(result.areas)
|
||||
? result.areas
|
||||
: [];
|
||||
}
|
||||
|
||||
function rebuildSelection(): void {
|
||||
const favorites = root.fixtureMode
|
||||
? root.fixtureFavorites
|
||||
@@ -364,6 +439,16 @@ Singleton {
|
||||
];
|
||||
}
|
||||
|
||||
// Deliberately leaves light.fixture_all and light.fixture_corner unclaimed
|
||||
// so fixtures exercise the trailing "Other" bucket as well as the rooms.
|
||||
function fixtureAreas(): var {
|
||||
return [
|
||||
{ id: "kitchen", name: "Kitchen", entities: ["light.fixture_kitchen"] },
|
||||
{ id: "living_room", name: "Living room", entities: ["light.fixture_living", "light.fixture_desk"] },
|
||||
{ id: "bedroom", name: "Bedroom", entities: ["light.fixture_bedroom", "light.fixture_hall"] }
|
||||
];
|
||||
}
|
||||
|
||||
function fixturePreferenceRecords(): var {
|
||||
return [
|
||||
{ id: "light.fixture_all", alias: " Whole home " },
|
||||
@@ -479,6 +564,7 @@ Singleton {
|
||||
: (name === "process-slow-actions" ? "slow-success" : "")));
|
||||
if (name === "unavailable") {
|
||||
root.catalog = [];
|
||||
root.areas = [];
|
||||
root.fixtureFavorites = [];
|
||||
root.rebuildSelection();
|
||||
root.phase = "unavailable";
|
||||
@@ -488,6 +574,7 @@ Singleton {
|
||||
}
|
||||
|
||||
root.catalog = root.fixtureEntities();
|
||||
root.areas = root.fixtureAreas();
|
||||
root.fixtureFavorites = root.fixturePreferenceRecords();
|
||||
if (name === "available-extra") {
|
||||
root.catalog = root.catalog.concat([{
|
||||
@@ -530,6 +617,7 @@ Singleton {
|
||||
root.fixtureProcessMode = "";
|
||||
root.phase = "loading";
|
||||
root.catalog = [];
|
||||
root.areas = [];
|
||||
root.selectedEntities = [];
|
||||
root.fixtureFavorites = [];
|
||||
root.stale = false;
|
||||
@@ -545,6 +633,14 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: areasProc
|
||||
command: [root.helperPath, "areas"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.consumeAreas(this.text)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: actionProc
|
||||
stdout: StdioCollector {
|
||||
|
||||
Reference in New Issue
Block a user