This page documents the user interface components that render entity displays and configuration screens.
Display Components
CompactRepresentation
The compact view shown in the system tray or panel.
Location: package/contents/ui/CompactRepresentation.qml
Type: MouseArea
Implementation:
import QtQuick
import "./mdi"
MouseArea {
onClicked: root.expanded = !root.expanded
MdiIcon {
name: "home-assistant"
anchors.fill: parent
anchors.centerIn: parent
}
}
Behavior:
- Displays the Home Assistant logo icon
- Toggles plasmoid expansion on click
- Appears in system tray or panel when plasmoid is in compact mode
Visual Properties:
- Icon: Material Design Icon
home-assistant
- Size: Determined by panel/tray constraints
- Interactive: Clickable to expand
FullRepresentation
The expanded view showing entity tiles in a grid.
Location: package/contents/ui/FullRepresentation.qml
Type: PlasmaExtras.Representation
Structure:
PlasmaExtras.Representation {
Loader { // Main content
sourceComponent: gridComponent
active: root.initialized
}
StatusIndicator { // Error overlay
icon: "data-error"
message: ha?.errorString || ''
}
Loader { // ClientFactory error
active: ClientFactory.error
sourceComponent: PlaceholderMessage
}
Loader { // Configuration required
active: plasmoid.configurationRequired
sourceComponent: ConfigureButton
}
}
Grid Component
From FullRepresentation.qml:23-43:
Component {
id: gridComponent
ScrollView {
GridView {
interactive: false
clip: true
readonly property int dynamicColumnNumber:
Math.min(Math.max(width / minItemWidth, 1), count)
readonly property int dynamicCellWidth:
Math.max(width / dynamicColumnNumber, minItemWidth)
readonly property int minItemWidth:
Kirigami.Units.iconSizes.enormous
cellWidth: dynamicCellWidth
cellHeight: minItemWidth / 2
model: itemModel
delegate: Entity {
width: GridView.view.cellWidth - Kirigami.Units.smallSpacing
height: GridView.view.cellHeight - Kirigami.Units.smallSpacing
contentItem: EntityDelegateTile {}
}
}
}
}
Grid Behavior:
- Columns: Dynamically calculated based on available width
- Minimum: 1 column
- Maximum: Number of entities
- Formula:
width / minItemWidth, clamped to entity count
- Cell dimensions:
- Width: Dynamically calculated to fill width evenly
- Height: Half of
minItemWidth
- Minimum width:
Kirigami.Units.iconSizes.enormous (typically 128px)
- Spacing:
Kirigami.Units.smallSpacing between cells
- Loading: Only rendered when
root.initialized === true
Status Indicator
From FullRepresentation.qml:45-53:
StatusIndicator {
icon: "data-error"
size: Kirigami.Units.iconSizes.small
message: ha?.errorString || ''
anchors {
bottom: parent.bottom
right: parent.right
}
}
Display:
- Shows error icon in bottom-right corner
- Only visible when
ha.errorString is not empty
- Red warning indicator with hover tooltip
Error States
The component shows different placeholders for error conditions:
ClientFactory Error (FullRepresentation.qml:55-69):
Loader {
active: ClientFactory.error
sourceComponent: PlasmaExtras.PlaceholderMessage {
text: i18n("Failed to create WebSocket client")
explanation: ClientFactory.errorString().split(/\:\d+\s/)[1]
iconName: "error"
helpfulAction: Action {
icon.name: "link"
text: i18n("Show requirements")
onTriggered: Qt.openUrlExternally(
`${plasmoid.metaData.website}/tree/v${plasmoid.metaData.version}#requirements`
)
}
}
}
Configuration Required (FullRepresentation.qml:71-81):
Loader {
active: plasmoid.configurationRequired
&& (plasmoid.formFactor === PlasmaCore.Types.Vertical
|| plasmoid.formFactor === PlasmaCore.Types.Horizontal)
sourceComponent: PlasmaComponents3.Button {
icon.name: "configure"
text: i18nd("plasmashellprivateplugin", "Configure…")
onClicked: plasmoid.internalAction("configure").trigger()
}
}
EntityDelegateTile
Individual entity tile component displaying icon, name, and value.
Location: package/contents/ui/EntityDelegateTile.qml
Type: GridLayout
Implementation (EntityDelegateTile.qml:1-42):
import QtQuick
import QtQuick.Layouts
import org.kde.plasma.extras as PlasmaExtras
import org.kde.plasma.components as PlasmaComponents3
import org.kde.kirigami as Kirigami
GridLayout {
id: grid
columns: 2
rows: model.value ? 2 : 1
clip: true
columnSpacing: Kirigami.Units.smallSpacing
rowSpacing: 0
DynamicIcon {
name: model.icon
Layout.rowSpan: model.value ? 2 : 1
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
}
PlasmaExtras.Heading {
id: stateValue
level: 4
text: model.value
elide: Text.ElideRight
visible: !!text
wrapMode: Text.NoWrap
font.weight: Font.Bold
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
}
PlasmaComponents3.Label {
id: label
text: name
elide: Text.ElideRight
Layout.alignment: model.value ? Qt.AlignTop : 0
Layout.fillWidth: true
}
}
Layout:
┌─────────────────────────┐
│ [Icon] [Value (bold)] │
│ [Name] │
└─────────────────────────┘
When no value:
┌─────────────────────────┐
│ [Icon] [Name] │
└─────────────────────────┘
Layout Logic:
- Grid: 2 columns, 1-2 rows depending on
model.value
- Icon:
- Spans 1 or 2 rows (2 if value exists)
- Fixed width:
Kirigami.Units.iconSizes.medium (32px)
- Value (top-right):
- Bold heading level 4
- Only visible if
model.value is not empty
- Aligned to bottom of cell
- Elides with ellipsis if too long
- Name (bottom-right or middle-right):
- Regular label text
- Aligned to top if value exists, centered otherwise
- Elides with ellipsis if too long
Model Properties Used:
model.icon: Icon name (from Entity.icon)
model.value: Computed display value (from Entity.value)
name: Entity name (from Entity.name)
Entity Wrapper Component
The Entity wrapper adds interactivity to each tile:
Location: package/contents/ui/Entity.qml
Type: PlasmaComponents3.Button
Implementation (Entity.qml:7-71):
PlasmaComponents3.Button {
id: control
down: model.active
flat: plasmoid.configuration.flat
enabled: !!actions.length
// Tooltip showing available actions
PlasmaComponents3.ToolTip {
visible: control.hovered && text
text: actions.map(c => c.item.tip).join("\n")
}
// Action loaders for click and scroll
readonly property list<Loader> actionLoaders: [
Loader {
active: !!default_action
// Handles click action
},
Loader {
active: model.active && !!scroll_action
// Handles scroll action with progress indicator
}
]
}
The Entity component provides:
- Click actions: Calls
default_action service when clicked
- Scroll actions: Adjusts numeric values via scroll wheel, shows progress indicator
- Tooltips: Displays action descriptions on hover
- Visual feedback: Down state when entity is active, flat style option
- Action validation: Only enabled when actions are configured
Configuration Components
ConfigGeneral
General settings page for connection configuration.
Location: package/contents/ui/ConfigGeneral.qml
Type: KCM.SimpleKCM
Structure:
KCM.SimpleKCM {
property string cfg_url
property alias cfg_flat: flat.checked
Kirigami.FormLayout {
Secrets { id: secrets } // KDE Wallet integration
ComboBox { // URL selection
id: url
editable: true
Kirigami.FormData.label: i18n("Home Assistant URL")
}
TextField { // Token input
id: token
text: secrets.token
Kirigami.FormData.label: i18n("Token")
}
CheckBox { // Display option
id: flat
Kirigami.FormData.label: i18n("Flat entities")
}
}
function saveConfig() {
secrets.set(url.editText, token.text)
}
}
Fields:
Home Assistant server URLValidation:
- Removes trailing slashes and whitespace
- Example:
http://homeassistant.local:8123
Whether to use flat entity display style
URL ComboBox Behavior (ConfigGeneral.qml:38-55):
- Editable: User can type or select from dropdown
- Model: Populated with previously saved URLs from KDE Wallet
- Validation: Removes whitespace and trailing slashes on blur
- Events:
onActiveFocusChanged: Validates when focus lost
onHoveredChanged: Validates when mouse leaves
onAccepted: Validates on Enter key
onActivated: Loads token for selected URL
Token TextField (ConfigGeneral.qml:61-70):
- Stored securely in KDE Wallet via Secrets component
- Retrieved automatically when URL is selected
- Saved when configuration is applied
Secrets Integration (ConfigGeneral.qml:15-31):
Secrets {
id: secrets
property string token
onReady: {
restore(cfg_url) // Load token for current URL
list().then(urls => (url.model = urls)) // Populate dropdown
}
function restore(entryKey) {
if (!entryKey) {
return this.token = ""
}
get(entryKey)
.then(t => this.token = t)
.catch(() => this.token = "")
}
}
Help Text:
- URL format examples provided
- Link to token generation page (dynamic based on URL)
- Clickable link:
{url}/profile/security
ConfigItems
Entity list management and configuration page.
Location: package/contents/ui/ConfigItems.qml
Type: KCM.ScrollViewKCM
Structure:
KCM.ScrollViewKCM {
property string cfg_items // JSON string
property ListModel items // Runtime model
property var services // From Home Assistant
property var entities // From Home Assistant
property Client ha // WebSocket client
header: Kirigami.InlineMessage { // Error display
text: ha?.errorString
}
view: ListView { // Entity list
model: items
delegate: EntityListItem {}
}
footer: ColumnLayout { // Action buttons
Button { text: i18n("Add") }
Button { icon.name: 'application-menu' } // Backup menu
}
}
Entity List
From ConfigItems.qml:94-163:
view: ListView {
id: itemList
model: Object.keys(entities).length ? items : [] // Wait for entity data
delegate: EntityListItem {}
moveDisplaced: Transition {
NumberAnimation { properties: "y"; duration: Kirigami.Units.longDuration }
}
BusyIndicator {
visible: busy
}
}
EntityListItem Delegate (ConfigItems.qml:128-163):
component EntityListItem: Item {
width: ListView.view.width
implicitHeight: listItem.height
ItemDelegate {
id: listItem
readonly property var entity: entities[model.entity_id]
contentItem: RowLayout {
Kirigami.ListItemDragHandle { // Reorder handle
onMoveRequested: (oldIndex, newIndex) =>
items.move(oldIndex, newIndex, 1)
onDropped: save()
}
DynamicIcon { // Entity icon
name: model.icon || listItem.entity?.attributes.icon || ''
}
Kirigami.TitleSubtitle { // Name and ID
title: model.name || listItem.entity?.attributes.friendly_name
subtitle: model.entity_id
}
ToolButton { // Edit button
icon.name: 'edit-entry'
onClicked: openDialog(new Model.ConfigEntity(model), index)
}
ToolButton { // Delete button
icon.name: 'delete'
onClicked: removeItem(index)
}
}
}
}
Features:
- Drag handles for reordering entities
- Animated transitions when items move
- Edit and delete buttons per item
- Shows entity icon, name, and ID
- Falls back to Home Assistant data if custom fields empty
From ConfigItems.qml:28-92:
Add Button:
Button {
icon.name: 'list-add'
text: i18n("Add")
onClicked: openDialog(new Model.ConfigEntity())
}
Backup Menu:
Button {
icon.name: 'application-menu'
down: backupMenu.visible
onClicked: backupMenu.visible = !backupMenu.visible
}
ColumnLayout {
id: backupMenu
visible: false
Button { // Import
icon.name: 'document-import'
text: i18n("Import")
onClicked: file.open().then(data => setItems(data))
}
Button { // Export
icon.name: 'document-export'
text: i18n("Export")
onClicked: file.save(cfg_items)
}
Kirigami.ActionTextField { // Auto-backup file
id: autoBackupFileField
readOnly: true
onPressed: file.select().then(file => cfg_autoBackupFile = file)
}
}
File Operations:
- Format:
.hapi files (Home Assistant Plasma Items)
- Content: JSON array of ConfigEntity objects
- Import: Replaces all current items
- Export: Saves current configuration
- Auto-backup: Automatically saves on every change if path is set
Data Loading
From ConfigItems.qml:108-126:
Component.onCompleted: {
setItems(cfg_items) // Load saved configuration
ha = ClientFactory.getClient(this, plasmoid.configuration.url)
ha.readyChanged.connect(fetchData) // Fetch when connected
}
function fetchData() {
if (!ha?.ready) return
return Promise.all([ha.getStates(), ha.getServices()])
.then(([e, s]) => {
entities = arrayToObject(e, 'entity_id') // Convert to object
services = s
busy = false
}).catch(() => busy = false)
}
Loading sequence:
- Parse
cfg_items JSON to populate ListModel
- Create Client instance via ClientFactory
- Wait for client to become ready
- Fetch all entity states and services
- Convert arrays to objects for fast lookup
- Hide busy indicator
ConfigItem
Individual entity configuration form.
Location: package/contents/ui/ConfigItem.qml
Type: Kirigami.FormLayout
Structure:
Kirigami.FormLayout {
property var item // ConfigEntity instance
readonly property var source: entities[item.entity_id] || {}
readonly property var itemServices: services[item.domain] || {}
TextField { /* Entity ID */ }
Row { /* Display attribute */ }
TextField { /* Name */ }
Row { /* Icon */ }
CheckBox { /* Notify */ }
ServiceSelector { /* Click action */ }
ServiceSelector { /* Scroll action */ }
}
Fields
Entity ID (ConfigItem.qml:13-23):
TextField {
Kirigami.FormData.label: i18n("Entity")
text: item.entity_id
onEditingFinished: {
item.entity_id = text // Triggers ConfigEntity reactivity
itemChanged()
}
Autocompletion {
model: Object.keys(entities).sort()
}
}
- Autocomplete from all available entities
- Updates ConfigEntity (triggers domain/action updates)
Display Attribute (ConfigItem.qml:25-41):
Row {
CheckBox {
id: useAttribute
checked: !!item.attribute
}
ComboBox {
displayText: currentText || item.attribute
model: source.attributes ? Object.keys(source.attributes) : []
onActivated: index => item.attribute = model[index]
enabled: useAttribute.checked
}
}
- Checkbox to enable attribute display mode
- Dropdown populated from entity’s actual attributes
- Disabled when checkbox unchecked (clears attribute)
Name (ConfigItem.qml:43-48):
TextField {
text: item.name || ''
placeholderText: source.attributes?.friendly_name || ''
onTextChanged: item.name = text
Kirigami.FormData.label: i18n("Name")
}
- Shows Home Assistant name as placeholder
- Empty = use default name
Icon (ConfigItem.qml:50-62):
Row {
TextField {
id: iconName
text: item.icon || ''
placeholderText: source.attributes?.icon || 'mdi: | plasma:'
onTextChanged: item.icon = text
}
DynamicIcon { // Live preview
name: iconName.text || iconName.placeholderText
}
}
- Shows Home Assistant icon as placeholder
- Live preview of icon next to field
- Supports MDI (
mdi:) and Plasma icons
Notify (ConfigItem.qml:64-68):
CheckBox {
Kirigami.FormData.label: i18n("Notify about changes")
checked: !!item.notify
onCheckedChanged: item.notify = checked
}
Service Selectors
ServiceSelector Component (ConfigItem.qml:70-96):
component ServiceSelector: Row {
visible: !!serviceSelector.count
property alias currentValue: serviceSelector.currentValue
property var initialValue
property var serviceFilter
default property alias data: nested.data
CheckBox {
id: useAction
}
ComboBox {
id: serviceSelector
model: serviceFilter
? Object.keys(itemServices).filter(serviceFilter)
: Object.keys(itemServices)
enabled: useAction.checked
onEnabledChanged: if (!enabled) currentIndex = -1
onCurrentIndexChanged: useAction.checked = ~currentIndex
}
Column {
id: nested
enabled: serviceSelector.enabled
}
}
Default Action (ConfigItem.qml:98-102):
ServiceSelector {
Kirigami.FormData.label: i18n("Click action")
initialValue: item.default_action?.service
onCurrentValueChanged: s => item.default_action = { service: currentValue }
}
- Shows all services for entity’s domain
- Sets
default_action with selected service
- ConfigEntity automatically fills domain and target
Scroll Action (ConfigItem.qml:104-119):
ServiceSelector {
id: scrollActionSelector
Kirigami.FormData.label: i18n("Scroll action")
serviceFilter: k => getNumberFields(itemServices[k]).length
initialValue: item.scroll_action?.service
onCurrentValueChanged:
scrollFieldSelector.model = getNumberFields(itemServices[currentValue])
ComboBox { // Nested field selector
id: scrollFieldSelector
model: scrollActionSelector.currentValue
onCurrentValueChanged: item.scroll_action = {
service: scrollActionSelector.currentValue,
data_field: currentValue
}
}
}
- Filtered to only show services with number fields
- Shows nested field selector for choosing which attribute to control
- Only shows fields that exist in entity’s current attributes
getNumberFields Function (ConfigItem.qml:121-128):
function getNumberFields({ fields = {} } = {}) {
return Object.keys(fields).reduce((f, id) => {
const field = fields[id]
if (field.fields) f.push(...getNumberFields(field)) // Recurse
if (field.selector?.number && id in source.attributes) f.push(id)
return f
}, [])
}
- Recursively searches service field definitions
- Only includes number fields that exist in entity’s attributes
- Example: brightness for lights, temperature for climate
Dialog Usage
From ConfigItems.qml:165-200:
Component {
id: dialogComponent
Kirigami.Dialog {
property int index
property var item
readonly property bool acceptable: !!itemForm.item?.entity_id
ConfigItem {
id: itemForm
item: dialog.item
}
onAccepted: itemAccepted(index, item)
}
}
function openDialog(data, index = -1) {
const dialog = dialogComponent.createObject(parent, {
index: index,
item: data,
title: data.name || data.entity_id || i18n('New')
})
dialog.open()
dialog.onItemAccepted.connect((index, item) => {
if (~index) {
return updateItem(item, index) // Edit existing
}
return addItem(item) // Add new
})
}
Dialog behavior:
- OK button enabled only when entity_id is set
- Title shows entity name or “New”
- Index -1 = add new item
- Index >= 0 = edit existing item
Helper Components
These components are used throughout the UI:
DynamicIcon
Purpose: Renders either MDI icons or Plasma icons
Usage:
DynamicIcon {
name: "mdi:lightbulb" // or "plasma-icon-name"
}
StatusIndicator
Purpose: Shows error overlay in bottom-right corner
Usage:
StatusIndicator {
icon: "data-error"
message: errorString
}
Autocompletion
Purpose: Adds autocomplete dropdown to TextField
Usage:
TextField {
Autocompletion {
model: ["option1", "option2", "option3"]
}
}
File
Purpose: File dialog wrapper for import/export
Usage:
File {
id: file
defaultSuffix: "hapi"
nameFilters: ["Home Assistant Plasma Items (*.hapi)"]
}
Button {
onClicked: file.save(jsonData)
}
Responsive Behavior
Grid Layout
The entity grid automatically adapts to available space:
- Wide screens: Multiple columns (up to entity count)
- Narrow screens: Single column
- Minimum width:
Kirigami.Units.iconSizes.enormous
- Cell height: Always half of minimum width (2:1 aspect ratio)
Example calculations:
Width: 600px, minItemWidth: 128px, entities: 10
→ dynamicColumnNumber = min(max(600/128, 1), 10) = min(4, 10) = 4 columns
→ dynamicCellWidth = max(600/4, 128) = 150px
→ cellHeight = 128/2 = 64px
Width: 100px, minItemWidth: 128px, entities: 5
→ dynamicColumnNumber = min(max(100/128, 1), 5) = min(1, 5) = 1 column
→ dynamicCellWidth = max(100/1, 128) = 128px
→ cellHeight = 128/2 = 64px
The plasmoid adapts to different contexts:
- System tray: Shows compact representation only
- Panel: Shows compact representation, expands to popup
- Desktop widget: Shows full representation directly
- Configuration required: Shows configure button in panel/tray only
Styling and Theming
All components use Kirigami and Plasma Components:
- Colors: Automatic from Plasma theme
- Spacing:
Kirigami.Units.smallSpacing, largeSpacing, etc.
- Icons:
Kirigami.Units.iconSizes.small/medium/large/enormous
- Typography:
PlasmaExtras.Heading levels, PlasmaComponents3.Label
- Animations:
Kirigami.Units.longDuration for transitions
Accessibility
- Keyboard navigation: All interactive elements are keyboard accessible
- Screen readers: Proper labels via
Kirigami.FormData.label
- Focus indicators: Automatic from Plasma theme
- Tooltips: Error messages in StatusIndicator