Skip to content

Commit f6d3e3f

Browse files
authored
Merge pull request #648 from Pozzoo/main
add(hassio): plugin that integrates the bar with home assistant
2 parents 9d84057 + a8f8ef1 commit f6d3e3f

10 files changed

Lines changed: 1437 additions & 0 deletions

File tree

hassio/BarWidget.qml

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import QtQuick
2+
import QtQuick.Layouts
3+
import Quickshell
4+
import qs.Commons
5+
import qs.Widgets
6+
7+
NIconButton {
8+
id: root
9+
10+
property var pluginApi: null
11+
property ShellScreen screen
12+
property string widgetId: ""
13+
property string section: ""
14+
property int sectionWidgetIndex: -1
15+
property int sectionWidgetsCount: 0
16+
17+
property var main: pluginApi?.mainInstance ?? null
18+
19+
// "Connected" - authenticated and live
20+
// "Connecting" - socket opening or authenticating (first attempt or after settings change)
21+
// "Disconnected" - dropped after a successful connection; reconnect backoff in progress
22+
// "Unconfigured" - no token set, nothing to attempt
23+
// "AuthFailed" - token rejected by HA
24+
readonly property string _status: {
25+
if (!root.main)
26+
return "Unconfigured";
27+
if (root.main.haToken === "")
28+
return "Unconfigured";
29+
if (root.main.authFailed)
30+
return "AuthFailed";
31+
if (root.main.authenticated)
32+
return "Connected";
33+
// Not yet authenticated - distinguish first-time connect from a drop-and-retry
34+
if (root.main.isReconnecting)
35+
return "Disconnected";
36+
return "Connecting";
37+
}
38+
39+
readonly property string _statusLabel: {
40+
switch (root._status) {
41+
case "Connected":
42+
return pluginApi?.tr("widget.status_connected");
43+
case "Disconnected":
44+
return pluginApi?.tr("widget.status_disconnected");
45+
case "Connecting":
46+
return pluginApi?.tr("widget.status_connecting");
47+
case "AuthFailed":
48+
return pluginApi?.tr("widget.status_auth_failed");
49+
case "Unconfigured":
50+
return pluginApi?.tr("widget.status_unconfigured");
51+
default:
52+
return pluginApi?.tr("widget.status_unconfigured");
53+
}
54+
}
55+
56+
icon: "smart-home"
57+
colorFg: {
58+
switch (root._status) {
59+
case "Connected":
60+
return Color.mPrimary;
61+
case "Connecting":
62+
return Color.mOnError;
63+
case "Disconnected":
64+
return Color.mError;
65+
case "AuthFailed":
66+
return Color.mError;
67+
case "Unconfigured":
68+
return Color.mOnSurfaceVariant;
69+
default:
70+
return Color.mOnSurfaceVariant;
71+
}
72+
}
73+
74+
colorBg: Color.mSurfaceVariant
75+
colorBgHover: Color.mHover
76+
colorFgHover: Color.mOnHover
77+
colorBorder: "transparent"
78+
colorBorderHover: "transparent"
79+
80+
onClicked: pluginApi.togglePanel(root.screen, this)
81+
82+
tooltipText: pluginApi?.tr("widget.tooltip", {
83+
status: root._statusLabel
84+
})
85+
86+
implicitHeight: Style.barHeight
87+
88+
// Pulse only when actively trying to connect (token present, socket live, not yet authed)
89+
SequentialAnimation on opacity {
90+
running: root._status === "Connecting"
91+
loops: Animation.Infinite
92+
NumberAnimation {
93+
to: 0.3
94+
duration: 600
95+
easing.type: Easing.InOutSine
96+
}
97+
NumberAnimation {
98+
to: 1.0
99+
duration: 600
100+
easing.type: Easing.InOutSine
101+
}
102+
}
103+
104+
// Snap back to full opacity when not connecting
105+
opacity: root._status !== "Connecting" ? 1.0 : opacity
106+
}

