import QtQuick import qs.config import qs.services SettingsPage { id: root objectName: "home-phone-page" title: "Home & Phone" lede: "Choose what appears in Control Center and keep phone continuity close at hand." property string lightQuery: searchInput.text.trim().toLowerCase() readonly property var availableLights: HomeAssistant.catalog.filter(entity => { 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.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"; } function saveHomeAssistantConfig(): void { HomeAssistantConfig.save( homeUrlInput.text, homeEntitiesInput.text, homeTokenInput.text ); } Connections { target: HomeAssistantConfig function onConfigurationSaved(): void { homeTokenInput.clear(); homeUrlInput.text = HomeAssistantConfig.url; homeEntitiesInput.text = HomeAssistantConfig.entities.join(", "); } } SettingsCard { title: "Home Assistant" subtitle: root.homeStatus() SettingRow { label: "Connection" detail: HomeAssistantConfig.tokenConfigured ? "A long-lived access token is stored privately" : "Paste a long-lived access token to connect" value: HomeAssistantConfig.configured ? "Configured" : "Not configured" } SettingRow { label: "Server URL" detail: "The local or remote address of Home Assistant" controlWidth: 330 Rectangle { anchors.fill: parent radius: Theme.pillRadius color: Theme.alpha(Theme.fg, 0.07) border.width: homeUrlInput.activeFocus ? 2 : 1 border.color: homeUrlInput.activeFocus ? Theme.alpha(Theme.accent, 0.55) : "transparent" TextInput { id: homeUrlInput anchors.fill: parent anchors.leftMargin: 12 anchors.rightMargin: 12 activeFocusOnTab: true text: HomeAssistantConfig.url color: Theme.fg selectionColor: Theme.alpha(Theme.accent, 0.5) selectedTextColor: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSize verticalAlignment: TextInput.AlignVCenter clip: true Text { anchors.fill: parent visible: homeUrlInput.text === "" text: "https://homeassistant.local:8123" color: Theme.fgMuted font: homeUrlInput.font verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } } } } SettingRow { label: "Access token" detail: HomeAssistantConfig.tokenConfigured ? "Stored · leave blank to keep it" : "Create one in your Home Assistant profile" controlWidth: 330 PasswordField { id: homeTokenInput anchors.fill: parent placeholder: HomeAssistantConfig.tokenConfigured ? "Stored token" : "Long-lived access token" onAccepted: root.saveHomeAssistantConfig() } } Column { width: parent.width spacing: 7 topPadding: 10 bottomPadding: 12 Text { width: parent.width text: "Light entities" color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: Font.Medium } Text { width: parent.width text: "Comma-separated entity IDs. These define the discoverable light catalog." color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall wrapMode: Text.WordWrap } Rectangle { width: parent.width height: 72 radius: 10 color: Theme.alpha(Theme.fg, 0.055) border.width: homeEntitiesInput.activeFocus ? 2 : 1 border.color: homeEntitiesInput.activeFocus ? Theme.alpha(Theme.accent, 0.55) : Theme.alpha(Theme.fg, 0.06) TextEdit { id: homeEntitiesInput anchors.fill: parent anchors.margins: 10 activeFocusOnTab: true text: HomeAssistantConfig.entities.join(", ") color: Theme.fg selectionColor: Theme.alpha(Theme.accent, 0.5) selectedTextColor: Theme.fg font.family: Theme.fontMono font.pixelSize: Theme.fontSizeSmall wrapMode: TextEdit.Wrap clip: true Text { anchors.fill: parent visible: homeEntitiesInput.text === "" text: "light.living_room, light.kitchen" color: Theme.fgMuted font: homeEntitiesInput.font wrapMode: Text.WordWrap } } } } Text { width: parent.width visible: HomeAssistantConfig.lastError !== "" text: HomeAssistantConfig.lastError color: Theme.danger font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall wrapMode: Text.WordWrap bottomPadding: 9 } SettingRow { label: "Private configuration" detail: "Saved with owner-only permissions in a private environment file" controlWidth: 216 Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 8 SettingsButton { id: clearTokenButton text: "Clear token" enabled: HomeAssistantConfig.tokenConfigured && !HomeAssistantConfig.busy activeFocusOnTab: enabled border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08) onClicked: HomeAssistantConfig.clearToken() Keys.onReturnPressed: if (enabled) HomeAssistantConfig.clearToken() Keys.onSpacePressed: if (enabled) HomeAssistantConfig.clearToken() } SettingsButton { id: saveHomeConfigButton text: HomeAssistantConfig.busy ? "Saving…" : "Save" tone: "accent" enabled: !HomeAssistantConfig.busy activeFocusOnTab: enabled border.width: activeFocus ? 2 : 0 border.color: activeFocus ? Theme.fg : "transparent" onClicked: root.saveHomeAssistantConfig() Keys.onReturnPressed: if (enabled) root.saveHomeAssistantConfig() Keys.onSpacePressed: if (enabled) root.saveHomeAssistantConfig() } } } SettingRow { label: "Light catalog" detail: "Light state is read through the Home Assistant helper." divider: false controlWidth: 176 Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 8 SettingsButton { id: refreshButton text: "Refresh" activeFocusOnTab: true border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08) onClicked: HomeAssistant.refresh() Keys.onReturnPressed: HomeAssistant.refresh() Keys.onSpacePressed: HomeAssistant.refresh() } SettingsButton { id: openHomeButton text: "Open" activeFocusOnTab: true border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08) onClicked: HomeAssistant.open() Keys.onReturnPressed: HomeAssistant.open() Keys.onSpacePressed: HomeAssistant.open() } } } } SettingsCard { title: "Control Center lights" subtitle: HomeAssistant.selectedEntities.length === 0 ? "Select the lights that belong on your shelf." : `${Math.min(4, HomeAssistant.selectedEntities.length)} in Control Center · ${HomeAssistant.selectedEntities.length} selected` Text { width: parent.width visible: HomeAssistant.selectedEntities.length === 0 text: "Choose lights below to build your Control Center shelf." color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSize wrapMode: Text.WordWrap topPadding: 3 bottomPadding: 13 } GridView { id: favoritesGrid width: parent.width height: Math.ceil(count / 2) * cellHeight visible: count > 0 interactive: false clip: false cellWidth: width / 2 cellHeight: 116 model: HomeAssistant.selectedEntities delegate: HomeFavoriteCard { required property var modelData width: GridView.view.cellWidth - 6 height: 108 favorite: ({ id: modelData.id, alias: modelData.name === modelData.sourceName ? "" : modelData.name }) sourceName: modelData.sourceName featured: index < 4 canMoveEarlier: index > 0 canMoveLater: index < favoritesGrid.count - 1 onAliasCommitted: (id, alias) => HomePreferences.setAlias(id, alias) onMoveRequested: (id, targetIndex) => HomePreferences.move(id, targetIndex) onRemoveRequested: id => HomePreferences.remove(id) } } Rectangle { width: parent.width height: 46 visible: HomePreferences.saveError !== "" radius: 9 color: Theme.alpha(Theme.warn, 0.09) border.width: 1 border.color: Theme.alpha(Theme.warn, 0.24) Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: retryButton.left anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: HomePreferences.saveError color: Theme.warn font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall elide: Text.ElideRight } SettingsButton { id: retryButton anchors.right: parent.right anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter text: "Retry" activeFocusOnTab: true border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.warn : Theme.alpha(Theme.fg, 0.08) onClicked: HomePreferences.retrySave() Keys.onReturnPressed: HomePreferences.retrySave() Keys.onSpacePressed: HomePreferences.retrySave() } } } SettingsCard { title: "Available lights" subtitle: "Search the Home Assistant catalog by source name or entity ID." Rectangle { width: parent.width height: 38 radius: 10 color: Theme.alpha(Theme.fg, searchInput.activeFocus ? 0.08 : 0.05) border.width: searchInput.activeFocus ? 2 : 1 border.color: searchInput.activeFocus ? Theme.alpha(Theme.accent, 0.72) : Theme.alpha(Theme.fg, 0.065) Text { anchors.left: parent.left anchors.leftMargin: 11 anchors.verticalCenter: parent.verticalCenter text: "\u{F0349}" color: Theme.fgDim font.family: Theme.fontMono font.pixelSize: 14 } TextInput { id: searchInput anchors.left: parent.left anchors.leftMargin: 36 anchors.right: parent.right anchors.rightMargin: 11 anchors.verticalCenter: parent.verticalCenter activeFocusOnTab: true color: Theme.fg selectionColor: Theme.accent selectedTextColor: Theme.bgDark font.family: Theme.fontFamily font.pixelSize: Theme.fontSize clip: true Text { anchors.fill: parent visible: searchInput.text === "" && !searchInput.activeFocus text: "Search available lights" color: Theme.fgMuted font: searchInput.font verticalAlignment: Text.AlignVCenter } } } Column { width: parent.width visible: root.availableLights.length > 0 Repeater { model: root.availableLights AvailableLightRow { required property var modelData width: parent.width entity: modelData onAddRequested: id => HomePreferences.add(id) } } } Text { width: parent.width visible: root.availableLights.length === 0 text: root.availableEmptyText color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSize horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap topPadding: 18 bottomPadding: 10 } } SettingsCard { title: "Phone continuity" subtitle: "Keep the Messages handoff independent from phone connectivity." SettingRow { label: "Messages" detail: "Opens BlueBubbles" divider: false controlWidth: 204 Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 12 Text { anchors.verticalCenter: parent.verticalCenter text: SystemSettings.bluebubblesAvailable ? "Installed" : "Unavailable" color: SystemSettings.bluebubblesAvailable ? Theme.ok : Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall } SettingsButton { id: openBlueBubblesButton text: "Open" enabled: SystemSettings.bluebubblesAvailable activeFocusOnTab: enabled border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08) onClicked: SystemSettings.openApplication("bluebubbles") Keys.onReturnPressed: if (enabled) SystemSettings.openApplication("bluebubbles") Keys.onSpacePressed: if (enabled) SystemSettings.openApplication("bluebubbles") } } } } }