Skip to main content

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.

The Client component provides a WebSocket-based API for communicating with Home Assistant. It handles authentication, connection management, entity subscriptions, and service calls.

Overview

The Client is a QML component that wraps QtWebSockets.WebSocket and implements the Home Assistant WebSocket API protocol. Location: package/contents/ui/Client.qml Type: BaseObject (extends QtObject)

Properties

baseUrl
string
required
The base URL of the Home Assistant server (e.g., http://homeassistant.local:8123)Automatically converted to WebSocket URL by replacing http with ws and appending /api/websocket
token
string
required
Home Assistant long-lived access token for authenticationTypically retrieved from KDE Wallet via ClientFactory
ready
bool
Indicates whether the client is connected and authenticatedBecomes true after successful authentication, false on disconnect or before auth
configured
bool
Indicates whether the client has both URL and token configuredAutomatically activates WebSocket connection when true
errorString
string
Contains the last error message, or empty string if no errorAutomatically cleared when connection becomes ready

Public Methods

subscribeEntities

Subscribes to state updates for specific entities.
function subscribeEntities(entity_ids, callback)
entity_ids
array
required
Array of entity IDs to subscribe to (e.g., ["light.living_room", "sensor.temperature"])
callback
function
required
Function called when entity states changeReceives a single event parameter containing:
  • a: Initial entity states (object mapping entity_id to state data)
  • c: Changed entity states (object with entity_id keys and change data)
return
function
Returns an unsubscribe function that cancels the subscription when called
Example:
const unsubscribe = client.subscribeEntities(
    ["light.bedroom", "sensor.temperature"],
    (event) => {
        if (event.a) {
            // Initial state: {"light.bedroom": {s: "on", a: {...}}, ...}
            console.log("Initial states:", JSON.stringify(event.a))
        }
        if (event.c) {
            // State change: {"light.bedroom": {"+": {s: "off"}}}
            console.log("State changes:", JSON.stringify(event.c))
        }
    }
)

// Later: cancel subscription
unsubscribe()

callService

Calls a Home Assistant service with optional data.
function callService({domain, service, target}, data)
domain
string
required
Service domain (e.g., "light", "switch", "climate")
service
string
required
Service name (e.g., "turn_on", "turn_off", "set_temperature")
target
object
Service target specificationCommon formats:
  • {entity_id: "light.bedroom"} - Single entity
  • {entity_id: ["light.bedroom", "light.kitchen"]} - Multiple entities
  • {area_id: "living_room"} - All entities in area
data
object
Additional service data (parameters)Examples:
  • {brightness: 255} for light.turn_on
  • {temperature: 22} for climate.set_temperature
return
Promise
Promise that resolves when the service call succeeds, or rejects with error object
Example:
// Turn on a light with brightness
client.callService(
    {
        domain: "light",
        service: "turn_on",
        target: {entity_id: "light.bedroom"}
    },
    {brightness: 200}
).then(() => {
    console.log("Light turned on")
}).catch(error => {
    console.error("Failed:", error.message)
})

// Toggle a switch
client.callService({
    domain: "switch",
    service: "toggle",
    target: {entity_id: "switch.fan"}
})

getStates

Retrieves current states for all or specific entities.
function getStates(entities)
entities
array
Optional array of entity IDs to filterIf omitted, returns all entity states
return
Promise<Array>
Promise that resolves to array of entity state objectsEach object contains:
  • entity_id: Entity identifier
  • state: Current state value
  • attributes: Object with entity attributes
  • last_changed: ISO timestamp
  • last_updated: ISO timestamp
Example:
// Get all states
client.getStates().then(states => {
    console.log(`Total entities: ${states.length}`)
})

// Get specific entities
client.getStates(["light.bedroom", "sensor.temperature"])
    .then(states => {
        states.forEach(s => {
            console.log(`${s.entity_id}: ${s.state}`)
        })
    })

getServices

Retrieves all available services and their metadata.
function getServices()
return
Promise<Object>
Promise that resolves to object mapping domain → service → metadataStructure:
{
  "light": {
    "turn_on": {
      "name": "Turn on",
      "description": "...",
      "fields": {
        "brightness": {
          "selector": {"number": {"min": 0, "max": 255}}
        }
      }
    }
  }
}
Example:
client.getServices().then(services => {
    // Get all light services
    const lightServices = Object.keys(services.light)
    console.log("Light services:", lightServices)
    
    // Check if service has brightness field
    const hasBrightness = "brightness" in services.light.turn_on.fields
})

Connection Lifecycle

Authentication Flow

  1. WebSocket connects to baseUrl/api/websocket
  2. Server sends {type: "auth_required"}
  3. Client sends {type: "auth", access_token: token}
  4. Server responds with {type: "auth_ok"} or {type: "auth_invalid"}
  5. ready property becomes true on success
  6. error signal emitted on auth failure
Implementation (Client.qml:57-66):
onTextMessageReceived: message => {
    const msg = JSON.parse(message)
    switch (msg.type) {
        case 'auth_required': auth(token); break;
        case 'auth_ok': ready = true; break;
        case 'auth_invalid': error(msg.message); break;
        case 'event': notifyStateUpdate(msg); break;
        case 'result': handleResult(msg); break;
    }
}

Keepalive Mechanism

The client uses a ping/pong system to maintain the connection:
  • Interval: 30 seconds (30000ms)
  • Behavior:
    • Sends ping command
    • Waits for any message response
    • If no response after 30s, reconnects
  • Reset: Timer resets on any message received
Implementation (Client.qml:26-44):
Timer {
    id: pingPongTimer
    interval: 30000
    running: ws.status
    repeat: true
    property bool waiting
    onTriggered: {
        if (waiting || !ws.open) {
            ws.reconnect()  // Reconnect if timeout
        } else {
            ws.ping()  // Send ping
        }   
        waiting = !waiting         
    }
    function reset() {
        waiting = false
        restart()
    }
}

Reconnection

The client automatically reconnects when:
  • Ping/pong timeout occurs (no response in 30s)
  • WebSocket connection is lost
  • reconnect() is called manually
Reconnection process:
function reconnect() {
    active = false  // Close connection
    active = true   // Reopen connection
}

Error Handling

Error Signal

The client emits an error signal when issues occur:
signal error(string msg)
Error sources:
  • Authentication failures (auth_invalid message)
  • WebSocket connection errors (errorString changed)
  • Service call failures (promise rejection)
Handling errors:
Connections {
    target: client
    function onError(msg) {
        console.error("Client error:", msg)
        // Update UI, show notification, etc.
    }
}

Error String Property

The errorString property contains the last error message:
  • Set when error signal is emitted
  • Automatically cleared when connection becomes ready
  • Displayed in UI via StatusIndicator
Implementation (Client.qml:20-24):
Connections {
    target: ws
    function onError(msg) { errorString = msg }
    function onReadyChanged() { ws.ready && (errorString = "") }
}

Internal Implementation Details

Message Counter

Each command is assigned a unique ID from an incrementing counter:
property int messageCounter: 0

function command(message) {
    return send(Object.assign({}, {id: messageCounter}, message))
}

function send(message) {
    sendTextMessage(JSON.stringify(message))
    return messageCounter++
}

Promise Management

Async commands store promise resolvers in a Map:
property var promises: new Map()

function commandAsync(message) {
    const id = command(message)
    return new Promise((resolve, reject) => 
        promises.set(id, {resolve, reject})
    )
}

function handleResult(msg) {
    const p = promises.get(msg.id)
    if (!p) return
    if (msg.success) {
        p.resolve(msg.result)
    } else {
        p.reject(msg.error)
    }
    promises.delete(msg.id)
}

Subscription Management

Subscriptions are tracked in a Map with callbacks:
property var subscriptions: new Map()

function subscribeEntities(entity_ids, callback) {
    if (!entity_ids) return
    const subscription = command({
        "type": "subscribe_entities",
        entity_ids
    })
    subscriptions.set(subscription, callback)
    return () => unsubscribe(subscription)
}

function notifyStateUpdate(msg) {
    const callback = subscriptions.get(msg.id)
    return callback && callback(msg.event)
}

Usage with ClientFactory

The recommended way to use the Client is via ClientFactory:
import "."

QtObject {
    property QtObject ha
    
    Component.onCompleted: {
        ha = ClientFactory.getClient(
            this,
            "http://homeassistant.local:8123"
        )
        ha.readyChanged.connect(onReady)
    }
    
    function onReady() {
        if (!ha.ready) return
        
        // Client is ready, start using API
        ha.getStates().then(states => {
            console.log("Loaded states:", states.length)
        })
    }
}
See Component Architecture for ClientFactory details.

Best Practices

  1. Always check ready: Wait for ready === true before making API calls
  2. Use ClientFactory: Don’t instantiate Client directly
  3. Handle errors: Connect to error signal or use promise .catch()
  4. Clean up subscriptions: Call returned unsubscribe function when done
  5. Monitor errorString: Display to users for connection issues
  6. Let keepalive work: Don’t manually manage ping/pong unless necessary

Build docs developers (and LLMs) love