hassio/BrowserView.qml

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import QtQuick
2+
import QtQuick.Layouts
3+
import qs.Commons
4+
import qs.Widgets
5+
6+
Item {
7+
id: root
8+
property var pluginApi: null
9+
property var main: null
10+
11+
property var _allEntities: []
12+
property string _searchText: ""
13+
property bool _loading: false
14+
property int _pinVersion: 0
15+
16+
clip: true
17+
18+
function load() {
19+
searchInput.text = "";
20+
root._searchText = "";
21+
_fetchAll();
22+
}
23+
24+
function _isPinned(entity_id) {
25+
const pinned = pluginApi?.pluginSettings?.entities ?? [];
26+
return pinned.includes(entity_id);
27+
}
28+
29+
function _togglePin(entity_id) {
30+
let pinned = pluginApi?.pluginSettings?.entities ?? [];
31+
pinned = [...pinned];
32+
const idx = pinned.indexOf(entity_id);
33+
if (idx >= 0) {
34+
pinned.splice(idx, 1);
35+
} else {
36+
pinned.push(entity_id);
37+
}
38+
pluginApi.pluginSettings.entities = pinned;
39+
pluginApi.saveSettings();
40+
root.main.refreshEntities();
41+
root._pinVersion++;
42+
}
43+
44+
ListModel {
45+
id: _filteredModel
46+
}
47+
48+
function _refilter() {
49+
const q = root._searchText.toLowerCase();
50+
const source = root._allEntities;
51+
52+
_filteredModel.clear();
53+
54+
for (const e of source) {
55+
if (!q || e.entity_id.toLowerCase().includes(q) || e.friendly_name.toLowerCase().includes(q)) {
56+
_filteredModel.append(e);
57+
}
58+
}
59+
}
60+
61+
function _fetchAll() {
62+
root._loading = true;
63+
root.main.getAllStates(function (results) {
64+
root._allEntities = results;
65+
root._refilter();
66+
root._loading = false;
67+
});
68+
}
69+
70+
ColumnLayout {
71+
anchors.fill: parent
72+
spacing: Style.marginM
73+
74+
NTextInput {
75+
id: searchInput
76+
Layout.fillWidth: true
77+
label: pluginApi?.tr("browser.search_label")
78+
placeholderText: pluginApi?.tr("browser.search_placeholder")
79+
onTextChanged: {
80+
root._searchText = text;
81+
root._refilter();
82+
}
83+
}
84+
85+
// Loading state
86+
Item {
87+
Layout.fillWidth: true
88+
Layout.fillHeight: true
89+
visible: root._loading
90+
91+
ColumnLayout {
92+
anchors.centerIn: parent
93+
spacing: Style.marginM
94+
95+
NIcon {
96+
Layout.alignment: Qt.AlignHCenter
97+
icon: "loader"
98+
color: Color.mOnSurfaceVariant
99+
100+
RotationAnimation on rotation {
101+
running: root._loading
102+
loops: Animation.Infinite
103+
from: 0
104+
to: 360
105+
duration: 1000
106+
}
107+
}
108+
109+
NText {
110+
Layout.alignment: Qt.AlignHCenter
111+
text: pluginApi?.tr("browser.loading")
112+
color: Color.mOnSurfaceVariant
113+
pointSize: Style.fontSizeM
114+
}
115+
}
116+
}
117+
118+
// Entity list
119+
NScrollView {
120+
Layout.fillWidth: true
121+
Layout.fillHeight: true
122+
visible: !root._loading
123+
clip: true
124+
125+
ListView {
126+
width: parent.width
127+
height: parent.height
128+
clip: true
129+
130+
model: _filteredModel
131+
spacing: Style.marginS
132+
133+
delegate: Rectangle {
134+
id: entityRow
135+
width: ListView.view.width
136+
height: Math.round(56 * Style.uiScaleRatio)
137+
color: Color.mSurfaceVariant
138+
radius: Style.radiusM
139+
140+
readonly property bool pinned: {
141+
root._pinVersion;
142+
return root._isPinned(model.entity_id);
143+
}
144+
145+
RowLayout {
146+
anchors {
147+
fill: parent
148+
margins: Style.marginM
149+
}
150+
spacing: Style.marginM
151+
152+
ColumnLayout {
153+
Layout.fillWidth: true
154+
spacing: Style.marginXXS
155+
156+
NText {
157+
text: model.friendly_name
158+
color: Color.mOnSurface
159+
pointSize: Style.fontSizeM
160+
elide: Text.ElideRight
161+
Layout.fillWidth: true
162+
}
163+
164+
NText {
165+
text: model.entity_id
166+
color: Color.mOnSurfaceVariant
167+
pointSize: Style.fontSizeS
168+
elide: Text.ElideRight
169+
Layout.fillWidth: true
170+
}
171+
}
172+
173+
NIconButton {
174+
icon: entityRow.pinned ? "pin-filled" : "pin"
175+
color: entityRow.pinned ? Color.mTertiary : Color.mOutline
176+
177+
onClicked: root._togglePin(model.entity_id)
178+
}
179+
}
180+
}
181+
}
182+
}
183+
}
184+
}

0 commit comments

Comments
 (0)