feat: added home assistant widget

This commit is contained in:
2026-09-21 17:48:09 +02:00
parent 62e760f126
commit e0a27e59b4
9 changed files with 1006 additions and 6 deletions
+225
View File
@@ -0,0 +1,225 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: haSettings
property var settings: null
readonly property var daemon: (typeof PluginService !== "undefined" && PluginService.pluginInstances) ? PluginService.pluginInstances["gameBar"] : null
readonly property var service: daemon ? daemon.homeService : null
readonly property var entityList: service ? service.entityList : []
property string filterText: ""
// Read straight from the daemon's (reactive) plugin data. Caching this in a
// local list loses the saved picks, because PluginSettings.loadValue() still
// returns the default while pluginService is being injected - and the next
// pick would then save that empty list over the stored one.
readonly property var selectedIds: {
const data = service ? service.pluginData : null;
const stored = data ? data.haEntities : null;
if (Array.isArray(stored))
return stored;
const fallback = settings ? settings.loadValue("haEntities", []) : [];
return Array.isArray(fallback) ? fallback : [];
}
readonly property var filteredEntities: {
const query = filterText.trim().toLowerCase();
const list = haSettings.entityList;
if (query === "")
return list;
return list.filter(entity => {
const name = (entity.attributes && entity.attributes.friendly_name) ? entity.attributes.friendly_name : "";
return entity.entity_id.toLowerCase().includes(query) || name.toLowerCase().includes(query);
});
}
readonly property string statusText: {
if (!service)
return "Plugin daemon not running";
if (!service.configured)
return "Enter a URL and token, then refresh";
if (service.loading)
return "Contacting Home Assistant…";
if (service.lastError !== "")
return service.lastError;
if (service.everLoaded)
return entityList.length + " entities · " + selectedIds.length + " picked";
return "Not loaded yet";
}
function isSelected(entityId) {
return haSettings.selectedIds.indexOf(entityId) !== -1;
}
function setSelected(entityId, selected) {
const next = haSettings.selectedIds.slice();
const index = next.indexOf(entityId);
if (selected && index === -1)
next.push(entityId);
else if (!selected && index !== -1)
next.splice(index, 1);
if (settings)
settings.saveValue("haEntities", next);
}
implicitHeight: column.implicitHeight
height: implicitHeight
Component.onCompleted: {
if (service && service.configured && !service.everLoaded)
service.refresh();
}
Column {
id: column
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.spacingS
StyledText {
text: "Home Assistant"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Pick the entities you want in the Home widget. The access token is stored in plugin_settings.json."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
anchors.verticalCenter: parent.verticalCenter
width: Math.round(parent.width * 0.3)
text: "Status"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Math.round(parent.width * 0.3) - refreshButton.width - parent.spacing * 2
text: haSettings.statusText
font.pixelSize: Theme.fontSizeSmall
color: (haSettings.service && haSettings.service.lastError !== "") ? Theme.error : Theme.surfaceText
elide: Text.ElideRight
}
DankButton {
id: refreshButton
text: "Refresh"
iconName: "refresh"
enabled: haSettings.service !== null && haSettings.service.configured
onClicked: haSettings.service.refresh()
}
}
DankTextField {
width: parent.width
placeholderText: "Filter entities (e.g. light, kitchen)"
leftIconName: "search"
showClearButton: true
text: haSettings.filterText
onTextChanged: haSettings.filterText = text
visible: haSettings.entityList.length > 0
}
Rectangle {
width: parent.width
height: 280
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
visible: haSettings.entityList.length > 0
DankListView {
id: entityListView
anchors.fill: parent
anchors.margins: Theme.spacingS
clip: true
spacing: 2
model: haSettings.filteredEntities
delegate: Rectangle {
id: entityRow
required property var modelData
readonly property bool picked: haSettings.isSelected(modelData.entity_id)
width: entityListView.width
height: 44
radius: Theme.cornerRadius
color: picked ? Theme.primarySelected : (rowArea.containsMouse ? Theme.surfaceHover : "transparent")
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: pickToggle.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: 0
StyledText {
width: parent.width
text: (entityRow.modelData.attributes && entityRow.modelData.attributes.friendly_name) ? entityRow.modelData.attributes.friendly_name : entityRow.modelData.entity_id
font.pixelSize: Theme.fontSizeSmall
font.weight: entityRow.picked ? Font.Medium : Font.Normal
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: entityRow.modelData.entity_id + " · " + entityRow.modelData.state
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
DankToggle {
id: pickToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
hideText: true
checked: entityRow.picked
onToggled: isChecked => haSettings.setSelected(entityRow.modelData.entity_id, isChecked)
}
MouseArea {
id: rowArea
anchors.fill: parent
anchors.rightMargin: pickToggle.width + Theme.spacingM
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: haSettings.setSelected(entityRow.modelData.entity_id, !entityRow.picked)
}
}
}
}
}
}