Fix Home action completion and test isolation

This commit is contained in:
Gabriel Brown
2026-08-17 16:49:50 -04:00
parent c2b19f7545
commit a325ec807e
3 changed files with 217 additions and 42 deletions
@@ -21,12 +21,17 @@ Singleton {
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
readonly property var visibleEntities: root.selectedEntities.slice(0, 4)
readonly property int discoveredCount: root.catalog.length
@@ -177,7 +182,7 @@ Singleton {
root.pendingBrightness = nextPending;
}
if (root.fixtureMode) {
if (root.fixtureMode && root.fixtureProcessMode === "") {
root.applyFixtureAction(action);
root.finishActionState(action.entityId);
return;
@@ -192,27 +197,73 @@ Singleton {
return;
root.activeAction = root.actionQueue[0];
actionProc.command = root.activeAction.kind === "brightness"
? [root.helperPath, "brightness", root.activeAction.entityId, String(root.activeAction.percent)]
: [root.helperPath, "toggle", root.activeAction.entityId];
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 consumeAction(text: string): void {
function fixtureActionCommand(action: var): var {
if (root.fixtureProcessMode === "no-output"
&& action.entityId === "light.fixture_kitchen") {
return ["/usr/bin/sh", "-c", "sleep 0.5; exit 7"];
}
return ["/usr/bin/sh", "-c", "sleep 0.5; printf '%s' '{\"ok\":true}'"];
}
function handleActionStreamFinished(text: string): void {
if (root.activeAction === null || root.actionStreamFinished)
return;
root.actionResponseText = text;
root.actionStreamFinished = true;
actionCompletionTimer.restart();
}
function handleActionExited(exitCode: int): void {
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 = "action-failed";
let errorCode = text === "" ? "action-failed" : "invalid-response";
try {
const result = JSON.parse(text);
ok = result.ok === true;
errorCode = String(result.error || errorCode);
ok = exitCode === 0 && result.ok === true;
errorCode = String(result.error || "action-failed");
} catch (error) {
errorCode = "invalid-response";
// 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);
@@ -220,12 +271,15 @@ Singleton {
const nextErrors = Object.assign({}, root.entityErrors);
if (ok) {
delete nextErrors[completedAction.entityId];
refreshDelay.restart();
if (root.fixtureMode && root.fixtureProcessMode !== "")
root.applyFixtureAction(completedAction);
else
refreshDelay.restart();
} else {
nextErrors[completedAction.entityId] = errorCode;
}
root.entityErrors = nextErrors;
root.startNextAction();
queueAdvanceTimer.restart();
}
function finishActionState(entityId: string): void {
@@ -283,19 +337,29 @@ Singleton {
}
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 applyFixture(name: string): void {
if (["ready", "stale", "unavailable", "missing-selected", "action-error"].indexOf(name) < 0)
if (["ready", "stale", "unavailable", "missing-selected", "action-error",
"process-actions", "process-no-output"].indexOf(name) < 0)
return;
root.fixtureMode = true;
root.resetActionState();
root.fixtureProcessMode = name === "process-actions"
? "success"
: (name === "process-no-output" ? "no-output" : "");
if (name === "unavailable") {
root.catalog = [];
root.fixtureFavorites = [];
@@ -326,6 +390,7 @@ Singleton {
function clearFixture(): void {
root.fixtureMode = false;
root.fixtureProcessMode = "";
root.phase = "loading";
root.catalog = [];
root.selectedEntities = [];
@@ -347,9 +412,27 @@ Singleton {
Process {
id: actionProc
stdout: StdioCollector {
id: actionOutput
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 (actionProc.running) {
queueAdvanceTimer.restart();
return;
}
root.startNextAction();
}
onExited: (code, status) => root.consumeAction(actionOutput.text)
}
Timer {
+1
View File
@@ -270,6 +270,7 @@ ShellRoot {
selectedIds: HomeAssistant.selectedEntities.map(entity => entity.id),
entities: HomeAssistant.selectedEntities,
stale: HomeAssistant.stale,
busy: HomeAssistant.busyEntityIds.length > 0,
busyEntityIds: HomeAssistant.busyEntityIds,
pendingBrightness: HomeAssistant.pendingBrightness,
entityErrors: HomeAssistant.entityErrors,