feat: added home assistant widget
This commit is contained in:
@@ -36,6 +36,16 @@ PluginComponent {
|
|||||||
"y": 0.14,
|
"y": 0.14,
|
||||||
"defaultVisible": true
|
"defaultVisible": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "home",
|
||||||
|
"title": "Home",
|
||||||
|
"icon": "home",
|
||||||
|
"width": 420,
|
||||||
|
"height": 420,
|
||||||
|
"x": 0.56,
|
||||||
|
"y": 0.50,
|
||||||
|
"defaultVisible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "system",
|
"id": "system",
|
||||||
"title": "Performance",
|
"title": "Performance",
|
||||||
@@ -175,12 +185,42 @@ PluginComponent {
|
|||||||
return "SUCCESS";
|
return "SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function homeToggle(entityId: string) : string {
|
||||||
|
if (!entityId)
|
||||||
|
return "USAGE: homeToggle <entity_id>";
|
||||||
|
|
||||||
|
const domain = haService.domainOf(entityId);
|
||||||
|
if (["scene", "script", "button", "input_button"].indexOf(domain) !== -1)
|
||||||
|
haService.press(entityId);
|
||||||
|
else
|
||||||
|
haService.toggle(entityId);
|
||||||
|
return "SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function homeStatus() : string {
|
||||||
|
const picked = Array.isArray(root.pluginData.haEntities) ? root.pluginData.haEntities.length : 0;
|
||||||
|
return "configured=" + haService.configured + " url=" + (haService.baseUrl || "(none)") + " tokenChars=" + haService.token.length + " http=" + haService.lastStatus + " entities=" + haService.entityList.length + " picked=" + picked + " error=" + (haService.lastError || "(none)");
|
||||||
|
}
|
||||||
|
|
||||||
|
function homeRefresh() : string {
|
||||||
|
haService.refresh();
|
||||||
|
return "SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
function resetLayout() : string {
|
function resetLayout() : string {
|
||||||
root.resetLayout();
|
root.resetLayout();
|
||||||
return "SUCCESS";
|
return "SUCCESS";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property alias homeService: haService
|
||||||
|
|
||||||
|
GameBarHomeService {
|
||||||
|
id: haService
|
||||||
|
|
||||||
|
daemon: root
|
||||||
|
}
|
||||||
|
|
||||||
GameBarOverlay {
|
GameBarOverlay {
|
||||||
id: overlay
|
id: overlay
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: row
|
||||||
|
|
||||||
|
property var service: null
|
||||||
|
property string entityId: ""
|
||||||
|
|
||||||
|
readonly property var entity: service ? service.entityState(entityId) : null
|
||||||
|
readonly property string domain: service ? service.domainOf(entityId) : ""
|
||||||
|
readonly property var attributes: (entity && entity.attributes) ? entity.attributes : ({})
|
||||||
|
readonly property string state: entity ? String(entity.state) : "unavailable"
|
||||||
|
// Scenes/scripts/buttons report a timestamp or "unknown" rather than a state,
|
||||||
|
// so they must not be greyed out as unavailable.
|
||||||
|
readonly property bool available: entity !== null && (isButton || (state !== "unavailable" && state !== "unknown"))
|
||||||
|
readonly property bool isOn: state === "on" || state === "playing" || state === "open" || state === "home"
|
||||||
|
|
||||||
|
readonly property bool isToggleable: ["light", "switch", "fan", "input_boolean", "siren", "humidifier", "automation"].indexOf(domain) !== -1
|
||||||
|
readonly property bool isMediaPlayer: domain === "media_player"
|
||||||
|
readonly property bool isCover: domain === "cover"
|
||||||
|
readonly property bool isButton: ["scene", "script", "button", "input_button"].indexOf(domain) !== -1
|
||||||
|
// number / input_number expose min, max and step and are set with set_value.
|
||||||
|
readonly property bool isNumber: domain === "number" || domain === "input_number"
|
||||||
|
readonly property real numberMin: attributes.min !== undefined ? attributes.min : 0
|
||||||
|
readonly property real numberMax: attributes.max !== undefined ? attributes.max : 100
|
||||||
|
readonly property real numberStep: (attributes.step !== undefined && attributes.step > 0) ? attributes.step : 1
|
||||||
|
readonly property real numberValue: {
|
||||||
|
const parsed = parseFloat(state);
|
||||||
|
return isNaN(parsed) ? numberMin : parsed;
|
||||||
|
}
|
||||||
|
// DankSlider works in whole numbers, so only drive it directly when the
|
||||||
|
// entity's own range is integral; otherwise map it onto 0-100.
|
||||||
|
readonly property bool numberDirect: isNumber && Number.isInteger(numberMin) && Number.isInteger(numberMax) && Number.isInteger(numberStep)
|
||||||
|
readonly property string numberUnit: attributes.unit_of_measurement || ""
|
||||||
|
|
||||||
|
function numberFromSlider(sliderValue) {
|
||||||
|
if (row.numberDirect)
|
||||||
|
return sliderValue;
|
||||||
|
|
||||||
|
const span = row.numberMax - row.numberMin;
|
||||||
|
const raw = row.numberMin + (sliderValue / 100) * span;
|
||||||
|
const stepped = row.numberMin + Math.round((raw - row.numberMin) / row.numberStep) * row.numberStep;
|
||||||
|
return Math.min(row.numberMax, Math.max(row.numberMin, Math.round(stepped * 1000) / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool hasBrightness: domain === "light" && isOn && attributes.brightness !== undefined && attributes.brightness !== null
|
||||||
|
readonly property bool hasVolume: isMediaPlayer && attributes.volume_level !== undefined && attributes.volume_level !== null
|
||||||
|
readonly property bool hasSlider: hasBrightness || hasVolume || (isNumber && available)
|
||||||
|
|
||||||
|
readonly property int sliderPercent: {
|
||||||
|
if (hasBrightness)
|
||||||
|
return Math.round((attributes.brightness / 255) * 100);
|
||||||
|
if (hasVolume)
|
||||||
|
return Math.round(attributes.volume_level * 100);
|
||||||
|
if (isNumber) {
|
||||||
|
if (numberDirect)
|
||||||
|
return Math.round(numberValue);
|
||||||
|
|
||||||
|
const span = numberMax - numberMin;
|
||||||
|
return span > 0 ? Math.round(((numberValue - numberMin) / span) * 100) : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string icon: {
|
||||||
|
switch (domain) {
|
||||||
|
case "light":
|
||||||
|
return "lightbulb";
|
||||||
|
case "switch":
|
||||||
|
return "power_settings_new";
|
||||||
|
case "fan":
|
||||||
|
return "mode_fan";
|
||||||
|
case "media_player":
|
||||||
|
return "speaker";
|
||||||
|
case "cover":
|
||||||
|
return "blinds";
|
||||||
|
case "scene":
|
||||||
|
return "auto_awesome";
|
||||||
|
case "script":
|
||||||
|
return "play_arrow";
|
||||||
|
case "button":
|
||||||
|
case "input_button":
|
||||||
|
return "radio_button_checked";
|
||||||
|
case "climate":
|
||||||
|
return "thermostat";
|
||||||
|
case "number":
|
||||||
|
case "input_number":
|
||||||
|
return "tune";
|
||||||
|
case "lock":
|
||||||
|
return "lock";
|
||||||
|
case "binary_sensor":
|
||||||
|
return "sensors";
|
||||||
|
case "sensor":
|
||||||
|
return "monitoring";
|
||||||
|
}
|
||||||
|
return "home";
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string detail: {
|
||||||
|
if (!available)
|
||||||
|
return "Unavailable";
|
||||||
|
if (row.isButton) {
|
||||||
|
switch (domain) {
|
||||||
|
case "scene":
|
||||||
|
return "Scene";
|
||||||
|
case "script":
|
||||||
|
return "Script";
|
||||||
|
}
|
||||||
|
return "Button";
|
||||||
|
}
|
||||||
|
if (row.isMediaPlayer) {
|
||||||
|
const media = attributes.media_title || "";
|
||||||
|
return media !== "" ? state + " · " + media : state;
|
||||||
|
}
|
||||||
|
if (domain === "sensor" || domain === "binary_sensor")
|
||||||
|
return state + (attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : "");
|
||||||
|
if (domain === "climate" && attributes.current_temperature !== undefined)
|
||||||
|
return state + " · " + attributes.current_temperature + "°";
|
||||||
|
if (row.isNumber)
|
||||||
|
return row.numberValue + (row.numberUnit !== "" ? " " + row.numberUnit : "");
|
||||||
|
if (row.hasBrightness)
|
||||||
|
return row.sliderPercent + "%";
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function primaryAction() {
|
||||||
|
if (!service || !available)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (row.isButton)
|
||||||
|
service.press(entityId);
|
||||||
|
else if (row.isToggleable || row.isMediaPlayer)
|
||||||
|
service.toggle(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
height: hasSlider ? 74 : 48
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: row.isOn ? Theme.primarySelected : Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency)
|
||||||
|
border.color: row.isOn ? Theme.primary : Theme.outlineLight
|
||||||
|
border.width: row.isOn ? 1 : Theme.layerOutlineWidth
|
||||||
|
opacity: available ? 1 : 0.55
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: entityIcon
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 12
|
||||||
|
name: row.icon
|
||||||
|
size: Theme.iconSize - 6
|
||||||
|
color: row.isOn ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: labels
|
||||||
|
|
||||||
|
anchors.left: entityIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.right: controls.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 8
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: row.service ? row.service.friendlyName(row.entityId) : row.entityId
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: row.isOn ? Font.Medium : Font.Normal
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: row.detail
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: controls
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 6
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
visible: row.isCover
|
||||||
|
iconName: "keyboard_arrow_up"
|
||||||
|
buttonSize: 30
|
||||||
|
iconSize: 18
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: row.service.cover(row.entityId, "open_cover")
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
visible: row.isCover
|
||||||
|
iconName: "stop"
|
||||||
|
buttonSize: 30
|
||||||
|
iconSize: 16
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: row.service.cover(row.entityId, "stop_cover")
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
visible: row.isCover
|
||||||
|
iconName: "keyboard_arrow_down"
|
||||||
|
buttonSize: 30
|
||||||
|
iconSize: 18
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: row.service.cover(row.entityId, "close_cover")
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
visible: row.isMediaPlayer
|
||||||
|
iconName: row.attributes.is_volume_muted === true ? "volume_off" : "volume_up"
|
||||||
|
buttonSize: 30
|
||||||
|
iconSize: 17
|
||||||
|
iconColor: row.attributes.is_volume_muted === true ? Theme.error : Theme.surfaceText
|
||||||
|
onClicked: row.service.setMuted(row.entityId, row.attributes.is_volume_muted !== true)
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
visible: row.isMediaPlayer
|
||||||
|
iconName: row.state === "playing" ? "pause" : "play_arrow"
|
||||||
|
buttonSize: 30
|
||||||
|
iconSize: 18
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: row.service.callService("media_player", "media_play_pause", {
|
||||||
|
"entity_id": row.entityId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
visible: row.isToggleable
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: row.isOn
|
||||||
|
enabled: row.available
|
||||||
|
onToggled: isChecked => row.service.toggle(row.entityId)
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
visible: row.isButton
|
||||||
|
iconName: "play_arrow"
|
||||||
|
buttonSize: 30
|
||||||
|
iconSize: 18
|
||||||
|
iconColor: Theme.primary
|
||||||
|
onClicked: row.service.press(row.entityId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
id: slider
|
||||||
|
|
||||||
|
visible: row.hasSlider
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.bottomMargin: -4
|
||||||
|
minimum: row.numberDirect ? Math.round(row.numberMin) : 0
|
||||||
|
maximum: row.numberDirect ? Math.round(row.numberMax) : 100
|
||||||
|
step: row.numberDirect ? Math.round(row.numberStep) : 1
|
||||||
|
showValue: false
|
||||||
|
enabled: row.available
|
||||||
|
onSliderValueChanged: newValue => {
|
||||||
|
if (row.hasBrightness)
|
||||||
|
row.service.setBrightness(row.entityId, newValue);
|
||||||
|
else if (row.hasVolume)
|
||||||
|
row.service.setVolume(row.entityId, newValue);
|
||||||
|
else if (row.isNumber)
|
||||||
|
row.service.setNumber(row.entityId, row.numberFromSlider(newValue));
|
||||||
|
}
|
||||||
|
Component.onCompleted: slider.value = row.sliderPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the slider in step with Home Assistant without fighting the user's drag.
|
||||||
|
onSliderPercentChanged: {
|
||||||
|
if (!slider.isDragging)
|
||||||
|
slider.value = row.sliderPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: row.hasSlider ? slider.top : parent.bottom
|
||||||
|
anchors.right: controls.left
|
||||||
|
enabled: row.available && (row.isToggleable || row.isMediaPlayer || row.isButton)
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: row.primaryAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: service
|
||||||
|
|
||||||
|
property var daemon: null
|
||||||
|
|
||||||
|
readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({})
|
||||||
|
readonly property string baseUrl: String(pluginData.haUrl || "").trim().replace(/\/+$/, "")
|
||||||
|
readonly property string token: String(pluginData.haToken || "").trim()
|
||||||
|
readonly property int pollInterval: Math.max(2, pluginData.haPollInterval || 5) * 1000
|
||||||
|
readonly property bool configured: baseUrl !== "" && token !== ""
|
||||||
|
|
||||||
|
// entity_id -> state object, plus a sorted array for pickers.
|
||||||
|
property var entities: ({})
|
||||||
|
property var entityList: []
|
||||||
|
property string lastError: ""
|
||||||
|
property bool loading: false
|
||||||
|
property bool everLoaded: false
|
||||||
|
property int lastStatus: 0
|
||||||
|
|
||||||
|
// Polling only runs while something is actually showing HA data.
|
||||||
|
property int consumers: 0
|
||||||
|
|
||||||
|
signal refreshed
|
||||||
|
signal callFailed(string message)
|
||||||
|
|
||||||
|
function addConsumer() {
|
||||||
|
service.consumers = service.consumers + 1;
|
||||||
|
if (service.consumers === 1)
|
||||||
|
service.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeConsumer() {
|
||||||
|
service.consumers = Math.max(0, service.consumers - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityState(entityId) {
|
||||||
|
return service.entities[entityId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function friendlyName(entityId) {
|
||||||
|
const state = service.entityState(entityId);
|
||||||
|
if (state && state.attributes && state.attributes.friendly_name)
|
||||||
|
return state.attributes.friendly_name;
|
||||||
|
|
||||||
|
return entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function domainOf(entityId) {
|
||||||
|
const parts = String(entityId || "").split(".");
|
||||||
|
return parts.length > 1 ? parts[0] : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A changed URL or token invalidates whatever the last attempt reported.
|
||||||
|
onTokenChanged: service.lastError = ""
|
||||||
|
onBaseUrlChanged: service.lastError = ""
|
||||||
|
|
||||||
|
// The token is handed to curl through the environment, so it never shows up
|
||||||
|
// in the process arguments (and there is no stdin race to lose it to).
|
||||||
|
readonly property string curlGetScript: 'exec curl -sS --fail-with-body --connect-timeout 4 --max-time 10 -w "\n%{http_code}" -H "Authorization: Bearer $GAMEBAR_HA_TOKEN" -H "Content-Type: application/json" "$1"'
|
||||||
|
readonly property string curlPostScript: 'exec curl -sS --fail-with-body --connect-timeout 4 --max-time 10 -H "Authorization: Bearer $GAMEBAR_HA_TOKEN" -H "Content-Type: application/json" -X POST -d "$2" "$1"'
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!service.configured) {
|
||||||
|
service.lastError = "Not configured";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (service.loading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
service.loading = true;
|
||||||
|
statesProc.environment = {
|
||||||
|
"GAMEBAR_HA_TOKEN": service.token
|
||||||
|
};
|
||||||
|
statesProc.command = ["sh", "-c", service.curlGetScript, "gameBar", service.baseUrl + "/api/states"];
|
||||||
|
statesProc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function callService(domain, method, data) {
|
||||||
|
if (!service.configured)
|
||||||
|
return;
|
||||||
|
|
||||||
|
service.callQueue.push({
|
||||||
|
"url": service.baseUrl + "/api/services/" + domain + "/" + method,
|
||||||
|
"body": JSON.stringify(data || {})
|
||||||
|
});
|
||||||
|
service.drainQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
property var callQueue: []
|
||||||
|
|
||||||
|
function drainQueue() {
|
||||||
|
if (callProc.running || service.callQueue.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const next = service.callQueue.shift();
|
||||||
|
callProc.environment = {
|
||||||
|
"GAMEBAR_HA_TOKEN": service.token
|
||||||
|
};
|
||||||
|
callProc.command = ["sh", "-c", service.curlPostScript, "gameBar", next.url, next.body];
|
||||||
|
callProc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(entityId) {
|
||||||
|
const domain = service.domainOf(entityId);
|
||||||
|
const toggleDomains = ["light", "switch", "fan", "input_boolean", "siren", "humidifier", "media_player", "automation"];
|
||||||
|
const target = toggleDomains.indexOf(domain) !== -1 ? domain : "homeassistant";
|
||||||
|
service.callService(target, "toggle", {
|
||||||
|
"entity_id": entityId
|
||||||
|
});
|
||||||
|
service.scheduleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBrightness(entityId, percent) {
|
||||||
|
service.callService("light", "turn_on", {
|
||||||
|
"entity_id": entityId,
|
||||||
|
"brightness_pct": Math.round(percent)
|
||||||
|
});
|
||||||
|
service.scheduleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVolume(entityId, percent) {
|
||||||
|
service.callService("media_player", "volume_set", {
|
||||||
|
"entity_id": entityId,
|
||||||
|
"volume_level": Math.max(0, Math.min(1, percent / 100))
|
||||||
|
});
|
||||||
|
service.scheduleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMuted(entityId, muted) {
|
||||||
|
service.callService("media_player", "volume_mute", {
|
||||||
|
"entity_id": entityId,
|
||||||
|
"is_volume_muted": muted === true
|
||||||
|
});
|
||||||
|
service.scheduleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNumber(entityId, value) {
|
||||||
|
service.callService(service.domainOf(entityId), "set_value", {
|
||||||
|
"entity_id": entityId,
|
||||||
|
"value": value
|
||||||
|
});
|
||||||
|
service.scheduleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function press(entityId) {
|
||||||
|
const domain = service.domainOf(entityId);
|
||||||
|
if (domain === "scene" || domain === "script")
|
||||||
|
service.callService(domain, "turn_on", {
|
||||||
|
"entity_id": entityId
|
||||||
|
});
|
||||||
|
else
|
||||||
|
service.callService(domain, "press", {
|
||||||
|
"entity_id": entityId
|
||||||
|
});
|
||||||
|
service.scheduleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cover(entityId, action) {
|
||||||
|
service.callService("cover", action, {
|
||||||
|
"entity_id": entityId
|
||||||
|
});
|
||||||
|
service.scheduleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRefresh() {
|
||||||
|
followUpTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: statesProc
|
||||||
|
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
service.loading = false;
|
||||||
|
let raw = String(text || "").trim();
|
||||||
|
if (raw === "")
|
||||||
|
return;
|
||||||
|
|
||||||
|
// curl -w appends the HTTP status on its own line.
|
||||||
|
const split = raw.lastIndexOf("\n");
|
||||||
|
if (split !== -1) {
|
||||||
|
const tail = raw.substring(split + 1).trim();
|
||||||
|
if (/^[0-9]{3}$/.test(tail)) {
|
||||||
|
service.lastStatus = parseInt(tail, 10);
|
||||||
|
raw = raw.substring(0, split).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service.lastStatus === 401) {
|
||||||
|
service.lastError = "401 Unauthorized - check the access token";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
service.lastError = parsed.message || "Unexpected response";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = {};
|
||||||
|
for (const item of parsed)
|
||||||
|
map[item.entity_id] = item;
|
||||||
|
|
||||||
|
service.entities = map;
|
||||||
|
service.entityList = parsed.slice().sort((a, b) => a.entity_id.localeCompare(b.entity_id));
|
||||||
|
service.lastError = "";
|
||||||
|
service.everLoaded = true;
|
||||||
|
service.refreshed();
|
||||||
|
} catch (e) {
|
||||||
|
service.lastError = "Invalid response from Home Assistant";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const raw = String(text || "").trim();
|
||||||
|
if (raw !== "")
|
||||||
|
service.lastError = raw.split("\n")[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
service.loading = false;
|
||||||
|
if (exitCode !== 0 && service.lastError === "")
|
||||||
|
service.lastError = "Request failed (curl " + exitCode + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: callProc
|
||||||
|
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const raw = String(text || "").trim();
|
||||||
|
if (raw !== "")
|
||||||
|
service.callFailed(raw.split("\n")[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode !== 0)
|
||||||
|
service.callFailed("Service call failed (curl " + exitCode + ")");
|
||||||
|
|
||||||
|
service.drainQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: pollTimer
|
||||||
|
|
||||||
|
interval: service.pollInterval
|
||||||
|
repeat: true
|
||||||
|
running: service.consumers > 0 && service.configured
|
||||||
|
onTriggered: service.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Home Assistant needs a moment to apply a service call before it shows up
|
||||||
|
// in /api/states.
|
||||||
|
Timer {
|
||||||
|
id: followUpTimer
|
||||||
|
|
||||||
|
interval: 400
|
||||||
|
repeat: false
|
||||||
|
onTriggered: service.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: widget
|
||||||
|
|
||||||
|
property var service: null
|
||||||
|
|
||||||
|
readonly property var selected: {
|
||||||
|
const stored = (service && service.pluginData) ? service.pluginData.haEntities : null;
|
||||||
|
return Array.isArray(stored) ? stored : [];
|
||||||
|
}
|
||||||
|
readonly property bool configured: service ? service.configured : false
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (service)
|
||||||
|
service.addConsumer();
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (service)
|
||||||
|
service.removeConsumer();
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: list
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: widget.configured && widget.selected.length > 0
|
||||||
|
clip: true
|
||||||
|
contentHeight: entityColumn.height
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: entityColumn
|
||||||
|
|
||||||
|
width: list.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: widget.selected
|
||||||
|
|
||||||
|
delegate: GameBarHomeRow {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
width: entityColumn.width
|
||||||
|
service: widget.service
|
||||||
|
entityId: modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingL
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: !list.visible
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: widget.configured ? "playlist_add" : "home"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: widget.configured ? "No entities picked yet.\nChoose them in Settings > Plugins > Game Bar." : "Home Assistant is not set up.\nAdd your URL and token in Settings > Plugins > Game Bar."
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
text: widget.service ? widget.service.lastError : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: text !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,6 +104,8 @@ FocusScope {
|
|||||||
return audioWidgetComponent;
|
return audioWidgetComponent;
|
||||||
case "apps":
|
case "apps":
|
||||||
return appsWidgetComponent;
|
return appsWidgetComponent;
|
||||||
|
case "home":
|
||||||
|
return homeWidgetComponent;
|
||||||
case "system":
|
case "system":
|
||||||
return systemWidgetComponent;
|
return systemWidgetComponent;
|
||||||
}
|
}
|
||||||
@@ -124,6 +126,14 @@ FocusScope {
|
|||||||
GameBarAppMixer {}
|
GameBarAppMixer {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: homeWidgetComponent
|
||||||
|
|
||||||
|
GameBarHomeWidget {
|
||||||
|
service: root.daemon ? root.daemon.homeService : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: systemWidgetComponent
|
id: systemWidgetComponent
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,37 @@ PluginSettings {
|
|||||||
rightIcon: "dark_mode"
|
rightIcon: "dark_mode"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StringSetting {
|
||||||
|
settingKey: "haUrl"
|
||||||
|
label: "Home Assistant URL"
|
||||||
|
description: "e.g. http://homeassistant.local:8123"
|
||||||
|
placeholder: "http://homeassistant.local:8123"
|
||||||
|
}
|
||||||
|
|
||||||
|
StringSetting {
|
||||||
|
settingKey: "haToken"
|
||||||
|
label: "Long-lived access token"
|
||||||
|
description: "Home Assistant profile page, bottom of Security tab"
|
||||||
|
placeholder: "eyJhbGciOi..."
|
||||||
|
}
|
||||||
|
|
||||||
|
SliderSetting {
|
||||||
|
settingKey: "haPollInterval"
|
||||||
|
label: "Home Assistant refresh interval"
|
||||||
|
description: "How often entity states are polled while the Home widget is open"
|
||||||
|
defaultValue: 5
|
||||||
|
minimum: 2
|
||||||
|
maximum: 30
|
||||||
|
unit: "s"
|
||||||
|
leftIcon: "speed"
|
||||||
|
rightIcon: "schedule"
|
||||||
|
}
|
||||||
|
|
||||||
|
GameBarHomeSettings {
|
||||||
|
width: parent.width
|
||||||
|
settings: settings
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: resetColumn.implicitHeight
|
height: resetColumn.implicitHeight
|
||||||
|
|||||||
@@ -9,6 +9,28 @@ An overlay for [DankMaterialShell](https://danklinux.com/docs/dankmaterialshell/
|
|||||||
| **Audio** | Master output volume + mute, output device switching, microphone volume + mute, input device switching |
|
| **Audio** | Master output volume + mute, output device switching, microphone volume + mute, input device switching |
|
||||||
| **Apps** | Per-application volume sliders with individual mute, app icon and what is playing |
|
| **Apps** | Per-application volume sliders with individual mute, app icon and what is playing |
|
||||||
| **Performance** | CPU (usage, temperature), memory (used / total), network (rx/tx), disk (root usage, read/write) |
|
| **Performance** | CPU (usage, temperature), memory (used / total), network (rx/tx), disk (root usage, read/write) |
|
||||||
|
| **Home** | Home Assistant entities you pick: lights (toggle + brightness), numbers (slider over the entity's own min/max/step), switches/fans, media players (volume, mute, play/pause), covers, scenes and scripts, plus read-only sensors |
|
||||||
|
|
||||||
|
## Home Assistant
|
||||||
|
|
||||||
|
Settings → Plugins → Game Bar:
|
||||||
|
|
||||||
|
1. **Home Assistant URL**, e.g. `http://homeassistant.local:8123`
|
||||||
|
2. **Long-lived access token** — HA profile page → *Security* → *Create token*.
|
||||||
|
It is stored in `plugin_settings.json`, which is world-readable by default
|
||||||
|
(`chmod 600` it if that matters to you).
|
||||||
|
3. **Refresh**, then tick the entities you want in the Home widget.
|
||||||
|
|
||||||
|
State is polled (default every 5 s) only while the Home widget is open. The token is handed to
|
||||||
|
`curl` through the process environment, so it never shows up in the process list.
|
||||||
|
|
||||||
|
If a refresh fails, `dms ipc call gameBar homeStatus` shows what the plugin actually sent and got
|
||||||
|
back. A `401` means Home Assistant rejected the token; compare with
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <token>" https://your-ha/api/
|
||||||
|
```
|
||||||
|
Note that editing `plugin_settings.json` by hand does not reach a running shell.
|
||||||
|
|
||||||
### IPC commands
|
### IPC commands
|
||||||
|
|
||||||
@@ -17,7 +39,10 @@ An overlay for [DankMaterialShell](https://danklinux.com/docs/dankmaterialshell/
|
|||||||
| `dms ipc call gameBar toggle` | Toggle the overlay |
|
| `dms ipc call gameBar toggle` | Toggle the overlay |
|
||||||
| `dms ipc call gameBar open` | Open the overlay |
|
| `dms ipc call gameBar open` | Open the overlay |
|
||||||
| `dms ipc call gameBar close` | Close the overlay |
|
| `dms ipc call gameBar close` | Close the overlay |
|
||||||
| `dms ipc call gameBar widget audio\|apps\|system` | Show/hide one widget |
|
| `dms ipc call gameBar widget audio\|apps\|home\|system` | Show/hide one widget |
|
||||||
|
| `dms ipc call gameBar homeToggle <entity_id>` | Toggle an entity (activates a scene/script/button) |
|
||||||
|
| `dms ipc call gameBar homeRefresh` | Re-poll Home Assistant now |
|
||||||
|
| `dms ipc call gameBar homeStatus` | Print URL, token length, HTTP status, entity/picked counts and last error |
|
||||||
| `dms ipc call gameBar resetLayout` | Forget all saved widget positions |
|
| `dms ipc call gameBar resetLayout` | Forget all saved widget positions |
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
@@ -25,6 +50,7 @@ An overlay for [DankMaterialShell](https://danklinux.com/docs/dankmaterialshell/
|
|||||||
Settings → Plugins → Game Bar (gear icon):
|
Settings → Plugins → Game Bar (gear icon):
|
||||||
|
|
||||||
- **Background dimming** — how dark the desktop behind the overlay gets (0–95%).
|
- **Background dimming** — how dark the desktop behind the overlay gets (0–95%).
|
||||||
|
- **Home Assistant** — URL, access token, refresh interval, and the entity picker.
|
||||||
- **Reset widget positions** — back to the default arrangement on every monitor.
|
- **Reset widget positions** — back to the default arrangement on every monitor.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
+7
-5
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://github.com/AvengeMedia/DankMaterialShell/raw/refs/heads/master/PLUGINS/plugin-schema.json",
|
"$schema": "https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/master/quickshell/PLUGINS/plugin-schema.json",
|
||||||
"id": "gameBar",
|
"id": "gameBar",
|
||||||
"name": "Game Bar",
|
"name": "Game Bar",
|
||||||
"description": "Game Bar style overlay with quick actions and system monitoring",
|
"description": "Game Bar style overlay with movable widgets for quick settings and controls",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "keule",
|
"author": "keule2",
|
||||||
"icon": "sports_esports",
|
"icon": "sports_esports",
|
||||||
"type": "daemon",
|
"type": "daemon",
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
"requires_dms": ">=1.5.0",
|
"requires_dms": ">=1.5.0",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"settings_read",
|
"settings_read",
|
||||||
"settings_write"
|
"settings_write",
|
||||||
|
"process",
|
||||||
|
"network"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user