Skip to main content
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:
cfg_url
string
Home Assistant server URLValidation:
  • Removes trailing slashes and whitespace
  • Example: http://homeassistant.local:8123
cfg_flat
boolean
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

Action Buttons

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:
  1. Parse cfg_items JSON to populate ListModel
  2. Create Client instance via ClientFactory
  3. Wait for client to become ready
  4. Fetch all entity states and services
  5. Convert arrays to objects for fast lookup
  6. 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

Form Factor Adaptation

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