Fix nested fixture transition draining

This commit is contained in:
Gabriel Brown
2026-08-17 17:16:36 -04:00
parent 4f93522f7d
commit 9a99c6064e
3 changed files with 100 additions and 3 deletions
@@ -35,10 +35,13 @@ Singleton {
property bool fixtureTransitionDraining: false property bool fixtureTransitionDraining: false
property bool fixtureTransitionStreamFinished: false property bool fixtureTransitionStreamFinished: false
property bool fixtureTransitionExited: false property bool fixtureTransitionExited: false
property var pendingFixtureTarget: null
property int delayedFixtureExitCode: 0
readonly property var visibleEntities: root.selectedEntities.slice(0, 4) readonly property var visibleEntities: root.selectedEntities.slice(0, 4)
readonly property int discoveredCount: root.catalog.length readonly property int discoveredCount: root.catalog.length
readonly property int configuredCount: root.selectedEntities.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 // Temporary aliases keep the current Home controls usable until the shelf
// switches to the selected-entity and per-entity action interfaces. // switches to the selected-entity and per-entity action interfaces.
@@ -219,10 +222,14 @@ Singleton {
} }
function fixtureActionCommand(action: var): var { 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" if (root.fixtureProcessMode === "no-output"
&& action.entityId === "light.fixture_kitchen") { && action.entityId === "light.fixture_kitchen") {
return ["/usr/bin/sh", "-c", "sleep 0.5; exit 7"]; 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}'"]; return ["/usr/bin/sh", "-c", "sleep 0.5; printf '%s' '{\"ok\":true}'"];
} }
@@ -240,6 +247,20 @@ Singleton {
} }
function handleActionExited(exitCode: int): void { 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) { if (root.fixtureTransitionDraining) {
root.fixtureTransitionExited = true; root.fixtureTransitionExited = true;
fixtureTransitionTimer.restart(); fixtureTransitionTimer.restart();
@@ -368,6 +389,9 @@ Singleton {
} }
function beginFixtureTransition(): void { function beginFixtureTransition(): void {
if (root.fixtureTransitionDraining)
return;
const shouldDrain = root.fixtureMode const shouldDrain = root.fixtureMode
&& root.fixtureProcessMode !== "" && root.fixtureProcessMode !== ""
&& (actionProc.running || root.activeAction !== null); && (actionProc.running || root.activeAction !== null);
@@ -397,22 +421,59 @@ Singleton {
if (!root.fixtureTransitionStreamFinished || !root.fixtureTransitionExited) if (!root.fixtureTransitionStreamFinished || !root.fixtureTransitionExited)
return; return;
const target = root.pendingFixtureTarget;
root.pendingFixtureTarget = null;
root.fixtureTransitionDraining = false; root.fixtureTransitionDraining = false;
root.fixtureTransitionStreamFinished = false; root.fixtureTransitionStreamFinished = false;
root.fixtureTransitionExited = false; root.fixtureTransitionExited = false;
if (target !== null)
root.installFixtureTarget(target);
queueAdvanceTimer.restart(); queueAdvanceTimer.restart();
} }
function applyFixture(name: string): void { function applyFixture(name: string): void {
if (["ready", "stale", "unavailable", "missing-selected", "action-error", if (["ready", "stale", "unavailable", "missing-selected", "action-error",
"process-actions", "process-no-output"].indexOf(name) < 0) "process-actions", "process-no-output", "process-delayed-exit",
"process-slow-actions"].indexOf(name) < 0)
return; 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(); 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.fixtureMode = true;
root.fixtureProcessMode = name === "process-actions" root.fixtureProcessMode = name === "process-actions"
? "success" ? "success"
: (name === "process-no-output" ? "no-output" : ""); : (name === "process-no-output"
? "no-output"
: (name === "process-delayed-exit"
? "delayed-exit"
: (name === "process-slow-actions" ? "slow-success" : "")));
if (name === "unavailable") { if (name === "unavailable") {
root.catalog = []; root.catalog = [];
root.fixtureFavorites = []; root.fixtureFavorites = [];
@@ -442,7 +503,10 @@ Singleton {
} }
function clearFixture(): void { function clearFixture(): void {
root.beginFixtureTransition(); root.requestFixtureTarget({ fixture: false, name: "" });
}
function installLiveState(): void {
root.fixtureMode = false; root.fixtureMode = false;
root.fixtureProcessMode = ""; root.fixtureProcessMode = "";
root.phase = "loading"; root.phase = "loading";
@@ -496,6 +560,12 @@ Singleton {
onTriggered: root.tryFinishFixtureTransition() onTriggered: root.tryFinishFixtureTransition()
} }
Timer {
id: delayedFixtureExitTimer
interval: 1000
onTriggered: root.recordActionExited(root.delayedFixtureExitCode)
}
Timer { Timer {
interval: 60000 interval: 60000
repeat: true repeat: true
+2
View File
@@ -274,6 +274,8 @@ ShellRoot {
busyEntityIds: HomeAssistant.busyEntityIds, busyEntityIds: HomeAssistant.busyEntityIds,
pendingBrightness: HomeAssistant.pendingBrightness, pendingBrightness: HomeAssistant.pendingBrightness,
entityErrors: HomeAssistant.entityErrors, entityErrors: HomeAssistant.entityErrors,
actionProcessRunning: HomeAssistant.actionProcessRunning,
actionStreamFinished: HomeAssistant.actionStreamFinished,
lastError: HomeAssistant.lastError lastError: HomeAssistant.lastError
}); });
} }
@@ -209,6 +209,31 @@ wait_for_home_status '
(.entities[] | select(.id == "light.fixture_hall") | .active == true) (.entities[] | select(.id == "light.fixture_hall") | .active == true)
' 'switching process fixtures stranded or misattributed the new action' ' 'switching process fixtures stranded or misattributed the new action'
qs_for_test ipc call home-assistant fixture process-delayed-exit >/dev/null
qs_for_test ipc call home-assistant brightness light.fixture_living 88 >/dev/null
wait_for_home_status '
.busyEntityIds == ["light.fixture_living"] and
.pendingBrightness["light.fixture_living"] == 88 and
.actionProcessRunning == false and
.actionStreamFinished == true
' 'nested fixture transition setup did not reach its delayed-exit window'
qs_for_test ipc call home-assistant fixture process-no-output >/dev/null
qs_for_test ipc call home-assistant fixture process-slow-actions >/dev/null
qs_for_test ipc call home-assistant brightness light.fixture_desk 57 >/dev/null
wait_for_home_status '
.phase == "ready" and
.busy == false and
.busyEntityIds == [] and
.pendingBrightness == {} and
.entityErrors == {} and
(.entities[] |
select(.id == "light.fixture_living") |
.active == false and .brightnessPct == 36) and
(.entities[] |
select(.id == "light.fixture_desk") |
.active == true and .brightnessPct == 57)
' 'nested fixture transitions released the drain or corrupted the latest action'
qs_for_test ipc call home-assistant fixture missing-selected >/dev/null qs_for_test ipc call home-assistant fixture missing-selected >/dev/null
jq -e ' jq -e '
.fixture == true and .fixture == true and