diff --git a/config/dot/quickshell/modules/settings/HomePhonePage.qml b/config/dot/quickshell/modules/settings/HomePhonePage.qml index 4a4fc5b..a88eac4 100644 --- a/config/dot/quickshell/modules/settings/HomePhonePage.qml +++ b/config/dot/quickshell/modules/settings/HomePhonePage.qml @@ -4,25 +4,38 @@ import qs.services Item { id: root + objectName: "home-phone-page" property string lightQuery: searchInput.text.trim().toLowerCase() readonly property var availableLights: HomeAssistant.catalog.filter(entity => { - if (HomePreferences.isSelected(entity.id)) + if (HomeAssistant.selectedEntities.some(selected => selected.id === entity.id)) return false; const haystack = (entity.sourceName + " " + entity.id).toLowerCase(); return root.lightQuery === "" || haystack.includes(root.lightQuery); }) + readonly property string availableEmptyText: root.availableLights.length > 0 + ? "" + : (root.lightQuery !== "" + ? "No lights match that search" + : (HomeAssistant.catalog.length === 0 + ? "No lights discovered" + : "All discovered lights are already selected")) + readonly property var pageDiagnostics: ({ + availableLightIds: root.availableLights.map(entity => entity.id), + availableEmptyText: root.availableEmptyText, + homeStatus: root.homeStatus() + }) function homeStatus(): string { - if (HomeAssistant.phase === "ready") - return `Connected · ${HomeAssistant.discoveredCount} lights discovered`; - if (HomeAssistant.phase === "degraded") - return "Last update unavailable · showing saved controls"; if (HomeAssistant.lastError === "authentication-required") return "Authentication required"; if (HomeAssistant.lastError === "not-configured") return "Home Assistant is not configured"; + if (HomeAssistant.phase === "ready") + return `Connected · ${HomeAssistant.discoveredCount} lights discovered`; + if (HomeAssistant.phase === "degraded") + return "Last update unavailable · showing saved controls"; if (HomeAssistant.phase === "loading") return "Connecting to Home Assistant"; return "Home Assistant is unavailable"; @@ -250,9 +263,7 @@ Item { Text { width: parent.width visible: root.availableLights.length === 0 - text: root.lightQuery === "" && HomeAssistant.catalog.length > 0 - ? "All discovered lights are already selected" - : "No lights match that search" + text: root.availableEmptyText color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSize diff --git a/config/dot/quickshell/modules/settings/SettingsShell.qml b/config/dot/quickshell/modules/settings/SettingsShell.qml index 7ea5701..191d28b 100644 --- a/config/dot/quickshell/modules/settings/SettingsShell.qml +++ b/config/dot/quickshell/modules/settings/SettingsShell.qml @@ -6,6 +6,11 @@ Rectangle { id: root property var hostWindow: null + readonly property var homePhoneDiagnostics: pageLoader.status === Loader.Ready + && pageLoader.item + && pageLoader.item.objectName === "home-phone-page" + ? pageLoader.item.pageDiagnostics + : ({}) color: Theme.bg radius: 18 diff --git a/config/dot/quickshell/modules/settings/SettingsWindow.qml b/config/dot/quickshell/modules/settings/SettingsWindow.qml index 86bf906..d10b50f 100644 --- a/config/dot/quickshell/modules/settings/SettingsWindow.qml +++ b/config/dot/quickshell/modules/settings/SettingsWindow.qml @@ -6,6 +6,8 @@ import qs.services FloatingWindow { id: root + readonly property var homePhoneDiagnostics: settingsShell.homePhoneDiagnostics + title: "Panama Settings" visible: ShellState.settingsOpen implicitWidth: 1120 @@ -23,6 +25,7 @@ FloatingWindow { } SettingsShell { + id: settingsShell anchors.fill: parent hostWindow: root } diff --git a/config/dot/quickshell/services/HomeAssistant.qml b/config/dot/quickshell/services/HomeAssistant.qml index f7e0eea..2969635 100644 --- a/config/dot/quickshell/services/HomeAssistant.qml +++ b/config/dot/quickshell/services/HomeAssistant.qml @@ -434,7 +434,8 @@ Singleton { } function applyFixture(name: string): void { - if (["ready", "stale", "unavailable", "missing-selected", "action-error", + if (["ready", "available-extra", "stale", "stale-authentication", "stale-not-configured", + "unavailable", "missing-selected", "action-error", "process-actions", "process-no-output", "process-delayed-exit", "process-slow-actions"].indexOf(name) < 0) return; @@ -488,15 +489,31 @@ Singleton { root.catalog = root.fixtureEntities(); root.fixtureFavorites = root.fixturePreferenceRecords(); + if (name === "available-extra") { + root.catalog = root.catalog.concat([{ + id: "light.fixture_guest", + sourceName: "Guest lamp", + state: "off", + available: true, + active: false, + dimmable: true, + brightnessPct: 0 + }]); + } if (name === "missing-selected") { root.fixtureFavorites = root.fixtureFavorites.concat([ { id: "light.fixture_missing", alias: "Porch" } ]); } root.rebuildSelection(); - root.phase = name === "stale" ? "degraded" : "ready"; - root.stale = name === "stale"; - root.lastError = name === "stale" ? "unreachable" : ""; + const isStale = ["stale", "stale-authentication", "stale-not-configured"].indexOf(name) >= 0; + root.phase = isStale ? "degraded" : "ready"; + root.stale = isStale; + root.lastError = name === "stale-authentication" + ? "authentication-required" + : (name === "stale-not-configured" + ? "not-configured" + : (name === "stale" ? "unreachable" : "")); if (name === "action-error") { root.entityErrors = { "light.fixture_kitchen": "request-failed" diff --git a/config/dot/quickshell/shell.qml b/config/dot/quickshell/shell.qml index 6f84599..ebcc6af 100644 --- a/config/dot/quickshell/shell.qml +++ b/config/dot/quickshell/shell.qml @@ -85,7 +85,7 @@ ShellRoot { IntelligenceResult {} ActivityPanel {} PowerMenu {} - SettingsWindow {} + SettingsWindow { id: settingsWindow } // Toasts are their own always-on layer; they must be able to appear // without any overlay being open. @@ -295,7 +295,8 @@ ShellRoot { open: ShellState.settingsOpen, page: ShellState.settingsPage, discoveredCount: HomeAssistant.discoveredCount, - selectedCount: HomeAssistant.configuredCount + selectedCount: HomeAssistant.configuredCount, + homePhone: settingsWindow.homePhoneDiagnostics }); } } diff --git a/tests/quickshell/home-phone-settings-contract.sh b/tests/quickshell/home-phone-settings-contract.sh index 7840213..2513dbe 100755 --- a/tests/quickshell/home-phone-settings-contract.sh +++ b/tests/quickshell/home-phone-settings-contract.sh @@ -93,6 +93,7 @@ assert_contains 'HomePreferences.add' "$home_page" assert_contains 'HomePreferences.retrySave' "$home_page" assert_contains 'Choose lights below to build your Control Center shelf.' "$home_page" assert_contains 'All discovered lights are already selected' "$home_page" +assert_contains 'No lights discovered' "$home_page" assert_contains 'No lights match that search' "$home_page" assert_contains 'Opens BlueBubbles' "$home_page" [[ "$(rg -Fc 'required property var modelData' "$home_page")" -ge 2 ]] \ @@ -173,9 +174,100 @@ for _ in $(seq 1 40); do fi sleep 0.1 done -jq -e '.open == true and .page == "home-phone" and .discoveredCount == 7 and .selectedCount == 7' \ +jq -e ' + .open == true and + .page == "home-phone" and + .discoveredCount == 7 and + .selectedCount == 7 and + .homePhone.availableLightIds == [] and + .homePhone.availableEmptyText == "All discovered lights are already selected" +' \ <<<"$status" >/dev/null || fail "Home & Phone diagnostics are incomplete: $status" +qs_for_test ipc call home-assistant fixture available-extra >/dev/null +available_status='{}' +for _ in $(seq 1 40); do + available_status="$(qs_for_test ipc call settings status | jq -c .)" + if jq -e ' + .discoveredCount == 8 and + .selectedCount == 7 and + .homePhone.availableLightIds == ["light.fixture_guest"] + ' <<<"$available_status" >/dev/null; then + break + fi + sleep 0.1 +done +jq -e ' + .discoveredCount == 8 and + .selectedCount == 7 and + .homePhone.availableLightIds == ["light.fixture_guest"] +' <<<"$available_status" >/dev/null \ + || fail "an unselected fixture light was not the only available row: $available_status" + +qs_for_test ipc call home-assistant fixture stale-authentication >/dev/null +authentication_status='{}' +for _ in $(seq 1 40); do + authentication_status="$(qs_for_test ipc call settings status | jq -c .)" + if jq -e ' + .discoveredCount == 7 and + .selectedCount == 7 and + .homePhone.homeStatus == "Authentication required" + ' <<<"$authentication_status" >/dev/null; then + break + fi + sleep 0.1 +done +jq -e ' + .discoveredCount == 7 and + .selectedCount == 7 and + .homePhone.homeStatus == "Authentication required" +' <<<"$authentication_status" >/dev/null \ + || fail "stale authentication did not retain actionable copy: $authentication_status" + +qs_for_test ipc call home-assistant fixture stale-not-configured >/dev/null +not_configured_status='{}' +for _ in $(seq 1 40); do + not_configured_status="$(qs_for_test ipc call settings status | jq -c .)" + if jq -e ' + .discoveredCount == 7 and + .selectedCount == 7 and + .homePhone.homeStatus == "Home Assistant is not configured" + ' <<<"$not_configured_status" >/dev/null; then + break + fi + sleep 0.1 +done +jq -e ' + .discoveredCount == 7 and + .selectedCount == 7 and + .homePhone.homeStatus == "Home Assistant is not configured" +' <<<"$not_configured_status" >/dev/null \ + || fail "stale not-configured state did not retain actionable copy: $not_configured_status" + +qs_for_test ipc call home-assistant fixture unavailable >/dev/null +empty_status='{}' +for _ in $(seq 1 40); do + empty_status="$(qs_for_test ipc call settings status | jq -c .)" + if jq -e ' + .discoveredCount == 0 and + .selectedCount == 0 and + .homePhone.homeStatus == "Home Assistant is not configured" and + .homePhone.availableLightIds == [] and + .homePhone.availableEmptyText == "No lights discovered" + ' <<<"$empty_status" >/dev/null; then + break + fi + sleep 0.1 +done +jq -e ' + .discoveredCount == 0 and + .selectedCount == 0 and + .homePhone.homeStatus == "Home Assistant is not configured" and + .homePhone.availableLightIds == [] and + .homePhone.availableEmptyText == "No lights discovered" +' <<<"$empty_status" >/dev/null \ + || fail "an empty catalog did not render its distinct copy: $empty_status" + system_status="$(qs_for_test ipc call settings-system status | jq -c .)" jq -e '.bluebubblesAvailable == true' <<<"$system_status" >/dev/null \ || fail "BlueBubbles availability was not exposed: $system_status"