neiam
/home-assistant-plasmoid
Home Assistant Plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
wew lads
05507c6 · gmorell · 2025-06-23 21:11
Files changed
modified
README.md
+202
−53
| 2 | 2 | |
| 3 | 3 | This is a Plasma 6 compatible widget for controlling Home Assistant entities from your KDE desktop. |
| 4 | 4 | |
| 5 | −## Version 2.0 Changes (Plasma 6 Compatibility) | |
| 6 | − | |
| 7 | −This widget has been updated for Plasma 6 compatibility with the following changes: | |
| 8 | − | |
| 9 | −### Key Updates: | |
| 10 | −1. **Qt/QML Imports**: Updated to use version-less imports compatible with Qt6 | |
| 11 | −2. **Kirigami Integration**: Added proper Kirigami units and theming | |
| 12 | −3. **Configuration System**: Updated to use KCM.SimpleKCM for better integration | |
| 13 | −4. **Icon Handling**: Replaced deprecated PlasmaCore.IconItem with Kirigami.Icon | |
| 14 | −5. **Internationalization**: Added proper i18n() calls for all user-visible strings | |
| 15 | −6. **Layout Improvements**: Better responsive design using Kirigami.Units | |
| 16 | −7. **Tooltips**: Added proper tooltip integration | |
| 17 | − | |
| 18 | −### Technical Changes: | |
| 19 | −- Removed version numbers from import statements | |
| 20 | −- Updated `PlasmaCore.IconItem` to `Kirigami.Icon` | |
| 21 | −- Updated configuration UI to use `KCM.SimpleKCM` | |
| 22 | −- Added `QtQuick.Controls` as `QQC2` alias | |
| 23 | −- Updated action calls to use `plasmoid.internalAction()` | |
| 24 | −- Added proper switchWidth/switchHeight properties for compact representation | |
| 25 | − | |
| 26 | −## Installation | |
| 27 | − | |
| 28 | −The widget is installed in the standard Plasma widget location: | |
| 29 | −``` | |
| 30 | −~/.local/share/plasma/plasmoids/org.neiam.kde.homeassistant/ | |
| 31 | −``` | |
| 5 | +## Version 2.0 Features | |
| 6 | + | |
| 7 | +### ✨ **NEW: Entity Control System** | |
| 8 | +- **Configure Custom Entities**: Add any Home Assistant entity (lights, switches, automations, etc.) | |
| 9 | +- **Interactive Controls**: Click to toggle entities directly from your desktop | |
| 10 | +- **Visual State Feedback**: Buttons show current entity state with color coding | |
| 11 | +- **Flexible Layout**: Configurable grid layout with 1-6 entities per row | |
| 12 | +- **Smart Icons**: Auto-detects appropriate icons based on entity type | |
| 13 | +- **Real-time Updates**: Automatically refreshes entity states at configurable intervals | |
| 14 | + | |
| 15 | +### 🔍 **NEW: Entity Autocomplete & Browser** | |
| 16 | +- **Live Entity Browser**: Browse all entities directly from Home Assistant | |
| 17 | +- **Search & Filter**: Search by name or entity ID, filter by domain type | |
| 18 | +- **Visual Entity List**: See entity states, icons, and domain badges | |
| 19 | +- **Auto-fill Fields**: Automatically populate display names and icons | |
| 20 | +- **Smart Suggestions**: Recommends appropriate control types per entity | |
| 21 | +- **Connection Test**: Verify Home Assistant connection before browsing | |
| 22 | +- **Live Preview**: See exactly how your entity control will look before adding it | |
| 23 | +- **Interactive Icon Picker**: Browse and select icons visually from Plasma's icon collection | |
| 24 | + | |
| 25 | +### 🎛️ **Control Types Supported** | |
| 26 | +- **Toggle Button**: For lights, switches, automations | |
| 27 | +- **Switch Control**: Explicit on/off controls | |
| 28 | +- **Light Control**: Optimized for light entities | |
| 29 | +- **Status Display**: Read-only state display | |
| 30 | + | |
| 31 | +### 🎨 **Customization Options** | |
| 32 | +- **Button Sizes**: Small, Medium, Large | |
| 33 | +- **Show/Hide Labels**: Toggle entity names | |
| 34 | +- **Custom Icons**: Override default icons per entity | |
| 35 | +- **Grid Layout**: 1-6 entities per row | |
| 36 | +- **Update Intervals**: 5-300 seconds | |
| 32 | 37 | |
| 33 | 38 | ## Configuration |
| 34 | 39 | |
| 35 | −1. Right-click the widget and select "Configure" | |
| 36 | −2. Enter your Home Assistant URL (e.g., http://homeassistant.local:8123) | |
| 37 | −3. Enter your long-lived access token from Home Assistant | |
| 38 | −4. Set the update interval (5-300 seconds) | |
| 39 | − | |
| 40 | −## Compatibility | |
| 40 | +### **Step 1: General Settings** | |
| 41 | +1. Right-click the widget → "Configure" | |
| 42 | +2. In the **General** tab: | |
| 43 | + - Enter your Home Assistant URL (e.g., `http://homeassistant.local:8123`) | |
| 44 | + - Enter your long-lived access token | |
| 45 | + - Set update interval (5-300 seconds) | |
| 46 | + | |
| 47 | +### **Step 2: Entity Configuration** | |
| 48 | +1. Switch to the **Entities** tab | |
| 49 | +2. Configure display options: | |
| 50 | + - **Entities per row**: 1-6 (default: 3) | |
| 51 | + - **Show entity labels**: On/Off | |
| 52 | + - **Button size**: Small/Medium/Large | |
| 53 | + | |
| 54 | +3. **Browse & Add Entities** (NEW!): | |
| 55 | + - **Entity Browser**: Click the dropdown to browse all Home Assistant entities | |
| 56 | + - **Search**: Type to filter entities by name or ID | |
| 57 | + - **Domain Filter**: Click buttons to filter by entity type (light, switch, etc.) | |
| 58 | + - **Visual Selection**: See entity states, icons, and current values | |
| 59 | + - **Auto-fill**: Selected entities automatically populate fields | |
| 60 | + - **OR Manual Entry**: Enter entity details manually if needed | |
| 61 | + | |
| 62 | +4. **Configure Entity & Preview**: | |
| 63 | + - **Display Name**: Friendly name (auto-filled from Home Assistant) | |
| 64 | + - **Control Type**: Auto-suggested based on entity type | |
| 65 | + - **Icon Selection**: | |
| 66 | + - Manual entry in text field OR | |
| 67 | + - **Browse Icons**: Visual icon picker with search and categories | |
| 68 | + - **Icon Preview**: See current icon in real-time | |
| 69 | + - **Reset**: Clear custom icon to use auto-detected | |
| 70 | + - **Live Preview**: See exactly how the control will look with current settings | |
| 71 | + - **Interactive Preview**: Click the preview to test control behavior (safe mode) | |
| 72 | + - **Settings Indicators**: Visual indicators for labels, size, and control type | |
| 73 | + - Click **Add Entity** | |
| 74 | + | |
| 75 | +5. **Manage Entities**: | |
| 76 | + - Swipe left on any entity to remove it | |
| 77 | + - View entity details and domain badges | |
| 78 | + - Duplicate entities are automatically updated | |
| 79 | + | |
| 80 | +### **Step 3: Usage** | |
| 81 | +- **Compact Mode**: Shows badge with entity count | |
| 82 | +- **Click to Expand**: Opens full control panel | |
| 83 | +- **Entity Controls**: Click buttons to toggle entities | |
| 84 | +- **Tooltips**: Hover for detailed state information | |
| 85 | + | |
| 86 | +## Supported Entity Types | |
| 87 | + | |
| 88 | +| Domain | Auto-Icon | Supported Actions | | |
| 89 | +|--------|-----------|-------------------| | |
| 90 | +| `light.*` | 💡 Lightbulb | Toggle, Turn On/Off | | |
| 91 | +| `switch.*` | 🔌 Toggle Switch | Toggle, Turn On/Off | | |
| 92 | +| `fan.*` | 🌪️ Windy | Toggle, Turn On/Off | | |
| 93 | +| `automation.*` | ▶️ Play/Stop | Toggle, Turn On/Off | | |
| 94 | +| `input_boolean.*` | ☑️ Checkbox | Toggle | | |
| 95 | +| `climate.*` | 🌡️ Thermometer | Status Display | | |
| 96 | +| `lock.*` | 🔒 Lock | Status Display | | |
| 97 | +| `cover.*` | 🪟 Window | Status Display | | |
| 98 | + | |
| 99 | +## Entity ID Examples | |
| 41 | 100 | |
| 42 | −- **Plasma 6+**: Fully compatible | |
| 43 | −- **Qt 6+**: Required | |
| 44 | −- **KDE Frameworks 6+**: Required | |
| 101 | +``` | |
| 102 | +# Lights | |
| 103 | +light.living_room | |
| 104 | +light.bedroom_lamp | |
| 105 | +light.kitchen_ceiling | |
| 106 | + | |
| 107 | +# Switches | |
| 108 | +switch.living_room_fan | |
| 109 | +switch.coffee_maker | |
| 110 | +switch.outdoor_lights | |
| 111 | + | |
| 112 | +# Automations | |
| 113 | +automation.morning_routine | |
| 114 | +automation.security_mode | |
| 115 | +automation.bedtime_lights | |
| 116 | + | |
| 117 | +# Input Booleans | |
| 118 | +input_boolean.guest_mode | |
| 119 | +input_boolean.vacation_mode | |
| 120 | + | |
| 121 | +# Climate | |
| 122 | +climate.thermostat | |
| 123 | +climate.bedroom_ac | |
| 124 | +``` | |
| 45 | 125 | |
| 46 | −## Development Notes | |
| 126 | +## Plasma 6 Compatibility Updates | |
| 47 | 127 | |
| 48 | −This widget serves as a foundation for Home Assistant integration. Future versions may include: | |
| 49 | −- Entity state display | |
| 50 | −- Entity control buttons | |
| 51 | −- Custom entity selection | |
| 52 | −- Real-time updates via WebSocket | |
| 128 | +### Key Technical Changes: | |
| 129 | +- ✅ **Qt6 Compatible**: Version-less imports | |
| 130 | +- ✅ **Kirigami Integration**: Proper theming and units | |
| 131 | +- ✅ **KCM Configuration**: Modern config system | |
| 132 | +- ✅ **Component Updates**: Replaced deprecated elements | |
| 133 | +- ✅ **API Integration**: Full Home Assistant REST API support | |
| 134 | +- ✅ **State Management**: Real-time entity state tracking | |
| 135 | +- ✅ **Error Handling**: Robust error handling and logging | |
| 53 | 136 | |
| 54 | −## Files Structure | |
| 137 | +### Files Structure | |
| 55 | 138 | |
| 56 | 139 | ``` |
| 57 | 140 | contents/ |
| 58 | 141 | ├── config/ |
| 59 | −│ ├── config.qml # Configuration categories | |
| 60 | −│ └── main.xml # Configuration schema | |
| 142 | +│ ├── config.qml # Configuration categories | |
| 143 | +│ └── main.xml # Configuration schema | |
| 61 | 144 | └── ui/ |
| 62 | − ├── main.qml # Main widget UI | |
| 63 | − └── configGeneral.qml # Configuration page UI | |
| 64 | −metadata.json # Widget metadata | |
| 145 | + ├── main.qml # Main widget UI with entity grid | |
| 146 | + ├── configGeneral.qml # Connection configuration | |
| 147 | + ├── configEntities.qml # Entity management UI | |
| 148 | + ├── HomeAssistantAPI.qml # API communication layer | |
| 149 | + └── EntityControl.qml # Individual entity control component | |
| 150 | +metadata.json # Widget metadata | |
| 151 | +``` | |
| 152 | + | |
| 153 | +## Home Assistant Setup | |
| 154 | + | |
| 155 | +### 1. Create Long-Lived Access Token | |
| 156 | +1. Open Home Assistant | |
| 157 | +2. Go to Profile → Security | |
| 158 | +3. Scroll to "Long-Lived Access Tokens" | |
| 159 | +4. Click "Create Token" | |
| 160 | +5. Name it (e.g., "Plasma Widget") | |
| 161 | +6. Copy the token (you won't see it again!) | |
| 162 | + | |
| 163 | +### 2. Test API Access | |
| 164 | +```bash | |
| 165 | +# Test connection (replace URL and TOKEN) | |
| 166 | +curl -X GET \ | |
| 167 | + -H "Authorization: Bearer YOUR_TOKEN_HERE" \ | |
| 168 | + -H "Content-Type: application/json" \ | |
| 169 | + http://homeassistant.local:8123/api/ | |
| 65 | 170 | ``` |
| 66 | 171 | |
| 67 | 172 | ## Troubleshooting |
| 68 | 173 | |
| 69 | −If the widget doesn't load: | |
| 70 | −1. Check Plasma version (must be 6+) | |
| 71 | −2. Verify all files are in the correct location | |
| 72 | −3. Restart Plasma: `systemctl --user restart plasma-plasmashell` | |
| 73 | −4. Check system logs: `journalctl --user -f | grep plasma` | |
| 174 | +### Widget Not Loading | |
| 175 | +1. Check Plasma version: `plasmashell --version` (must be 6+) | |
| 176 | +2. Verify file permissions: `ls -la ~/.local/share/plasma/plasmoids/` | |
| 177 | +3. Check logs: `journalctl --user -f | grep plasma` | |
| 178 | + | |
| 179 | +### Connection Issues | |
| 180 | +1. Verify Home Assistant URL is accessible | |
| 181 | +2. Test API token in browser/curl | |
| 182 | +3. Check firewall settings | |
| 183 | +4. Ensure Home Assistant is running | |
| 184 | + | |
| 185 | +### Entity Control Issues | |
| 186 | +1. Verify entity IDs exist in Home Assistant | |
| 187 | +2. Check entity permissions (some may be read-only) | |
| 188 | +3. Review Home Assistant logs for API errors | |
| 189 | +4. Test entity control in Home Assistant UI first | |
| 190 | + | |
| 191 | +### Performance Tips | |
| 192 | +- Use longer update intervals (60+ seconds) for better performance | |
| 193 | +- Limit entities to 6-12 for optimal responsiveness | |
| 194 | +- Use "Status Display" for read-only entities | |
| 195 | + | |
| 196 | +## Advanced Usage | |
| 197 | + | |
| 198 | +### Custom Icons | |
| 199 | +Use any KDE icon name or file path: | |
| 200 | +- `lightbulb`, `power-socket`, `fan` | |
| 201 | +- `weather-clear`, `weather-cloudy` | |
| 202 | +- `/path/to/custom/icon.svg` | |
| 203 | + | |
| 204 | +### Entity Naming | |
| 205 | +- Use descriptive names: "Living Room Lights" vs "light.living_room" | |
| 206 | +- Keep names short for better layout | |
| 207 | +- Use consistent naming conventions | |
| 208 | + | |
| 209 | +## Compatibility | |
| 210 | + | |
| 211 | +- **Plasma 6+**: Required | |
| 212 | +- **Qt 6+**: Required | |
| 213 | +- **KDE Frameworks 6+**: Required | |
| 214 | +- **Home Assistant 2023.1+**: Recommended | |
| 215 | + | |
| 216 | +## Contributing | |
| 217 | + | |
| 218 | +This widget is open source. Feel free to contribute improvements: | |
| 219 | +- Enhanced entity types support | |
| 220 | +- Additional control types | |
| 221 | +- UI/UX improvements | |
| 222 | +- Performance optimizations |
modified
contents/config/config.qml
+5
−0
| 7 | 7 | icon: "configure" |
| 8 | 8 | source: "configGeneral.qml" |
| 9 | 9 | } |
| 10 | + ConfigCategory { | |
| 11 | + name: i18n("Entities") | |
| 12 | + icon: "view-list-details" | |
| 13 | + source: "configEntities.qml" | |
| 14 | + } | |
| 10 | 15 | } |
modified
contents/config/main.xml
+18
−3
| 23 | 23 | </group> |
| 24 | 24 | |
| 25 | 25 | <group name="Entities"> |
| 26 | − <entry name="selectedEntities" type="StringList"> | |
| 27 | − <label>Selected Entities</label> | |
| 28 | − <default></default> | |
| 26 | + <entry name="configuredEntities" type="String"> | |
| 27 | + <label>Configured Entities (JSON)</label> | |
| 28 | + <default>[]</default> | |
| 29 | + </entry> | |
| 30 | + | |
| 31 | + <entry name="maxEntitiesPerRow" type="Int"> | |
| 32 | + <label>Maximum entities per row</label> | |
| 33 | + <default>3</default> | |
| 34 | + </entry> | |
| 35 | + | |
| 36 | + <entry name="showEntityLabels" type="Bool"> | |
| 37 | + <label>Show entity labels</label> | |
| 38 | + <default>true</default> | |
| 39 | + </entry> | |
| 40 | + | |
| 41 | + <entry name="buttonSize" type="String"> | |
| 42 | + <label>Button size</label> | |
| 43 | + <default>medium</default> | |
| 29 | 44 | </entry> |
| 30 | 45 | </group> |
| 31 | 46 | </kcfg> |
added
contents/ui/EntityBrowser.qml
+300
−0
| 1 | +import QtQuick | |
| 2 | +import QtQuick.Controls as QQC2 | |
| 3 | +import QtQuick.Layouts | |
| 4 | +import org.kde.kirigami as Kirigami | |
| 5 | + | |
| 6 | +QQC2.ComboBox { | |
| 7 | + id: entityBrowser | |
| 8 | + | |
| 9 | + property alias homeAssistantAPI: homeAssistantAPI | |
| 10 | + property string selectedEntityId: "" | |
| 11 | + property string selectedEntityName: "" | |
| 12 | + property var selectedEntity: null | |
| 13 | + property var filteredDomains: [] // Empty = all domains, or ["light", "switch"] etc. | |
| 14 | + property string searchQuery: "" | |
| 15 | + | |
| 16 | + signal entitySelected(var entity) | |
| 17 | + | |
| 18 | + editable: true | |
| 19 | + textRole: "display_text" | |
| 20 | + valueRole: "entity_id" | |
| 21 | + | |
| 22 | + // Custom model for entities | |
| 23 | + model: ListModel { | |
| 24 | + id: entityModel | |
| 25 | + } | |
| 26 | + | |
| 27 | + onEditTextChanged: { | |
| 28 | + searchQuery = editText | |
| 29 | + filterEntities() | |
| 30 | + } | |
| 31 | + | |
| 32 | + onActivated: function(index) { | |
| 33 | + if (index >= 0 && index < entityModel.count) { | |
| 34 | + var entity = entityModel.get(index) | |
| 35 | + selectedEntityId = entity.entity_id | |
| 36 | + selectedEntityName = entity.friendly_name | |
| 37 | + selectedEntity = entity | |
| 38 | + entitySelected(entity) | |
| 39 | + } | |
| 40 | + } | |
| 41 | + | |
| 42 | + // Custom popup for better entity browsing | |
| 43 | + popup: QQC2.Popup { | |
| 44 | + id: entityPopup | |
| 45 | + y: entityBrowser.height | |
| 46 | + width: Math.max(entityBrowser.width, Kirigami.Units.gridUnit * 20) | |
| 47 | + height: Math.min(contentItem.implicitHeight, Kirigami.Units.gridUnit * 15) | |
| 48 | + | |
| 49 | + contentItem: ColumnLayout { | |
| 50 | + spacing: Kirigami.Units.smallSpacing | |
| 51 | + | |
| 52 | + // Domain filter | |
| 53 | + RowLayout { | |
| 54 | + Layout.fillWidth: true | |
| 55 | + | |
| 56 | + QQC2.Label { | |
| 57 | + text: i18n("Filter:") | |
| 58 | + font.pointSize: Kirigami.Theme.smallFont.pointSize | |
| 59 | + } | |
| 60 | + | |
| 61 | + Repeater { | |
| 62 | + model: ["all", "light", "switch", "fan", "automation", "climate", "cover", "lock"] | |
| 63 | + | |
| 64 | + QQC2.Button { | |
| 65 | + text: modelData === "all" ? i18n("All") : modelData | |
| 66 | + checkable: true | |
| 67 | + checked: filteredDomains.length === 0 && modelData === "all" || | |
| 68 | + filteredDomains.indexOf(modelData) !== -1 | |
| 69 | + | |
| 70 | + onClicked: { | |
| 71 | + if (modelData === "all") { | |
| 72 | + filteredDomains = [] | |
| 73 | + } else { | |
| 74 | + var domains = filteredDomains.slice() // copy array | |
| 75 | + var index = domains.indexOf(modelData) | |
| 76 | + if (index !== -1) { | |
| 77 | + domains.splice(index, 1) | |
| 78 | + } else { | |
| 79 | + domains.push(modelData) | |
| 80 | + } | |
| 81 | + filteredDomains = domains | |
| 82 | + } | |
| 83 | + filterEntities() | |
| 84 | + } | |
| 85 | + | |
| 86 | + font.pointSize: Kirigami.Theme.smallFont.pointSize | |
| 87 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 1.2 | |
| 88 | + } | |
| 89 | + } | |
| 90 | + } | |
| 91 | + | |
| 92 | + Kirigami.Separator { | |
| 93 | + Layout.fillWidth: true | |
| 94 | + } | |
| 95 | + | |
| 96 | + // Entity list | |
| 97 | + QQC2.ScrollView { | |
| 98 | + Layout.fillWidth: true | |
| 99 | + Layout.fillHeight: true | |
| 100 | + | |
| 101 | + ListView { | |
| 102 | + id: entityListView | |
| 103 | + model: entityModel | |
| 104 | + | |
| 105 | + delegate: QQC2.ItemDelegate { | |
| 106 | + width: entityListView.width | |
| 107 | + | |
| 108 | + contentItem: RowLayout { | |
| 109 | + spacing: Kirigami.Units.smallSpacing | |
| 110 | + | |
| 111 | + // Domain badge | |
| 112 | + Rectangle { | |
| 113 | + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 | |
| 114 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 0.8 | |
| 115 | + radius: Kirigami.Units.cornerRadius | |
| 116 | + color: getDomainColor(model.domain) | |
| 117 | + | |
| 118 | + QQC2.Label { | |
| 119 | + anchors.centerIn: parent | |
| 120 | + text: model.domain | |
| 121 | + color: "white" | |
| 122 | + font.pointSize: Kirigami.Theme.smallFont.pointSize | |
| 123 | + font.bold: true | |
| 124 | + } | |
| 125 | + } | |
| 126 | + | |
| 127 | + // Entity icon | |
| 128 | + Kirigami.Icon { | |
| 129 | + source: getEntityIcon(model) | |
| 130 | + Layout.preferredWidth: Kirigami.Units.iconSizes.small | |
| 131 | + Layout.preferredHeight: Kirigami.Units.iconSizes.small | |
| 132 | + } | |
| 133 | + | |
| 134 | + // Entity info | |
| 135 | + ColumnLayout { | |
| 136 | + Layout.fillWidth: true | |
| 137 | + spacing: 0 | |
| 138 | + | |
| 139 | + QQC2.Label { | |
| 140 | + text: model.friendly_name | |
| 141 | + font.bold: true | |
| 142 | + Layout.fillWidth: true | |
| 143 | + elide: Text.ElideRight | |
| 144 | + } | |
| 145 | + | |
| 146 | + QQC2.Label { | |
| 147 | + text: model.entity_id + " • " + model.state | |
| 148 | + color: Kirigami.Theme.disabledTextColor | |
| 149 | + font.pointSize: Kirigami.Theme.smallFont.pointSize | |
| 150 | + Layout.fillWidth: true | |
| 151 | + elide: Text.ElideRight | |
| 152 | + } | |
| 153 | + } | |
| 154 | + | |
| 155 | + // State indicator | |
| 156 | + Rectangle { | |
| 157 | + Layout.preferredWidth: Kirigami.Units.gridUnit * 0.5 | |
| 158 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 0.5 | |
| 159 | + radius: width / 2 | |
| 160 | + color: getStateColor(model.state) | |
| 161 | + } | |
| 162 | + } | |
| 163 | + | |
| 164 | + onClicked: { | |
| 165 | + entityBrowser.currentIndex = index | |
| 166 | + selectedEntityId = model.entity_id | |
| 167 | + selectedEntityName = model.friendly_name | |
| 168 | + selectedEntity = model | |
| 169 | + entitySelected(model) | |
| 170 | + entityPopup.close() | |
| 171 | + } | |
| 172 | + } | |
| 173 | + } | |
| 174 | + } | |
| 175 | + | |
| 176 | + // Status bar | |
| 177 | + RowLayout { | |
| 178 | + Layout.fillWidth: true | |
| 179 | + | |
| 180 | + QQC2.Label { | |
| 181 | + text: i18n("%1 entities found", entityModel.count) | |
| 182 | + font.pointSize: Kirigami.Theme.smallFont.pointSize | |
| 183 | + color: Kirigami.Theme.disabledTextColor | |
| 184 | + } | |
| 185 | + | |
| 186 | + Item { Layout.fillWidth: true } | |
| 187 | + | |
| 188 | + QQC2.BusyIndicator { | |
| 189 | + visible: homeAssistantAPI.isLoadingEntities | |
| 190 | + Layout.preferredWidth: Kirigami.Units.iconSizes.small | |
| 191 | + Layout.preferredHeight: Kirigami.Units.iconSizes.small | |
| 192 | + } | |
| 193 | + } | |
| 194 | + } | |
| 195 | + } | |
| 196 | + | |
| 197 | + // Home Assistant API instance | |
| 198 | + HomeAssistantAPI { | |
| 199 | + id: homeAssistantAPI | |
| 200 | + | |
| 201 | + onEntitiesLoaded: function(entities) { | |
| 202 | + filterEntities() | |
| 203 | + } | |
| 204 | + | |
| 205 | + onError: function(message) { | |
| 206 | + console.log("Entity browser API error:", message) | |
| 207 | + } | |
| 208 | + } | |
| 209 | + | |
| 210 | + function loadEntities() { | |
| 211 | + if (homeAssistantAPI.baseUrl && homeAssistantAPI.accessToken) { | |
| 212 | + homeAssistantAPI.getAllEntities() | |
| 213 | + } | |
| 214 | + } | |
| 215 | + | |
| 216 | + function filterEntities() { | |
| 217 | + entityModel.clear() | |
| 218 | + | |
| 219 | + if (!homeAssistantAPI.allEntities || homeAssistantAPI.allEntities.length === 0) { | |
| 220 | + return | |
| 221 | + } | |
| 222 | + | |
| 223 | + var searchResults = homeAssistantAPI.searchEntities(searchQuery, filteredDomains) | |
| 224 | + | |
| 225 | + // Limit results to prevent UI lag | |
| 226 | + var maxResults = 100 | |
| 227 | + for (var i = 0; i < Math.min(searchResults.length, maxResults); i++) { | |
| 228 | + var entity = searchResults[i] | |
| 229 | + entityModel.append({ | |
| 230 | + entity_id: entity.entity_id, | |
| 231 | + friendly_name: entity.friendly_name, | |
| 232 | + domain: entity.domain, | |
| 233 | + state: entity.state, | |
| 234 | + icon: entity.icon, | |
| 235 | + device_class: entity.device_class, | |
| 236 | + unit_of_measurement: entity.unit_of_measurement, | |
| 237 | + display_text: entity.friendly_name + " (" + entity.entity_id + ")" | |
| 238 | + }) | |
| 239 | + } | |
| 240 | + } | |
| 241 | + | |
| 242 | + function getDomainColor(domain) { | |
| 243 | + switch (domain) { | |
| 244 | + case "light": return "#FFA726" | |
| 245 | + case "switch": return "#42A5F5" | |
| 246 | + case "fan": return "#66BB6A" | |
| 247 | + case "automation": return "#AB47BC" | |
| 248 | + case "climate": return "#EF5350" | |
| 249 | + case "cover": return "#26A69A" | |
| 250 | + case "lock": return "#8D6E63" | |
| 251 | + case "sensor": return "#78909C" | |
| 252 | + case "binary_sensor": return "#90A4AE" | |
| 253 | + default: return "#9E9E9E" | |
| 254 | + } | |
| 255 | + } | |
| 256 | + | |
| 257 | + function getEntityIcon(entity) { | |
| 258 | + if (entity.icon) { | |
| 259 | + return entity.icon.replace("mdi:", "") | |
| 260 | + } | |
| 261 | + | |
| 262 | + switch (entity.domain) { | |
| 263 | + case "light": return entity.state === "on" ? "lightbulb-on" : "lightbulb" | |
| 264 | + case "switch": return entity.state === "on" ? "toggle-switch" : "toggle-switch-off" | |
| 265 | + case "fan": return "fan" | |
| 266 | + case "automation": return "playlist-play" | |
| 267 | + case "climate": return "thermometer" | |
| 268 | + case "cover": return "window-close" | |
| 269 | + case "lock": return entity.state === "locked" ? "lock" : "lock-open" | |
| 270 | + case "sensor": return "gauge" | |
| 271 | + case "binary_sensor": return "checkbox-marked-circle" | |
| 272 | + default: return "home-assistant" | |
| 273 | + } | |
| 274 | + } | |
| 275 | + | |
| 276 | + function getStateColor(state) { | |
| 277 | + switch (state) { | |
| 278 | + case "on": | |
| 279 | + case "open": | |
| 280 | + case "home": | |
| 281 | + case "unlocked": | |
| 282 | + return Kirigami.Theme.positiveTextColor | |
| 283 | + case "off": | |
| 284 | + case "closed": | |
| 285 | + case "away": | |
| 286 | + case "locked": | |
| 287 | + return Kirigami.Theme.disabledTextColor | |
| 288 | + case "unavailable": | |
| 289 | + case "unknown": | |
| 290 | + return Kirigami.Theme.negativeTextColor | |
| 291 | + default: | |
| 292 | + return Kirigami.Theme.textColor | |
| 293 | + } | |
| 294 | + } | |
| 295 | + | |
| 296 | + Component.onCompleted: { | |
| 297 | + // Auto-load entities when component is ready | |
| 298 | + Qt.callLater(loadEntities) | |
| 299 | + } | |
| 300 | +} |
added
contents/ui/EntityControl.qml
+159
−0
| 1 | +import QtQuick | |
| 2 | +import QtQuick.Controls as QQC2 | |
| 3 | +import QtQuick.Layouts | |
| 4 | +import org.kde.plasma.components as PlasmaComponents3 | |
| 5 | +import org.kde.kirigami as Kirigami | |
| 6 | + | |
| 7 | +Item { | |
| 8 | + id: entityControl | |
| 9 | + | |
| 10 | + property string entityId: "" | |
| 11 | + property string displayName: "" | |
| 12 | + property string controlType: "toggle" | |
| 13 | + property string iconName: "home-assistant" | |
| 14 | + property bool showLabel: true | |
| 15 | + property string buttonSize: "medium" | |
| 16 | + property var entityState: null | |
| 17 | + property bool isOn: entityState ? (entityState.state === "on" || entityState.state === "home" || entityState.state === "open") : false | |
| 18 | + | |
| 19 | + signal controlActivated(string entityId, string action, var data) | |
| 20 | + | |
| 21 | + readonly property int buttonSizePixels: { | |
| 22 | + switch (buttonSize) { | |
| 23 | + case "small": return Kirigami.Units.iconSizes.medium | |
| 24 | + case "large": return Kirigami.Units.iconSizes.huge | |
| 25 | + default: return Kirigami.Units.iconSizes.large | |
| 26 | + } | |
| 27 | + } | |
| 28 | + | |
| 29 | + width: buttonSizePixels + (showLabel ? Kirigami.Units.smallSpacing : 0) | |
| 30 | + height: buttonSizePixels + (showLabel ? Kirigami.Units.gridUnit : 0) | |
| 31 | + | |
| 32 | + ColumnLayout { | |
| 33 | + anchors.fill: parent | |
| 34 | + spacing: Kirigami.Units.smallSpacing | |
| 35 | + | |
| 36 | + // Main control button | |
| 37 | + PlasmaComponents3.Button { | |
| 38 | + id: controlButton | |
| 39 | + Layout.preferredWidth: buttonSizePixels | |
| 40 | + Layout.preferredHeight: buttonSizePixels | |
| 41 | + Layout.alignment: Qt.AlignHCenter | |
| 42 | + | |
| 43 | + // Visual state based on entity state | |
| 44 | + property color normalColor: Kirigami.Theme.buttonBackgroundColor | |
| 45 | + property color activeColor: isOn ? Kirigami.Theme.positiveBackgroundColor : normalColor | |
| 46 | + | |
| 47 | + background: Rectangle { | |
| 48 | + radius: Kirigami.Units.cornerRadius | |
| 49 | + color: controlButton.pressed ? Qt.darker(controlButton.activeColor, 1.1) : | |
| 50 | + controlButton.hovered ? Qt.lighter(controlButton.activeColor, 1.1) : | |
| 51 | + controlButton.activeColor | |
| 52 | + | |
| 53 | + border.color: isOn ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.buttonFocusColor | |
| 54 | + border.width: isOn ? 2 : 1 | |
| 55 | + | |
| 56 | + Behavior on color { ColorAnimation { duration: 150 } } | |
| 57 | + Behavior on border.color { ColorAnimation { duration: 150 } } | |
| 58 | + } | |
| 59 | + | |
| 60 | + contentItem: Kirigami.Icon { | |
| 61 | + source: getIconName() | |
| 62 | + color: isOn ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor | |
| 63 | + | |
| 64 | + Behavior on color { ColorAnimation { duration: 150 } } | |
| 65 | + } | |
| 66 | + | |
| 67 | + PlasmaComponents3.ToolTip { | |
| 68 | + text: getTooltipText() | |
| 69 | + } | |
| 70 | + | |
| 71 | + onClicked: handleControlClick() | |
| 72 | + } | |
| 73 | + | |
| 74 | + // Entity label | |
| 75 | + PlasmaComponents3.Label { | |
| 76 | + visible: showLabel | |
| 77 | + text: displayName | |
| 78 | + Layout.fillWidth: true | |
| 79 | + Layout.alignment: Qt.AlignHCenter | |
| 80 | + horizontalAlignment: Text.AlignHCenter | |
| 81 | + elide: Text.ElideRight | |
| 82 | + font.pointSize: Kirigami.Theme.smallFont.pointSize | |
| 83 | + color: isOn ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor | |
| 84 | + | |
| 85 | + Behavior on color { ColorAnimation { duration: 150 } } | |
| 86 | + } | |
| 87 | + } | |
| 88 | + | |
| 89 | + function getIconName() { | |
| 90 | + if (iconName && iconName !== "home-assistant") { | |
| 91 | + return iconName | |
| 92 | + } | |
| 93 | + | |
| 94 | + // Auto-detect icon based on entity type and state | |
| 95 | + var domain = entityId.split('.')[0] | |
| 96 | + switch (domain) { | |
| 97 | + case 'light': | |
| 98 | + return isOn ? "lightbulb-on" : "lightbulb" | |
| 99 | + case 'switch': | |
| 100 | + return isOn ? "toggle-switch" : "toggle-switch-off" | |
| 101 | + case 'fan': | |
| 102 | + return isOn ? "weather-windy" : "fan" | |
| 103 | + case 'automation': | |
| 104 | + return isOn ? "playlist-play" : "playlist-remove" | |
| 105 | + case 'climate': | |
| 106 | + return "thermometer" | |
| 107 | + case 'lock': | |
| 108 | + return entityState && entityState.state === "locked" ? "lock" : "lock-open" | |
| 109 | + case 'cover': | |
| 110 | + return entityState && entityState.state === "open" ? "window-open" : "window-close" | |
| 111 | + case 'input_boolean': | |
| 112 | + return isOn ? "checkbox-marked" : "checkbox-blank-outline" | |
| 113 | + default: | |
| 114 | + return "home-assistant" | |
| 115 | + } | |
| 116 | + } | |
| 117 | + | |
| 118 | + function getTooltipText() { | |
| 119 | + var stateText = entityState ? entityState.state : "unknown" | |
| 120 | + var tooltip = displayName + "\nState: " + stateText | |
| 121 | + | |
| 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 | |
| 128 | + if (entityState.attributes.brightness) { | |
| 129 | + tooltip += "\nBrightness: " + Math.round(entityState.attributes.brightness / 255 * 100) + "%" | |
| 130 | + } | |
| 131 | + if (entityState.attributes.temperature) { | |
| 132 | + tooltip += "\nTemperature: " + entityState.attributes.temperature + "°" | |
| 133 | + } | |
| 134 | + } | |
| 135 | + | |
| 136 | + return tooltip | |
| 137 | + } | |
| 138 | + | |
| 139 | + function handleControlClick() { | |
| 140 | + switch (controlType) { | |
| 141 | + case "toggle": | |
| 142 | + controlActivated(entityId, "toggle", {}) | |
| 143 | + break | |
| 144 | + case "switch": | |
| 145 | + controlActivated(entityId, isOn ? "turn_off" : "turn_on", {}) | |
| 146 | + break | |
| 147 | + case "light": | |
| 148 | + // For lights, we could show a context menu or brightness slider | |
| 149 | + controlActivated(entityId, "toggle", {}) | |
| 150 | + break | |
| 151 | + case "status": | |
| 152 | + // Status displays don't perform actions, just show state | |
| 153 | + break | |
| 154 | + default: | |
| 155 | + controlActivated(entityId, "toggle", {}) | |
| 156 | + break | |
| 157 | + } | |
| 158 | + } | |
| 159 | +} |
added
contents/ui/HomeAssistantAPI.qml
+253
−0
Click to load diff…
added
contents/ui/IconPicker.qml
+276
−0
Click to load diff…
added
contents/ui/configEntities.qml
+613
−0
Click to load diff…
modified
contents/ui/configGeneral.qml
+121
−0
Click to load diff…
modified
contents/ui/main.qml
+189
−12
Click to load diff…
modified
metadata.json
+2
−1
Click to load diff…
Parents: 6924a66