190 lines
6.0 KiB
QML
190 lines
6.0 KiB
QML
pragma Singleton
|
|
|
|
// Home Assistant state for the Control Center. Credentials and REST details
|
|
// remain behind the helper; QML receives a normalized light catalog 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.map(entity => Object.assign({}, entity, {
|
|
name: entity.sourceName, domain: "light"
|
|
}))
|
|
: [];
|
|
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, "catalog"]
|
|
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()
|
|
}
|