neiam
/home-assistant-plasmoid
Home Assistant Plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
better controls
acca0da · gmorell · 2025-06-23 22:05
Files changed
modified
contents/ui/EntityControl.qml
+345
−16
@@ -3,6 +3,7 @@ import QtQuick.Controls as QQC2
| 3 | 3 | import QtQuick.Layouts |
| 4 | 4 | import org.kde.plasma.components as PlasmaComponents3 |
| 5 | 5 | import org.kde.kirigami as Kirigami |
| 6 | +import org.kde.plasma.core as PlasmaCore | |
| 6 | 7 | |
| 7 | 8 | Item { |
| 8 | 9 | id: entityControl |
@@ -15,8 +16,30 @@ Item {
| 15 | 16 | property string buttonSize: "medium" |
| 16 | 17 | property var entityState: null |
| 17 | 18 | property bool isOn: entityState ? (entityState.state === "on" || entityState.state === "home" || entityState.state === "open") : false |
| 19 | + property bool isLoadingState: false | |
| 20 | + property bool hasValidState: entityState !== null && entityState !== undefined | |
| 21 | + property bool showExpandedControls: false | |
| 22 | + | |
| 23 | + // Light-specific properties | |
| 24 | + readonly property bool isLight: entityId.split('.')[0] === 'light' | |
| 25 | + readonly property bool supportsRgb: entityState && entityState.attributes && entityState.attributes.supported_color_modes && | |
| 26 | + (entityState.attributes.supported_color_modes.indexOf('rgb') !== -1 || | |
| 27 | + entityState.attributes.supported_color_modes.indexOf('rgbw') !== -1 || | |
| 28 | + entityState.attributes.supported_color_modes.indexOf('rgbww') !== -1) | |
| 29 | + readonly property bool supportsCct: entityState && entityState.attributes && entityState.attributes.supported_color_modes && | |
| 30 | + (entityState.attributes.supported_color_modes.indexOf('color_temp') !== -1) | |
| 31 | + readonly property bool supportsBrightness: entityState && entityState.attributes && entityState.attributes.supported_color_modes && | |
| 32 | + (entityState.attributes.supported_color_modes.indexOf('brightness') !== -1 || | |
| 33 | + entityState.attributes.brightness !== undefined) | |
| 34 | + readonly property int currentBrightness: entityState && entityState.attributes && entityState.attributes.brightness ? | |
| 35 | + Math.round(entityState.attributes.brightness / 255 * 100) : 0 | |
| 36 | + readonly property var currentRgbColor: entityState && entityState.attributes && entityState.attributes.rgb_color ? | |
| 37 | + entityState.attributes.rgb_color : [255, 255, 255] | |
| 38 | + readonly property int currentColorTemp: entityState && entityState.attributes && entityState.attributes.color_temp ? | |
| 39 | + entityState.attributes.color_temp : 370 | |
| 18 | 40 | |
| 19 | 41 | signal controlActivated(string entityId, string action, var data) |
| 42 | + signal stateRequested(string entityId) | |
| 20 | 43 | |
| 21 | 44 | readonly property int buttonSizePixels: { |
| 22 | 45 | switch (buttonSize) { |
@@ -26,8 +49,12 @@ Item {
| 26 | 49 | } |
| 27 | 50 | } |
| 28 | 51 | |
| 29 | − width: buttonSizePixels + (showLabel ? Kirigami.Units.smallSpacing : 0) | |
| 30 | − height: buttonSizePixels + (showLabel ? Kirigami.Units.gridUnit : 0) | |
| 52 | + readonly property bool hasExpandedControls: isLight && controlType === "light" && showExpandedControls && (supportsRgb || supportsCct || supportsBrightness) | |
| 53 | + | |
| 54 | + width: hasExpandedControls ? Math.max(buttonSizePixels * 4, Kirigami.Units.gridUnit * 8) : | |
| 55 | + buttonSizePixels + (showLabel ? Kirigami.Units.smallSpacing : 0) | |
| 56 | + height: hasExpandedControls ? buttonSizePixels + Kirigami.Units.gridUnit * 10 + (showLabel ? Kirigami.Units.gridUnit : 0) : | |
| 57 | + buttonSizePixels + (showLabel ? Kirigami.Units.gridUnit : 0) | |
| 31 | 58 | |
| 32 | 59 | ColumnLayout { |
| 33 | 60 | anchors.fill: parent |
@@ -42,7 +69,7 @@ Item {
| 42 | 69 | |
| 43 | 70 | // Visual state based on entity state |
| 44 | 71 | property color normalColor: Kirigami.Theme.buttonBackgroundColor |
| 45 | − property color activeColor: isOn ? Kirigami.Theme.positiveBackgroundColor : normalColor | |
| 72 | + property color activeColor: isOn ? (supportsRgb ? Qt.rgba(currentRgbColor[0]/255, currentRgbColor[1]/255, currentRgbColor[2]/255, 0.3) : Kirigami.Theme.positiveBackgroundColor) : normalColor | |
| 46 | 73 | |
| 47 | 74 | background: Rectangle { |
| 48 | 75 | radius: Kirigami.Units.cornerRadius |
@@ -55,13 +82,41 @@ Item {
| 55 | 82 | |
| 56 | 83 | Behavior on color { ColorAnimation { duration: 150 } } |
| 57 | 84 | Behavior on border.color { ColorAnimation { duration: 150 } } |
| 85 | + | |
| 86 | + // Advanced controls indicator for lights | |
| 87 | + Rectangle { | |
| 88 | + visible: isLight && controlType === "light" && (supportsRgb || supportsCct || supportsBrightness) | |
| 89 | + anchors.top: parent.top | |
| 90 | + anchors.right: parent.right | |
| 91 | + anchors.margins: 2 | |
| 92 | + width: 6 | |
| 93 | + height: 6 | |
| 94 | + radius: 3 | |
| 95 | + color: showExpandedControls ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor | |
| 96 | + | |
| 97 | + Behavior on color { ColorAnimation { duration: 150 } } | |
| 98 | + } | |
| 58 | 99 | } |
| 59 | 100 | |
| 60 | − contentItem: Kirigami.Icon { | |
| 61 | − source: getIconName() | |
| 62 | − color: isOn ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor | |
| 101 | + contentItem: RowLayout { | |
| 102 | + spacing: 0 | |
| 63 | 103 | |
| 64 | − Behavior on color { ColorAnimation { duration: 150 } } | |
| 104 | + Kirigami.Icon { | |
| 105 | + source: getIconName() | |
| 106 | + color: isOn ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor | |
| 107 | + Layout.fillWidth: true | |
| 108 | + Layout.fillHeight: true | |
| 109 | + | |
| 110 | + Behavior on color { ColorAnimation { duration: 150 } } | |
| 111 | + } | |
| 112 | + | |
| 113 | + // Loading indicator | |
| 114 | + PlasmaComponents3.BusyIndicator { | |
| 115 | + visible: isLoadingState | |
| 116 | + Layout.preferredWidth: Kirigami.Units.iconSizes.small | |
| 117 | + Layout.preferredHeight: Kirigami.Units.iconSizes.small | |
| 118 | + Layout.alignment: Qt.AlignCenter | |
| 119 | + } | |
| 65 | 120 | } |
| 66 | 121 | |
| 67 | 122 | PlasmaComponents3.ToolTip { |
@@ -69,6 +124,17 @@ Item {
| 69 | 124 | } |
| 70 | 125 | |
| 71 | 126 | onClicked: handleControlClick() |
| 127 | + | |
| 128 | + // Right-click to toggle expanded controls for lights | |
| 129 | + MouseArea { | |
| 130 | + anchors.fill: parent | |
| 131 | + acceptedButtons: Qt.RightButton | |
| 132 | + onClicked: { | |
| 133 | + if (isLight && controlType === "light" && (supportsRgb || supportsCct || supportsBrightness)) { | |
| 134 | + showExpandedControls = !showExpandedControls | |
| 135 | + } | |
| 136 | + } | |
| 137 | + } | |
| 72 | 138 | } |
| 73 | 139 | |
| 74 | 140 | // Entity label |
@@ -84,6 +150,212 @@ Item {
| 84 | 150 | |
| 85 | 151 | Behavior on color { ColorAnimation { duration: 150 } } |
| 86 | 152 | } |
| 153 | + | |
| 154 | + // Expanded Light Controls | |
| 155 | + Rectangle { | |
| 156 | + visible: hasExpandedControls | |
| 157 | + Layout.fillWidth: true | |
| 158 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 7 | |
| 159 | + color: Kirigami.Theme.backgroundColor | |
| 160 | + border.color: Kirigami.Theme.separatorColor | |
| 161 | + border.width: 1 | |
| 162 | + radius: Kirigami.Units.cornerRadius | |
| 163 | + | |
| 164 | + ColumnLayout { | |
| 165 | + anchors.fill: parent | |
| 166 | + anchors.margins: Kirigami.Units.smallSpacing | |
| 167 | + spacing: Kirigami.Units.smallSpacing | |
| 168 | + | |
| 169 | + // Brightness Control | |
| 170 | + RowLayout { | |
| 171 | + visible: supportsBrightness && isOn | |
| 172 | + Layout.fillWidth: true | |
| 173 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 1.2 | |
| 174 | + | |
| 175 | + Kirigami.Icon { | |
| 176 | + source: "brightness-low" | |
| 177 | + Layout.preferredWidth: Kirigami.Units.iconSizes.small | |
| 178 | + Layout.preferredHeight: Kirigami.Units.iconSizes.small | |
| 179 | + } | |
| 180 | + | |
| 181 | + QQC2.Slider { | |
| 182 | + id: brightnessSlider | |
| 183 | + Layout.fillWidth: true | |
| 184 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 1.2 | |
| 185 | + from: 1 | |
| 186 | + to: 100 | |
| 187 | + value: currentBrightness | |
| 188 | + stepSize: 1 | |
| 189 | + | |
| 190 | + onMoved: { | |
| 191 | + // Send brightness command on slider move | |
| 192 | + var brightnessValue = Math.round(value * 255 / 100) | |
| 193 | + controlActivated(entityId, "turn_on", { brightness: brightnessValue }) | |
| 194 | + } | |
| 195 | + | |
| 196 | + background: Rectangle { | |
| 197 | + x: brightnessSlider.leftPadding | |
| 198 | + y: brightnessSlider.topPadding + brightnessSlider.availableHeight / 2 - height / 2 | |
| 199 | + implicitWidth: 200 | |
| 200 | + implicitHeight: 8 | |
| 201 | + width: brightnessSlider.availableWidth | |
| 202 | + height: implicitHeight | |
| 203 | + radius: 4 | |
| 204 | + color: Kirigami.Theme.backgroundColor | |
| 205 | + border.color: Kirigami.Theme.separatorColor | |
| 206 | + | |
| 207 | + Rectangle { | |
| 208 | + width: brightnessSlider.visualPosition * parent.width | |
| 209 | + height: parent.height | |
| 210 | + color: Kirigami.Theme.highlightColor | |
| 211 | + radius: 4 | |
| 212 | + } | |
| 213 | + } | |
| 214 | + | |
| 215 | + handle: Rectangle { | |
| 216 | + x: brightnessSlider.leftPadding + brightnessSlider.visualPosition * (brightnessSlider.availableWidth - width) | |
| 217 | + y: brightnessSlider.topPadding + brightnessSlider.availableHeight / 2 - height / 2 | |
| 218 | + implicitWidth: 20 | |
| 219 | + implicitHeight: 20 | |
| 220 | + radius: 10 | |
| 221 | + color: brightnessSlider.pressed ? Qt.darker(Kirigami.Theme.highlightColor, 1.1) : Kirigami.Theme.highlightColor | |
| 222 | + border.color: Kirigami.Theme.backgroundColor | |
| 223 | + border.width: 2 | |
| 224 | + } | |
| 225 | + } | |
| 226 | + | |
| 227 | + PlasmaComponents3.Label { | |
| 228 | + text: currentBrightness + "%" | |
| 229 | + font.pointSize: Kirigami.Theme.smallFont.pointSize | |
| 230 | + Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5 | |
| 231 | + } | |
| 232 | + } | |
| 233 | + | |
| 234 | + // Color Temperature Control | |
| 235 | + RowLayout { | |
| 236 | + visible: supportsCct && isOn // Show alongside RGB controls | |
| 237 | + Layout.fillWidth: true | |
| 238 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 1.2 | |
| 239 | + | |
| 240 | + Kirigami.Icon { | |
| 241 | + source: "weather-clear-night" | |
| 242 | + Layout.preferredWidth: Kirigami.Units.iconSizes.small | |
| 243 | + Layout.preferredHeight: Kirigami.Units.iconSizes.small | |
| 244 | + } | |
| 245 | + | |
| 246 | + QQC2.Slider { | |
| 247 | + id: colorTempSlider | |
| 248 | + Layout.fillWidth: true | |
| 249 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 1.2 | |
| 250 | + from: 153 // ~6500K (cool) | |
| 251 | + to: 500 // ~2000K (warm) | |
| 252 | + value: currentColorTemp | |
| 253 | + stepSize: 1 | |
| 254 | + | |
| 255 | + onMoved: { | |
| 256 | + controlActivated(entityId, "turn_on", { color_temp: Math.round(value) }) | |
| 257 | + } | |
| 258 | + | |
| 259 | + background: Rectangle { | |
| 260 | + x: colorTempSlider.leftPadding | |
| 261 | + y: colorTempSlider.topPadding + colorTempSlider.availableHeight / 2 - height / 2 | |
| 262 | + implicitWidth: 200 | |
| 263 | + implicitHeight: 8 | |
| 264 | + width: colorTempSlider.availableWidth | |
| 265 | + height: implicitHeight | |
| 266 | + radius: 4 | |
| 267 | + | |
| 268 | + gradient: Gradient { | |
| 269 | + orientation: Gradient.Horizontal | |
| 270 | + GradientStop { position: 0.0; color: "#87CEEB" } // Cool blue | |
| 271 | + GradientStop { position: 1.0; color: "#FFA500" } // Warm orange | |
| 272 | + } | |
| 273 | + | |
| 274 | + border.color: Kirigami.Theme.separatorColor | |
| 275 | + border.width: 1 | |
| 276 | + } | |
| 277 | + | |
| 278 | + handle: Rectangle { | |
| 279 | + x: colorTempSlider.leftPadding + colorTempSlider.visualPosition * (colorTempSlider.availableWidth - width) | |
| 280 | + y: colorTempSlider.topPadding + colorTempSlider.availableHeight / 2 - height / 2 | |
| 281 | + implicitWidth: 20 | |
| 282 | + implicitHeight: 20 | |
| 283 | + radius: 10 | |
| 284 | + color: colorTempSlider.pressed ? Qt.darker(Kirigami.Theme.highlightColor, 1.1) : Kirigami.Theme.highlightColor | |
| 285 | + border.color: Kirigami.Theme.backgroundColor | |
| 286 | + border.width: 2 | |
| 287 | + } | |
| 288 | + } | |
| 289 | + | |
| 290 | + PlasmaComponents3.Label { | |
| 291 | + text: Math.round(1000000 / currentColorTemp) + "K" | |
| 292 | + font.pointSize: Kirigami.Theme.smallFont.pointSize | |
| 293 | + Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5 | |
| 294 | + } | |
| 295 | + } | |
| 296 | + | |
| 297 | + // RGB Color Control | |
| 298 | + RowLayout { | |
| 299 | + visible: supportsRgb && isOn | |
| 300 | + Layout.fillWidth: true | |
| 301 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5 | |
| 302 | + spacing: Kirigami.Units.smallSpacing | |
| 303 | + | |
| 304 | + // Color preset buttons | |
| 305 | + Repeater { | |
| 306 | + model: [ | |
| 307 | + { color: "#FF0000", name: "Red" }, | |
| 308 | + { color: "#00FF00", name: "Green" }, | |
| 309 | + { color: "#0000FF", name: "Blue" }, | |
| 310 | + { color: "#FFFF00", name: "Yellow" }, | |
| 311 | + { color: "#FF00FF", name: "Magenta" }, | |
| 312 | + { color: "#00FFFF", name: "Cyan" }, | |
| 313 | + { color: "#FFFFFF", name: "White" } | |
| 314 | + ] | |
| 315 | + | |
| 316 | + PlasmaComponents3.Button { | |
| 317 | + Layout.preferredWidth: Kirigami.Units.gridUnit * 1.2 | |
| 318 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 1.2 | |
| 319 | + | |
| 320 | + background: Rectangle { | |
| 321 | + radius: Kirigami.Units.cornerRadius | |
| 322 | + color: modelData.color | |
| 323 | + border.color: Qt.rgba(currentRgbColor[0]/255, currentRgbColor[1]/255, currentRgbColor[2]/255, 1.0) === color ? | |
| 324 | + Kirigami.Theme.highlightColor : Kirigami.Theme.separatorColor | |
| 325 | + border.width: Qt.rgba(currentRgbColor[0]/255, currentRgbColor[1]/255, currentRgbColor[2]/255, 1.0) === color ? 2 : 1 | |
| 326 | + } | |
| 327 | + | |
| 328 | + PlasmaComponents3.ToolTip { | |
| 329 | + text: modelData.name | |
| 330 | + } | |
| 331 | + | |
| 332 | + onClicked: { | |
| 333 | + var rgb = hexToRgb(modelData.color) | |
| 334 | + controlActivated(entityId, "turn_on", { rgb_color: [rgb.r, rgb.g, rgb.b] }) | |
| 335 | + } | |
| 336 | + } | |
| 337 | + } | |
| 338 | + } | |
| 339 | + | |
| 340 | + // Current color indicator | |
| 341 | + Rectangle { | |
| 342 | + visible: supportsRgb && isOn | |
| 343 | + Layout.fillWidth: true | |
| 344 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 0.8 | |
| 345 | + radius: Kirigami.Units.cornerRadius | |
| 346 | + color: Qt.rgba(currentRgbColor[0]/255, currentRgbColor[1]/255, currentRgbColor[2]/255, 1.0) | |
| 347 | + border.color: Kirigami.Theme.separatorColor | |
| 348 | + border.width: 1 | |
| 349 | + | |
| 350 | + PlasmaComponents3.Label { | |
| 351 | + anchors.centerIn: parent | |
| 352 | + text: "RGB: " + currentRgbColor[0] + ", " + currentRgbColor[1] + ", " + currentRgbColor[2] | |
| 353 | + font.pointSize: Kirigami.Theme.smallFont.pointSize | |
| 354 | + color: (currentRgbColor[0] + currentRgbColor[1] + currentRgbColor[2]) > 384 ? "black" : "white" | |
| 355 | + } | |
| 356 | + } | |
| 357 | + } | |
| 358 | + } | |
| 87 | 359 | } |
| 88 | 360 | |
| 89 | 361 | function getIconName() { |
@@ -116,21 +388,61 @@ Item {
| 116 | 388 | } |
| 117 | 389 | |
| 118 | 390 | function getTooltipText() { |
| 119 | − var stateText = entityState ? entityState.state : "unknown" | |
| 120 | − var tooltip = displayName + "\nState: " + stateText | |
| 391 | + var tooltip = displayName | |
| 121 | 392 | |
| 122 | − if (entityState && entityState.attributes) { | |
| 123 | − if (entityState.attributes.friendly_name) { | |
| 124 | − tooltip = entityState.attributes.friendly_name + "\nState: " + stateText | |
| 125 | − } | |
| 126 | − | |
| 127 | − // Add additional info for specific entity types | |
| 393 | + if (isLoadingState) { | |
| 394 | + tooltip += "\nState: Loading..." | |
| 395 | + return tooltip | |
| 396 | + } | |
| 397 | + | |
| 398 | + if (!hasValidState) { | |
| 399 | + tooltip += "\nState: Unavailable" | |
| 400 | + tooltip += "\nClick to refresh" | |
| 401 | + return tooltip | |
| 402 | + } | |
| 403 | + | |
| 404 | + var stateText = entityState.state || "unknown" | |
| 405 | + | |
| 406 | + // Use friendly name if available | |
| 407 | + if (entityState.attributes && entityState.attributes.friendly_name) { | |
| 408 | + tooltip = entityState.attributes.friendly_name | |
| 409 | + } | |
| 410 | + | |
| 411 | + tooltip += "\nState: " + stateText | |
| 412 | + | |
| 413 | + // Add additional info for specific entity types | |
| 414 | + if (entityState.attributes) { | |
| 128 | 415 | if (entityState.attributes.brightness) { |
| 129 | 416 | tooltip += "\nBrightness: " + Math.round(entityState.attributes.brightness / 255 * 100) + "%" |
| 130 | 417 | } |
| 418 | + if (entityState.attributes.rgb_color) { | |
| 419 | + tooltip += "\nRGB: " + entityState.attributes.rgb_color.join(", ") | |
| 420 | + } | |
| 421 | + if (entityState.attributes.color_temp) { | |
| 422 | + tooltip += "\nColor Temp: " + Math.round(1000000 / entityState.attributes.color_temp) + "K" | |
| 423 | + } | |
| 424 | + if (entityState.attributes.supported_color_modes && isLight) { | |
| 425 | + tooltip += "\nSupported: " + entityState.attributes.supported_color_modes.join(", ") | |
| 426 | + } | |
| 131 | 427 | if (entityState.attributes.temperature) { |
| 132 | 428 | tooltip += "\nTemperature: " + entityState.attributes.temperature + "°" |
| 133 | 429 | } |
| 430 | + if (entityState.attributes.humidity) { | |
| 431 | + tooltip += "\nHumidity: " + entityState.attributes.humidity + "%" | |
| 432 | + } | |
| 433 | + if (entityState.attributes.battery_level) { | |
| 434 | + tooltip += "\nBattery: " + entityState.attributes.battery_level + "%" | |
| 435 | + } | |
| 436 | + // Add last updated time | |
| 437 | + if (entityState.last_updated) { | |
| 438 | + var lastUpdated = new Date(entityState.last_updated) | |
| 439 | + tooltip += "\nLast updated: " + lastUpdated.toLocaleTimeString() | |
| 440 | + } | |
| 441 | + } | |
| 442 | + | |
| 443 | + // Add instruction for light controls | |
| 444 | + if (isLight && controlType === "light" && (supportsRgb || supportsCct || supportsBrightness)) { | |
| 445 | + tooltip += "\n\nRight-click for advanced controls" | |
| 134 | 446 | } |
| 135 | 447 | |
| 136 | 448 | return tooltip |
@@ -149,11 +461,28 @@ Item {
| 149 | 461 | controlActivated(entityId, "toggle", {}) |
| 150 | 462 | break |
| 151 | 463 | case "status": |
| 152 | − // Status displays don't perform actions, just show state | |
| 464 | + // Status displays don't perform actions, just refresh state | |
| 465 | + stateRequested(entityId) | |
| 153 | 466 | break |
| 154 | 467 | default: |
| 155 | 468 | controlActivated(entityId, "toggle", {}) |
| 156 | 469 | break |
| 157 | 470 | } |
| 471 | + | |
| 472 | + // Always try to refresh state after any action (or for status entities) | |
| 473 | + if (controlType !== "status") { | |
| 474 | + Qt.callLater(function() { | |
| 475 | + stateRequested(entityId) | |
| 476 | + }) | |
| 477 | + } | |
| 478 | + } | |
| 479 | + | |
| 480 | + function hexToRgb(hex) { | |
| 481 | + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); | |
| 482 | + return result ? { | |
| 483 | + r: parseInt(result[1], 16), | |
| 484 | + g: parseInt(result[2], 16), | |
| 485 | + b: parseInt(result[3], 16) | |
| 486 | + } : null; | |
| 158 | 487 | } |
| 159 | 488 | } |
modified
contents/ui/HomeAssistantAPI.qml
+43
−2
@@ -59,6 +59,7 @@ QtObject {
| 59 | 59 | function getState(entityId, callback) { |
| 60 | 60 | if (!baseUrl || !accessToken) { |
| 61 | 61 | error("Home Assistant URL and access token must be configured") |
| 62 | + if (callback) callback(false, null) | |
| 62 | 63 | return |
| 63 | 64 | } |
| 64 | 65 |
@@ -77,16 +78,39 @@ QtObject {
| 77 | 78 | stateChanged(entityId, state) |
| 78 | 79 | if (callback) callback(true, state) |
| 79 | 80 | } catch (e) { |
| 80 | − console.log("Error parsing state response:", e) | |
| 81 | + console.log("Error parsing state response for", entityId, ":", e) | |
| 82 | + error("Failed to parse state response for " + entityId) | |
| 81 | 83 | if (callback) callback(false, null) |
| 82 | 84 | } |
| 85 | + } else if (xhr.status === 404) { | |
| 86 | + console.log("Entity not found:", entityId) | |
| 87 | + error("Entity not found: " + entityId) | |
| 88 | + if (callback) callback(false, null) | |
| 89 | + } else if (xhr.status === 401) { | |
| 90 | + console.log("Authentication failed - check access token") | |
| 91 | + error("Authentication failed - check access token") | |
| 92 | + if (callback) callback(false, null) | |
| 83 | 93 | } else { |
| 84 | − console.log("Failed to get state for", entityId, ":", xhr.status) | |
| 94 | + console.log("Failed to get state for", entityId, ":", xhr.status, xhr.responseText) | |
| 95 | + error("Failed to get state for " + entityId + ": " + xhr.status) | |
| 85 | 96 | if (callback) callback(false, null) |
| 86 | 97 | } |
| 87 | 98 | } |
| 88 | 99 | } |
| 89 | 100 | |
| 101 | + xhr.onerror = function() { | |
| 102 | + console.log("Network error getting state for", entityId) | |
| 103 | + error("Network error getting state for " + entityId) | |
| 104 | + if (callback) callback(false, null) | |
| 105 | + } | |
| 106 | + | |
| 107 | + xhr.ontimeout = function() { | |
| 108 | + console.log("Timeout getting state for", entityId) | |
| 109 | + error("Timeout getting state for " + entityId) | |
| 110 | + if (callback) callback(false, null) | |
| 111 | + } | |
| 112 | + | |
| 113 | + xhr.timeout = 10000 // 10 second timeout | |
| 90 | 114 | xhr.send() |
| 91 | 115 | } |
| 92 | 116 |
@@ -250,4 +274,21 @@ QtObject {
| 250 | 274 | var domain = entityId.split('.')[0] |
| 251 | 275 | callService(domain, 'turn_off', entityId) |
| 252 | 276 | } |
| 277 | + | |
| 278 | + // Enhanced light control functions | |
| 279 | + function setLightBrightness(entityId, brightness) { | |
| 280 | + callService('light', 'turn_on', entityId, { brightness: brightness }) | |
| 281 | + } | |
| 282 | + | |
| 283 | + function setLightColor(entityId, rgbColor) { | |
| 284 | + callService('light', 'turn_on', entityId, { rgb_color: rgbColor }) | |
| 285 | + } | |
| 286 | + | |
| 287 | + function setLightColorTemp(entityId, colorTemp) { | |
| 288 | + callService('light', 'turn_on', entityId, { color_temp: colorTemp }) | |
| 289 | + } | |
| 290 | + | |
| 291 | + function setLightAdvanced(entityId, serviceData) { | |
| 292 | + callService('light', 'turn_on', entityId, serviceData) | |
| 293 | + } | |
| 253 | 294 | } |
modified
contents/ui/configEntities.qml
+6
−0
@@ -90,6 +90,7 @@ KCM.SimpleKCM {
| 90 | 90 | entityBrowser.editText = "" |
| 91 | 91 | entityBrowser.selectedEntityId = "" |
| 92 | 92 | entityBrowser.selectedEntityName = "" |
| 93 | + entityBrowser.selectedEntity = null | |
| 93 | 94 | newEntityIdField.text = "" |
| 94 | 95 | newEntityNameField.text = "" |
| 95 | 96 | newEntityIconField.text = "" |
@@ -234,6 +235,8 @@ KCM.SimpleKCM {
| 234 | 235 | Layout.fillWidth: true |
| 235 | 236 | |
| 236 | 237 | onEntitySelected: function(entity) { |
| 238 | + // Fill the entity ID field | |
| 239 | + newEntityIdField.text = entity.entity_id | |
| 237 | 240 | newEntityNameField.text = entity.friendly_name |
| 238 | 241 | newEntityIconField.text = entity.icon ? entity.icon.replace("mdi:", "") : "" |
| 239 | 242 |
@@ -277,8 +280,11 @@ KCM.SimpleKCM {
| 277 | 280 | |
| 278 | 281 | onTextChanged: { |
| 279 | 282 | if (text.trim()) { |
| 283 | + // Clear entity browser selection when manually typing | |
| 280 | 284 | entityBrowser.editText = "" |
| 281 | 285 | entityBrowser.selectedEntityId = text.trim() |
| 286 | + entityBrowser.selectedEntityName = "" | |
| 287 | + entityBrowser.selectedEntity = null | |
| 282 | 288 | } |
| 283 | 289 | } |
| 284 | 290 | } |
modified
contents/ui/main.qml
+28
−9
@@ -28,6 +28,7 @@ PlasmoidItem {
| 28 | 28 | property string buttonSize: plasmoid.configuration.buttonSize |
| 29 | 29 | |
| 30 | 30 | property var configuredEntities: [] |
| 31 | + property var entityStates: ({}) // Store entity states here | |
| 31 | 32 | property bool hasValidConfig: homeAssistantUrl && accessToken |
| 32 | 33 | property bool hasEntities: configuredEntities.length > 0 |
| 33 | 34 |
@@ -62,14 +63,13 @@ PlasmoidItem {
| 62 | 63 | accessToken: root.accessToken |
| 63 | 64 | |
| 64 | 65 | onStateChanged: function(entityId, state) { |
| 65 | − // Update the entity state in the grid | |
| 66 | − for (var i = 0; i < entityGrid.children.length; i++) { | |
| 67 | − var control = entityGrid.children[i] | |
| 68 | − if (control.entityId === entityId) { | |
| 69 | − control.entityState = state | |
| 70 | − break | |
| 71 | − } | |
| 72 | − } | |
| 66 | + // Store the state in our central state store | |
| 67 | + var newStates = root.entityStates | |
| 68 | + newStates[entityId] = state | |
| 69 | + root.entityStates = newStates | |
| 70 | + | |
| 71 | + // Trigger a change notification | |
| 72 | + root.entityStatesChanged() | |
| 73 | 73 | } |
| 74 | 74 | |
| 75 | 75 | onError: function(message) { |
@@ -165,6 +165,7 @@ PlasmoidItem {
| 165 | 165 | iconName: modelData.icon || "home-assistant" |
| 166 | 166 | showLabel: root.showEntityLabels |
| 167 | 167 | buttonSize: root.buttonSize |
| 168 | + entityState: root.entityStates[entityId] || null | |
| 168 | 169 | |
| 169 | 170 | onControlActivated: function(entityId, action, data) { |
| 170 | 171 | switch (action) { |
@@ -185,9 +186,27 @@ PlasmoidItem {
| 185 | 186 | }) |
| 186 | 187 | } |
| 187 | 188 | |
| 189 | + onStateRequested: function(entityId) { | |
| 190 | + if (root.hasValidConfig) { | |
| 191 | + isLoadingState = true | |
| 192 | + homeAssistantAPI.getState(entityId, function(success, state) { | |
| 193 | + isLoadingState = false | |
| 194 | + if (!success) { | |
| 195 | + console.log("Failed to refresh state for:", entityId) | |
| 196 | + } | |
| 197 | + }) | |
| 198 | + } | |
| 199 | + } | |
| 200 | + | |
| 188 | 201 | Component.onCompleted: { |
| 189 | 202 | if (root.hasValidConfig) { |
| 190 | − homeAssistantAPI.getState(entityId) | |
| 203 | + isLoadingState = true | |
| 204 | + homeAssistantAPI.getState(entityId, function(success, state) { | |
| 205 | + isLoadingState = false | |
| 206 | + if (!success) { | |
| 207 | + console.log("Failed to get initial state for:", entityId) | |
| 208 | + } | |
| 209 | + }) | |
| 191 | 210 | } |
| 192 | 211 | } |
| 193 | 212 | } |
Parents: 05507c6