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
699 lines
25 KiB
QML
699 lines
25 KiB
QML
pragma Singleton
|
|
|
|
// Home Assistant state for the Control Center. Credentials and REST details
|
|
// remain behind the helper; QML composes its catalog with Panama preferences.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
import qs.config
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-home-assistant"
|
|
|
|
// "loading" | "ready" | "degraded" | "unavailable"
|
|
property string phase: "loading"
|
|
property var catalog: []
|
|
property var areas: []
|
|
property var selectedEntities: []
|
|
property bool stale: false
|
|
property string lastError: ""
|
|
property bool fixtureMode: false
|
|
property var fixtureFavorites: []
|
|
property string fixtureProcessMode: ""
|
|
|
|
property var actionQueue: []
|
|
property var activeAction: null
|
|
property var busyEntityIds: []
|
|
property var pendingBrightness: ({})
|
|
property var entityErrors: ({})
|
|
property string actionResponseText: ""
|
|
property bool actionStreamFinished: false
|
|
property bool actionExited: false
|
|
property int actionExitCode: 0
|
|
property bool fixtureTransitionDraining: false
|
|
property bool fixtureTransitionStreamFinished: false
|
|
property bool fixtureTransitionExited: false
|
|
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
|
|
readonly property bool actionProcessRunning: actionProc.running
|
|
|
|
// Temporary aliases keep the current Home controls usable until the shelf
|
|
// switches to the selected-entity and per-entity action interfaces.
|
|
readonly property var entities: root.selectedEntities
|
|
readonly property string busyEntityId: root.busyEntityIds.length > 0
|
|
? root.busyEntityIds[0]
|
|
: ""
|
|
|
|
function refresh(): void {
|
|
if (root.fixtureMode || refreshProc.running)
|
|
return;
|
|
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 {
|
|
if (root.fixtureMode)
|
|
return;
|
|
|
|
let result = null;
|
|
try {
|
|
result = JSON.parse(text);
|
|
} catch (error) {
|
|
result = { ok: false, error: "invalid-response" };
|
|
}
|
|
|
|
if (result.ok === true) {
|
|
root.catalog = Array.isArray(result.entities) ? result.entities : [];
|
|
HomePreferences.initialize(Array.isArray(result.legacyEntityIds)
|
|
? result.legacyEntityIds
|
|
: []);
|
|
root.rebuildSelection();
|
|
root.phase = "ready";
|
|
root.stale = false;
|
|
root.lastError = "";
|
|
return;
|
|
}
|
|
|
|
root.lastError = String(result.error || "unreachable");
|
|
if (root.catalog.length > 0) {
|
|
root.phase = "degraded";
|
|
root.stale = true;
|
|
} else {
|
|
root.phase = "unavailable";
|
|
root.stale = false;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
: HomePreferences.favorites;
|
|
const catalogById = {};
|
|
for (let index = 0; index < root.catalog.length; index++) {
|
|
const entity = root.catalog[index];
|
|
catalogById[entity.id] = entity;
|
|
}
|
|
|
|
const nextSelection = [];
|
|
for (let index = 0; index < favorites.length; index++) {
|
|
const favorite = favorites[index];
|
|
const entity = catalogById[favorite.id];
|
|
const alias = String(favorite.alias || "").trim();
|
|
if (entity) {
|
|
nextSelection.push({
|
|
id: entity.id,
|
|
sourceName: entity.sourceName,
|
|
name: alias || entity.sourceName,
|
|
state: entity.state,
|
|
available: entity.available,
|
|
active: entity.active,
|
|
dimmable: entity.dimmable,
|
|
brightnessPct: entity.brightnessPct
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const sourceName = favorite.id.split(".")[1].split("_").join(" ");
|
|
nextSelection.push({
|
|
id: favorite.id,
|
|
sourceName,
|
|
name: alias || sourceName,
|
|
state: "unavailable",
|
|
available: false,
|
|
active: false,
|
|
dimmable: false,
|
|
brightnessPct: 0
|
|
});
|
|
}
|
|
root.selectedEntities = nextSelection;
|
|
}
|
|
|
|
Connections {
|
|
target: HomePreferences
|
|
function onFavoritesChanged(): void {
|
|
if (!root.fixtureMode)
|
|
root.rebuildSelection();
|
|
}
|
|
}
|
|
|
|
function isBusy(entityId: string): bool {
|
|
return root.busyEntityIds.indexOf(entityId) >= 0;
|
|
}
|
|
|
|
function pendingFor(entityId: string): int {
|
|
return Object.prototype.hasOwnProperty.call(root.pendingBrightness, entityId)
|
|
? root.pendingBrightness[entityId]
|
|
: -1;
|
|
}
|
|
|
|
function errorFor(entityId: string): string {
|
|
return String(root.entityErrors[entityId] || "");
|
|
}
|
|
|
|
function toggleEntity(entityId: string): void {
|
|
root.enqueueAction({ kind: "toggle", entityId });
|
|
}
|
|
|
|
function setBrightness(entityId: string, percent: int): void {
|
|
if (!Number.isInteger(percent) || percent < 0 || percent > 100)
|
|
return;
|
|
root.enqueueAction({ kind: "brightness", entityId, percent });
|
|
}
|
|
|
|
function enqueueAction(action: var): void {
|
|
if (root.fixtureTransitionDraining)
|
|
return;
|
|
if (!action || (action.kind !== "toggle" && action.kind !== "brightness"))
|
|
return;
|
|
if (typeof action.entityId !== "string" || action.entityId === "" || root.isBusy(action.entityId))
|
|
return;
|
|
|
|
const entity = root.catalog.find(candidate => candidate.id === action.entityId);
|
|
if (!entity || !entity.available || (action.kind === "brightness" && !entity.dimmable))
|
|
return;
|
|
|
|
const nextErrors = Object.assign({}, root.entityErrors);
|
|
delete nextErrors[action.entityId];
|
|
root.entityErrors = nextErrors;
|
|
root.busyEntityIds = root.busyEntityIds.concat([action.entityId]);
|
|
|
|
if (action.kind === "brightness") {
|
|
const nextPending = Object.assign({}, root.pendingBrightness);
|
|
nextPending[action.entityId] = action.percent;
|
|
root.pendingBrightness = nextPending;
|
|
}
|
|
|
|
if (root.fixtureMode && root.fixtureProcessMode === "") {
|
|
root.applyFixtureAction(action);
|
|
root.finishActionState(action.entityId);
|
|
return;
|
|
}
|
|
|
|
root.actionQueue = root.actionQueue.concat([action]);
|
|
root.startNextAction();
|
|
}
|
|
|
|
function startNextAction(): void {
|
|
if (root.fixtureTransitionDraining
|
|
|| actionProc.running
|
|
|| root.activeAction !== null
|
|
|| root.actionQueue.length === 0) {
|
|
return;
|
|
}
|
|
|
|
root.activeAction = root.actionQueue[0];
|
|
root.actionResponseText = "";
|
|
root.actionStreamFinished = false;
|
|
root.actionExited = false;
|
|
root.actionExitCode = 0;
|
|
if (root.fixtureProcessMode !== "") {
|
|
actionProc.command = root.fixtureActionCommand(root.activeAction);
|
|
} else {
|
|
actionProc.command = root.activeAction.kind === "brightness"
|
|
? [root.helperPath, "brightness", root.activeAction.entityId, String(root.activeAction.percent)]
|
|
: [root.helperPath, "toggle", root.activeAction.entityId];
|
|
}
|
|
actionProc.running = true;
|
|
}
|
|
|
|
function fixtureActionCommand(action: var): var {
|
|
if (root.fixtureProcessMode === "delayed-exit")
|
|
return ["/usr/bin/sh", "-c", "printf '%s' '{\"ok\":true}'; exit 7"];
|
|
if (root.fixtureProcessMode === "no-output"
|
|
&& action.entityId === "light.fixture_kitchen") {
|
|
return ["/usr/bin/sh", "-c", "sleep 0.5; exit 7"];
|
|
}
|
|
if (root.fixtureProcessMode === "slow-success")
|
|
return ["/usr/bin/sh", "-c", "sleep 2; printf '%s' '{\"ok\":true}'"];
|
|
return ["/usr/bin/sh", "-c", "sleep 0.5; printf '%s' '{\"ok\":true}'"];
|
|
}
|
|
|
|
function handleActionStreamFinished(text: string): void {
|
|
if (root.fixtureTransitionDraining) {
|
|
root.fixtureTransitionStreamFinished = true;
|
|
fixtureTransitionTimer.restart();
|
|
return;
|
|
}
|
|
if (root.activeAction === null || root.actionStreamFinished)
|
|
return;
|
|
root.actionResponseText = text;
|
|
root.actionStreamFinished = true;
|
|
actionCompletionTimer.restart();
|
|
}
|
|
|
|
function handleActionExited(exitCode: int): void {
|
|
// This fixture-only delay gives the contract a deterministic window
|
|
// where the process stopped but its exit bookkeeping is still pending.
|
|
if (root.fixtureMode
|
|
&& root.fixtureProcessMode === "delayed-exit"
|
|
&& root.activeAction !== null
|
|
&& !root.fixtureTransitionDraining) {
|
|
root.delayedFixtureExitCode = exitCode;
|
|
delayedFixtureExitTimer.restart();
|
|
return;
|
|
}
|
|
root.recordActionExited(exitCode);
|
|
}
|
|
|
|
function recordActionExited(exitCode: int): void {
|
|
if (root.fixtureTransitionDraining) {
|
|
root.fixtureTransitionExited = true;
|
|
fixtureTransitionTimer.restart();
|
|
return;
|
|
}
|
|
if (root.activeAction === null || root.actionExited)
|
|
return;
|
|
root.actionExited = true;
|
|
root.actionExitCode = exitCode;
|
|
actionCompletionTimer.restart();
|
|
}
|
|
|
|
function tryCompleteAction(): void {
|
|
if (root.activeAction === null)
|
|
return;
|
|
if (actionProc.running) {
|
|
actionCompletionTimer.restart();
|
|
return;
|
|
}
|
|
if (!root.actionExited || !root.actionStreamFinished)
|
|
return;
|
|
root.consumeAction(root.actionResponseText, root.actionExitCode);
|
|
}
|
|
|
|
function consumeAction(text: string, exitCode: int): void {
|
|
if (root.activeAction === null)
|
|
return;
|
|
|
|
const completedAction = root.activeAction;
|
|
let ok = false;
|
|
let errorCode = text === "" ? "action-failed" : "invalid-response";
|
|
try {
|
|
const result = JSON.parse(text);
|
|
ok = exitCode === 0 && result.ok === true;
|
|
errorCode = String(result.error || "action-failed");
|
|
} catch (error) {
|
|
// Empty output from a failed process is an action failure, while
|
|
// malformed non-empty output remains an invalid response.
|
|
}
|
|
|
|
actionCompletionTimer.stop();
|
|
root.actionQueue = root.actionQueue.slice(1);
|
|
root.activeAction = null;
|
|
root.finishActionState(completedAction.entityId);
|
|
|
|
const nextErrors = Object.assign({}, root.entityErrors);
|
|
if (ok) {
|
|
delete nextErrors[completedAction.entityId];
|
|
if (root.fixtureMode && root.fixtureProcessMode !== "")
|
|
root.applyFixtureAction(completedAction);
|
|
else
|
|
refreshDelay.restart();
|
|
} else {
|
|
nextErrors[completedAction.entityId] = errorCode;
|
|
}
|
|
root.entityErrors = nextErrors;
|
|
queueAdvanceTimer.restart();
|
|
}
|
|
|
|
function finishActionState(entityId: string): void {
|
|
root.busyEntityIds = root.busyEntityIds.filter(id => id !== entityId);
|
|
const nextPending = Object.assign({}, root.pendingBrightness);
|
|
delete nextPending[entityId];
|
|
root.pendingBrightness = nextPending;
|
|
}
|
|
|
|
function applyFixtureAction(action: var): void {
|
|
root.catalog = root.catalog.map(entity => {
|
|
if (entity.id !== action.entityId)
|
|
return entity;
|
|
if (action.kind === "brightness") {
|
|
return Object.assign({}, entity, {
|
|
state: action.percent === 0 ? "off" : "on",
|
|
active: action.percent > 0,
|
|
brightnessPct: action.percent
|
|
});
|
|
}
|
|
return Object.assign({}, entity, {
|
|
active: !entity.active,
|
|
state: entity.active ? "off" : "on"
|
|
});
|
|
});
|
|
root.rebuildSelection();
|
|
}
|
|
|
|
function open(): void {
|
|
Quickshell.execDetached([root.helperPath, "open"]);
|
|
}
|
|
|
|
function fixtureEntities(): var {
|
|
return [
|
|
{ id: "light.fixture_all", sourceName: "All lights", state: "on", available: true, active: true, dimmable: true, brightnessPct: 82 },
|
|
{ id: "light.fixture_kitchen", sourceName: "Kitchen", state: "on", available: true, active: true, dimmable: true, brightnessPct: 71 },
|
|
{ id: "light.fixture_living", sourceName: "Living room", state: "off", available: true, active: false, dimmable: true, brightnessPct: 36 },
|
|
{ id: "light.fixture_bedroom", sourceName: "Bedroom", state: "on", available: true, active: true, dimmable: true, brightnessPct: 48 },
|
|
{ id: "light.fixture_hall", sourceName: "Hall", state: "off", available: true, active: false, dimmable: false, brightnessPct: 0 },
|
|
{ id: "light.fixture_desk", sourceName: "Desk", state: "on", available: true, active: true, dimmable: true, brightnessPct: 24 },
|
|
{ id: "light.fixture_corner", sourceName: "Corner lamp", state: "unavailable", available: false, active: false, dimmable: false, brightnessPct: 0 }
|
|
];
|
|
}
|
|
|
|
// 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 " },
|
|
{ id: "light.fixture_kitchen", alias: "Kitchen island" },
|
|
{ id: "light.fixture_living", alias: "" },
|
|
{ id: "light.fixture_bedroom", alias: "" },
|
|
{ id: "light.fixture_hall", alias: "" },
|
|
{ id: "light.fixture_desk", alias: "Office" },
|
|
{ id: "light.fixture_corner", alias: "" }
|
|
];
|
|
}
|
|
|
|
function resetActionState(): void {
|
|
actionCompletionTimer.stop();
|
|
queueAdvanceTimer.stop();
|
|
root.actionQueue = [];
|
|
root.activeAction = null;
|
|
root.busyEntityIds = [];
|
|
root.pendingBrightness = {};
|
|
root.entityErrors = {};
|
|
root.actionResponseText = "";
|
|
root.actionStreamFinished = false;
|
|
root.actionExited = false;
|
|
root.actionExitCode = 0;
|
|
}
|
|
|
|
function beginFixtureTransition(): void {
|
|
if (root.fixtureTransitionDraining)
|
|
return;
|
|
|
|
const shouldDrain = root.fixtureMode
|
|
&& root.fixtureProcessMode !== ""
|
|
&& (actionProc.running || root.activeAction !== null);
|
|
const streamAlreadyFinished = root.actionStreamFinished;
|
|
const exitAlreadyObserved = root.actionExited;
|
|
|
|
fixtureTransitionTimer.stop();
|
|
root.fixtureTransitionDraining = shouldDrain;
|
|
root.fixtureTransitionStreamFinished = shouldDrain && streamAlreadyFinished;
|
|
root.fixtureTransitionExited = shouldDrain && exitAlreadyObserved;
|
|
root.resetActionState();
|
|
|
|
if (!shouldDrain)
|
|
return;
|
|
if (actionProc.running)
|
|
actionProc.running = false;
|
|
fixtureTransitionTimer.restart();
|
|
}
|
|
|
|
function tryFinishFixtureTransition(): void {
|
|
if (!root.fixtureTransitionDraining)
|
|
return;
|
|
if (actionProc.running) {
|
|
fixtureTransitionTimer.restart();
|
|
return;
|
|
}
|
|
if (!root.fixtureTransitionStreamFinished || !root.fixtureTransitionExited)
|
|
return;
|
|
|
|
const target = root.pendingFixtureTarget;
|
|
root.pendingFixtureTarget = null;
|
|
root.fixtureTransitionDraining = false;
|
|
root.fixtureTransitionStreamFinished = false;
|
|
root.fixtureTransitionExited = false;
|
|
if (target !== null)
|
|
root.installFixtureTarget(target);
|
|
queueAdvanceTimer.restart();
|
|
}
|
|
|
|
function applyFixture(name: string): void {
|
|
if (["ready", "available-extra", "stale", "stale-authentication", "stale-not-configured",
|
|
"unavailable", "missing-selected", "action-error",
|
|
"process-actions", "process-no-output", "process-delayed-exit",
|
|
"process-slow-actions"].indexOf(name) < 0)
|
|
return;
|
|
|
|
root.requestFixtureTarget({ fixture: true, name });
|
|
}
|
|
|
|
function requestFixtureTarget(target: var): void {
|
|
if (root.fixtureTransitionDraining) {
|
|
// Preserve the old process's callback guard and keep only the
|
|
// latest replacement requested during that drain.
|
|
root.pendingFixtureTarget = target;
|
|
root.resetActionState();
|
|
return;
|
|
}
|
|
|
|
root.pendingFixtureTarget = target;
|
|
root.beginFixtureTransition();
|
|
if (root.fixtureTransitionDraining)
|
|
return;
|
|
|
|
root.pendingFixtureTarget = null;
|
|
root.installFixtureTarget(target);
|
|
}
|
|
|
|
function installFixtureTarget(target: var): void {
|
|
if (target.fixture)
|
|
root.installFixture(target.name);
|
|
else
|
|
root.installLiveState();
|
|
}
|
|
|
|
function installFixture(name: string): void {
|
|
root.fixtureMode = true;
|
|
root.fixtureProcessMode = name === "process-actions"
|
|
? "success"
|
|
: (name === "process-no-output"
|
|
? "no-output"
|
|
: (name === "process-delayed-exit"
|
|
? "delayed-exit"
|
|
: (name === "process-slow-actions" ? "slow-success" : "")));
|
|
if (name === "unavailable") {
|
|
root.catalog = [];
|
|
root.areas = [];
|
|
root.fixtureFavorites = [];
|
|
root.rebuildSelection();
|
|
root.phase = "unavailable";
|
|
root.stale = false;
|
|
root.lastError = "not-configured";
|
|
return;
|
|
}
|
|
|
|
root.catalog = root.fixtureEntities();
|
|
root.areas = root.fixtureAreas();
|
|
root.fixtureFavorites = root.fixturePreferenceRecords();
|
|
if (name === "available-extra") {
|
|
root.catalog = root.catalog.concat([{
|
|
id: "light.fixture_guest",
|
|
sourceName: "Guest lamp",
|
|
state: "off",
|
|
available: true,
|
|
active: false,
|
|
dimmable: true,
|
|
brightnessPct: 0
|
|
}]);
|
|
}
|
|
if (name === "missing-selected") {
|
|
root.fixtureFavorites = root.fixtureFavorites.concat([
|
|
{ id: "light.fixture_missing", alias: "Porch" }
|
|
]);
|
|
}
|
|
root.rebuildSelection();
|
|
const isStale = ["stale", "stale-authentication", "stale-not-configured"].indexOf(name) >= 0;
|
|
root.phase = isStale ? "degraded" : "ready";
|
|
root.stale = isStale;
|
|
root.lastError = name === "stale-authentication"
|
|
? "authentication-required"
|
|
: (name === "stale-not-configured"
|
|
? "not-configured"
|
|
: (name === "stale" ? "unreachable" : ""));
|
|
if (name === "action-error") {
|
|
root.entityErrors = {
|
|
"light.fixture_kitchen": "request-failed"
|
|
};
|
|
}
|
|
}
|
|
|
|
function clearFixture(): void {
|
|
root.requestFixtureTarget({ fixture: false, name: "" });
|
|
}
|
|
|
|
function installLiveState(): void {
|
|
root.fixtureMode = false;
|
|
root.fixtureProcessMode = "";
|
|
root.phase = "loading";
|
|
root.catalog = [];
|
|
root.areas = [];
|
|
root.selectedEntities = [];
|
|
root.fixtureFavorites = [];
|
|
root.stale = false;
|
|
root.lastError = "";
|
|
root.refresh();
|
|
}
|
|
|
|
Process {
|
|
id: refreshProc
|
|
command: [root.helperPath, "catalog"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.consumeCatalog(this.text)
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: areasProc
|
|
command: [root.helperPath, "areas"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.consumeAreas(this.text)
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: actionProc
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.handleActionStreamFinished(this.text)
|
|
}
|
|
onExited: (code, status) => root.handleActionExited(code)
|
|
}
|
|
|
|
Timer {
|
|
id: actionCompletionTimer
|
|
interval: 0
|
|
onTriggered: root.tryCompleteAction()
|
|
}
|
|
|
|
Timer {
|
|
id: queueAdvanceTimer
|
|
interval: 0
|
|
onTriggered: {
|
|
if (root.fixtureTransitionDraining)
|
|
return;
|
|
if (actionProc.running) {
|
|
queueAdvanceTimer.restart();
|
|
return;
|
|
}
|
|
root.startNextAction();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: fixtureTransitionTimer
|
|
interval: 0
|
|
onTriggered: root.tryFinishFixtureTransition()
|
|
}
|
|
|
|
Timer {
|
|
id: delayedFixtureExitTimer
|
|
interval: 1000
|
|
onTriggered: root.recordActionExited(root.delayedFixtureExitCode)
|
|
}
|
|
|
|
Timer {
|
|
interval: 60000
|
|
repeat: true
|
|
running: ShellState.quickSettingsOpen && !root.fixtureMode
|
|
onTriggered: root.refresh()
|
|
}
|
|
|
|
Timer {
|
|
id: refreshDelay
|
|
interval: 350
|
|
onTriggered: root.refresh()
|
|
}
|
|
|
|
Component.onCompleted: root.refresh()
|
|
}
|