Documentation Index
Fetch the complete documentation index at: https://mintlify.com/korapp/plasma-homeassistant/llms.txt
Use this file to discover all available pages before exploring further.
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