pragma Singleton // Home Assistant state for the Control Center. Credentials and REST details // remain behind the helper; QML receives configured favourites only. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-home-assistant" // "loading" | "ready" | "degraded" | "unavailable" property string phase: "loading" property var entities: [] property bool stale: false property string busyEntityId: "" property string lastError: "" property bool fixtureMode: false readonly property var visibleEntities: root.entities.slice(0, 4) readonly property int configuredCount: root.entities.length function refresh(): void { if (root.fixtureMode || refreshProc.running) return; if (root.entities.length === 0) root.phase = "loading"; refreshProc.running = true; } function consumeSnapshot(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.entities = Array.isArray(result.entities) ? result.entities : []; root.phase = "ready"; root.stale = false; root.lastError = ""; return; } root.lastError = String(result.error || "unreachable"); if (root.entities.length > 0) { root.phase = "degraded"; root.stale = true; } else { root.phase = "unavailable"; root.stale = false; } } function toggleEntity(entityId: string): void { if (entityId === "" || root.busyEntityId !== "") return; if (root.fixtureMode) { root.entities = root.entities.map(entity => { if (entity.id !== entityId) return entity; return Object.assign({}, entity, { active: !entity.active, state: entity.active ? "off" : "on" }); }); return; } if (!root.entities.some(entity => entity.id === entityId)) return; root.busyEntityId = entityId; actionProc.command = [root.helperPath, "toggle", entityId]; actionProc.running = true; } function consumeAction(text: string): void { if (root.busyEntityId === "") return; let ok = false; let errorCode = "action-failed"; try { const result = JSON.parse(text); ok = result.ok === true; errorCode = String(result.error || errorCode); } catch (error) { errorCode = "invalid-response"; } root.busyEntityId = ""; if (ok) { root.lastError = ""; refreshDelay.restart(); } else { root.lastError = errorCode; if (root.entities.length > 0) { root.phase = "degraded"; root.stale = true; } } } function open(): void { Quickshell.execDetached([root.helperPath, "open"]); } function fixtureEntities(): var { return [ { id: "light.fixture_all", name: "All lights", domain: "light", state: "on", available: true, active: true }, { id: "light.fixture_kitchen", name: "Kitchen", domain: "light", state: "on", available: true, active: true }, { id: "light.fixture_living", name: "Living room", domain: "light", state: "off", available: true, active: false }, { id: "light.fixture_bedroom", name: "Bedroom", domain: "light", state: "on", available: true, active: true }, { id: "light.fixture_hall", name: "Hall", domain: "light", state: "off", available: true, active: false }, { id: "light.fixture_desk", name: "Desk", domain: "light", state: "off", available: true, active: false }, { id: "light.fixture_corner", name: "Corner lamp", domain: "light", state: "unavailable", available: false, active: false } ]; } function applyFixture(name: string): void { if (["ready", "stale", "unavailable"].indexOf(name) < 0) return; root.fixtureMode = true; root.busyEntityId = ""; if (name === "unavailable") { root.entities = []; root.phase = "unavailable"; root.stale = false; root.lastError = "not-configured"; return; } root.entities = root.fixtureEntities(); root.phase = name === "stale" ? "degraded" : "ready"; root.stale = name === "stale"; root.lastError = name === "stale" ? "unreachable" : ""; } function clearFixture(): void { root.fixtureMode = false; root.phase = "loading"; root.entities = []; root.stale = false; root.busyEntityId = ""; root.lastError = ""; root.refresh(); } Process { id: refreshProc command: [root.helperPath, "snapshot"] stdout: StdioCollector { onStreamFinished: root.consumeSnapshot(this.text) } } Process { id: actionProc stdout: StdioCollector { onStreamFinished: root.consumeAction(this.text) } onExited: (code, status) => { if (root.busyEntityId !== "") root.consumeAction(""); } } 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() }