Add private Home Assistant credentials settings

This commit is contained in:
Gabriel Brown
2026-08-18 06:14:07 -04:00
parent 39a26adcc2
commit de1b8a7673
6 changed files with 780 additions and 2 deletions
@@ -0,0 +1,120 @@
pragma Singleton
// Redacted Home Assistant configuration state. The helper is the only object
// that touches the private env file; QML never receives the stored token.
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-home-assistant-config"
property string url: ""
property var entities: []
property bool tokenConfigured: false
property bool configured: false
property string lastError: ""
property string pendingPayload: ""
readonly property bool busy: statusProc.running || writeProc.running
signal configurationSaved
function errorMessage(code: string): string {
switch (code) {
case "invalid-url": return "Enter an HTTP or HTTPS Home Assistant URL.";
case "invalid-token": return "The access token contains unsupported characters.";
case "invalid-entities": return "Entity IDs must look like light.living_room.";
case "read-failed": return "The private configuration file could not be read.";
case "write-failed": return "The private configuration file could not be saved.";
case "invalid-payload": return "The configuration could not be validated.";
default: return code ? "Home Assistant configuration is unavailable." : "";
}
}
function applyResult(text: string, saved: bool): void {
let result = null;
try {
result = JSON.parse(text);
} catch (error) {
result = { ok: false, error: "invalid-response" };
}
if (result.ok !== true) {
root.lastError = root.errorMessage(String(result.error || "invalid-response"));
return;
}
root.url = String(result.url || "");
root.entities = Array.isArray(result.entities) ? result.entities : [];
root.tokenConfigured = result.tokenConfigured === true;
root.configured = result.configured === true;
root.lastError = "";
if (saved) {
root.configurationSaved();
HomeAssistant.refresh();
}
}
function refresh(): void {
if (!statusProc.running && !writeProc.running)
statusProc.running = true;
}
// An empty token means "keep the stored token". Clearing is an explicit
// separate action so editing the URL can never erase a secret by accident.
function save(url: string, entitiesText: string, token: string): bool {
if (root.busy)
return false;
const payload = { url: url, entities: entitiesText };
if (token.trim() !== "")
payload.token = token;
return root.startWrite(payload);
}
function clearToken(): bool {
if (root.busy || !root.tokenConfigured)
return false;
return root.startWrite({ token: "" });
}
function startWrite(payload: var): bool {
root.lastError = "";
root.pendingPayload = JSON.stringify(payload);
writeProc.running = true;
return true;
}
Process {
id: statusProc
command: [root.helperPath, "status"]
stdout: StdioCollector {
onStreamFinished: root.applyResult(this.text, false)
}
onExited: (code, status) => {
if (code !== 0 && root.lastError === "")
root.lastError = "Home Assistant configuration could not be loaded.";
}
}
Process {
id: writeProc
command: [root.helperPath, "write"]
stdinEnabled: true
stdout: StdioCollector {
id: writeOutput
onStreamFinished: root.applyResult(this.text, true)
}
onStarted: {
writeProc.write(root.pendingPayload + "\n");
root.pendingPayload = "";
}
onExited: (code, status) => {
root.pendingPayload = "";
if (code !== 0 && root.lastError === "")
root.lastError = "Home Assistant configuration could not be saved.";
}
}
Component.onCompleted: root.refresh()
}