KaZa Home automation

Home automation

Integrator Guide

Integrator Guide

An integrator is someone who writes the home-specific application that sits on top of the KaZa platform. This is not the same as developing a plugin (which requires C++ and deals with a specific protocol). An integrator writes QML code — one part running on the server, one part compiled into the client — to describe their specific home: which devices exist, how they should respond to events, and how they should be presented to the user.

The reference example is KaZaExample: a production installation covering KNX lights and shutters, a ventilation system, a solar/battery installation, an EV charger, a pool controller, and power monitoring for over 20 individual circuits. Everything in this guide is illustrated with patterns drawn from that example.


Overview: What You Will Create

An integrator produces two artifacts:

  1. Server-side QML — a main.qml (and optional companion files) loaded by kazad at startup. This code runs continuously on the server. It declares plugins, wires up automation rules, schedules actions, and logs data.

  2. Client-side QML — a set of QML files compiled into a .rcc resource bundle. This bundle is served to connecting clients over the SSL port and provides the complete user interface.

The server and client share a common namespace: the object names you define on the server are the same strings the client uses in KzObject bindings.

Server machine                          Client devices
─────────────────────────────────       ──────────────────────────
/etc/kazad.conf
  qml.server = /opt/kaza/main.qml
  qml.client = /opt/kaza/app.rcc

main.qml ──► kazad (QML engine)
                
                
         KaZaObject registry
         (shared object names)
                
         SSL port 1756 ─────────────►  KaZa Client (native app)
         sends app.rcc on connect       Loads UI from app.rcc
                                        KzObject binds by name

Server-Side QML

Project Structure

There is no prescribed structure. A small installation fits in a single main.qml. A complex one should be split into thematic files:

my-home/
├── main.qml          # Root — imports all sub-components
├── Knx.qml           # KNX bus and power logging
├── Heating.qml       # Heating system logic
├── Energy.qml        # Solar, battery, grid
├── DataLogger.qml    # Periodic database inserts
└── Program.qml       # Schedulers and automation rules

Root Element: KaZaElement

Every server-side QML file uses KaZaElement as its root type, provided by org.kazoe.kaza 1.0. Companion files are instantiated as children of the root:

// main.qml
import QtQml 2.0
import org.kazoe.kaza 1.0

KaZaElement {
    id: root

    Component.onCompleted: {
        console.debug("Server started")
    }

    Knx      { id: knx }
    Heating  { id: heating }
    Energy   { id: energy }
    DataLogger { id: logger }
}
// Knx.qml
import QtQml 2.0
import org.kazoe.kaza 1.0
import org.kazoe.knx 1.0

KaZaElement {
    KnxBus {
        knxd: "ip:192.168.1.250"
        knxProj: "/etc/kaza/home.knxproj"
    }
    // KNX-specific objects and rules follow
}

Referencing Objects: KzObject

KzObject binds to a named object by string and mirrors its current value. It fires onValueChanged whenever the value changes on the server.

KzObject {
    id: loungeLightSwitch
    object: "light.switch.lounge"

    onValueChanged: {
        console.debug("Lounge light is now:", value)
    }
}

Writing a value from the server:

loungeLightSwitch.set(true)           // write without confirmation
loungeLightSwitch.set(true, true)     // write and request confirmation from hardware
loungeLightSwitch.refresh()           // ask the plugin to re-read hardware value

KzObject key properties:

Property/Method Type Description
object string Object name in the registry
value QVariant Current value (read-only)
unit string Unit string (read-only)
set(v, confirm) method Write a value
refresh() method Force hardware re-read
rawid() method Returns numeric object ID (useful as DB foreign key)

Creating Custom Objects: KaZaObject

For values that are not produced by a plugin — computed values, state flags, external API data — declare a KaZaObject directly in QML:

KaZaObject {
    id: tempoTomorrow
    name: "tempo.tomorrow"
}

Once declared, the object is in the registry and is synchronized to all clients. Assign values directly:

tempoTomorrow.value = jsonResponse.codeJour

From KaZaExample main.qml, this pattern exposes the next-day French electricity tariff color fetched from an HTTP API:

import QtQml 2.0
import org.kazoe.kaza 1.0

KaZaElement {
    id: root

    KaZaObject {
        id: tempoTomorrow
        name: "tempo.tomorrow"
    }

    function getTomorrowTempoData() {
        var xhr = new XMLHttpRequest()
        xhr.open("GET", "https://www.api-couleur-tempo.fr/api/jourTempo/tomorrow")
        xhr.onreadystatechange = function() {
            if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                var json = JSON.parse(xhr.responseText)
                tempoTomorrow.value = json.codeJour
            }
        }
        xhr.send()
    }

    Timer {
        interval: 15 * 60 * 1000
        running: true
        repeat: true
        onTriggered: root.getTomorrowTempoData()
    }

    Component.onCompleted: root.getTomorrowTempoData()

    // sub-components
    Knx      { id: knx }
    Tesla    { id: tesla }
    Helios   { id: helios }
    Program  { id: prog }
    DataLogger { id: datalogger }
}

Object Naming Convention

Use a hierarchical dot-separated scheme. Consistent naming makes the namespace readable and supports fuzzy filtering with the kls tool.

<domain>.<category>.<location_or_id>

Examples from KaZaExample:
  light.switch.lounge
  light.switch.kitchen
  blind.goto.lounge
  blind.goto.bedroom2
  meas.power.plugs_floor
  meas.power.dishwasher
  meas.power.pac
  meas.temperature.porch
  device.boiler.tank_top
  device.boiler.tank_bottom
  configuration.mode_manuel.lounge
  helios.inInsideTemperature
  victron.battery.soc
  internal.presence.fabien
  tempo.tomorrow
  tank.rainwater
  system.home.edf_tarif_current

KNX Integration

Import org.kazoe.knx 1.0 and declare a KnxBus. The plugin reads the .knxproj file (ETS export) and creates one KaZaObject per group address.

import org.kazoe.knx 1.0

KaZaElement {
    KnxBus {
        knxd: "ip:192.168.1.250"
        knxProj: "/etc/kaza/home.knxproj"
    }
}

All group addresses from the ETS project are then available by name. From KaZaExample Knx.qml, power meters are logged to the database on every value change:

KzObject {
    object: "meas.power.plugs_floor"
    onValueChanged: {
        kazamanager.runDbQuery(
            "INSERT INTO power (gad, power) VALUES ("
            + rawid() + "," + (value / 1000) + ")"
        )
    }
}

KzObject {
    object: "meas.power.dishwasher"
    onValueChanged: {
        kazamanager.runDbQuery(
            "INSERT INTO power (gad, power) VALUES ("
            + rawid() + "," + value + ")"
        )
    }
}

A scheduler reactivates detectors and purges old data every morning:

Scheduler {
    hour: "9"
    minute: "0"
    onTimeout: {
        detecteurLounge.set(1)
        detecteurKitchen.set(1)
        kazamanager.runDbQuery(
            "DELETE FROM power WHERE time < NOW() - INTERVAL '10 days'"
        )
    }
}

MQTT Integration

Import org.kazoe.mqtt 1.0:

import org.kazoe.mqtt 1.0

MqttClient {
    hostname: "localhost"
    port: 1883
    clientId: "kaza-server"

    MqttTopic {
        topic: "home/sensors/outdoor/temperature"
        onMessageReceived: function(message) {
            outdoorTemp.value = parseFloat(message)
        }
    }
}

KaZaObject {
    id: outdoorTemp
    name: "sensor.outdoor.temperature"
}

Time-Based Automation: Scheduler

Scheduler fires onTimeout when the current time matches all configured fields. Unset fields act as wildcards.

Property Example Meaning
year "2025" Match specific year
month "6,7,8" Summer months
day "1" First of month
wday "1,2,3,4,5" Weekdays (1=Monday)
hour "7" 7 AM
minute "*/15" Every 15 minutes
// Weekday morning routine
Scheduler {
    wday: "1,2,3,4,5"
    hour: "7"
    minute: "0"
    onTimeout: {
        blindBedroom.set(100)
        lightKitchen.set(true)
    }
}

// Night mode
Scheduler {
    hour: "23"
    minute: "0"
    onTimeout: {
        lightLounge.set(false)
        lightKitchen.set(false)
    }
}

// Every 5 minutes, log temperature
Scheduler {
    minute: "*/5"
    onTimeout: {
        kazamanager.runDbQuery(
            "INSERT INTO temperature (sensor, celsius) VALUES ('porch',"
            + kMeasTempOutside.value + ")"
        )
    }
}

// Monthly cleanup
Scheduler {
    day: "1"
    hour: "3"
    minute: "0"
    onTimeout: {
        kazamanager.runDbQuery(
            "DELETE FROM power WHERE time < NOW() - INTERVAL '1 year'"
        )
    }
}

Database Logging

If /etc/kazad.conf configures a PostgreSQL connection, kazamanager.runDbQuery(sql) runs SQL asynchronously:

-- Create once in psql
CREATE TABLE power (
    time   TIMESTAMP DEFAULT NOW(),
    gad    INTEGER,
    power  FLOAT
);
CREATE INDEX ON power(time);
CREATE INDEX ON power(gad);
KzObject {
    object: "meas.power.boiler_electric"
    onValueChanged: {
        kazamanager.runDbQuery(
            "INSERT INTO power (gad, power) VALUES ("
            + rawid() + "," + value + ")"
        )
    }
}

Notifications

KaZa provides two complementary notification mechanisms: immediate notifications sent to currently connected clients, and persistent alarms that the Android client can retrieve even when the app is in the background.

Immediate notifications: kazamanager.notify()

kazamanager.notify(message) is a QML slot that pushes a text message to all connected clients over the SSL channel as a FRAME_SYSTEM frame. On Android the client converts it into a native system notification. On Linux and Windows it is currently delivered as a text command that the client QML can display however it chooses.

// Send to every connected client
kazamanager.notify("Front door opened")

// Triggered by a sensor
KzObject {
    object: "knx.sensor.entrance.door"
    onValueChanged: {
        if (value === true) {
            kazamanager.notify("Front door opened")
        }
    }
}

// Triggered by a scheduler — daily reminder
Scheduler {
    hour: "8"
    minute: "0"
    onTimeout: {
        var msg = "Good morning! Outdoor: "
                  + kMeasTempOutside.value.toFixed(1) + " °C"
        kazamanager.notify(msg)
    }
}
Targeting specific users

Prefix the message with one or more /username tokens to deliver the notification only to clients authenticated as those users. The username is the one used during certificate provisioning.

// Send only to the user "fabien"
kazamanager.notify("/fabien Washing machine finished")

// Send to two users, leave others out
kazamanager.notify("/fabien /aurelie Pool pump alarm!")

// No prefix → broadcast to everyone connected
kazamanager.notify("Power outage detected")

The server extracts every token that starts with / from the beginning of the string, strips them, and delivers the remaining text only to connections whose authenticated username matches.

KzAlarm — Persistent background alarms

kazamanager.notify() only reaches clients that are currently connected. For notifications that must be delivered even when the app is in the background or the phone screen is off, use KzAlarm.

KzAlarm is a QML type that registers a named alarm condition with the server. The Android client runs a background service (NotificationService) that periodically opens a short SSL connection and requests the current alarm list via the ALARMS: command. When an alarm is active (enable: true) the service triggers a native Android notification, regardless of whether the main app is running.

import org.kazoe.kaza 1.0

KaZaElement {

    // Declare a persistent alarm
    KzAlarm {
        id: poolAlarm
        title: "Pool alert"
        message: "Pool pump has not run for 24 hours"
        enable: false   // set to true to activate
    }

    // Activate/deactivate based on a sensor
    KzObject {
        id: pumpStatus
        object: "device.pool.pump_running"
        onValueChanged: {
            // If pump hasn't run — flag the alarm
            poolAlarm.enable = !value
        }
    }
}

KzAlarm properties:

Property Type Description
enable bool true = alarm is active and will be delivered to polling clients
title string Notification title shown on the device
message string Notification body text
admin bool If true, alarm is only visible to admin users
debug bool If true, alarm is suppressed in production builds

Multiple KzAlarm instances can coexist. Each one is independent. The client background service receives all active alarms for the authenticated user in a single compressed response.

Combining both mechanisms

For time-critical events (gas leak, alarm triggered, water sensor) you typically want both: an immediate notification to clients that are open, and a persistent alarm that will wake up the phone if the app is not running.

KzAlarm {
    id: waterAlarm
    title: "Water leak!"
    message: "Water detected in the basement"
    enable: false
}

KzObject {
    object: "knx.sensor.basement.water"
    onValueChanged: {
        if (value === true) {
            // Immediate push to anyone connected right now
            kazamanager.notify("URGENT: Water leak in basement!")

            // Persistent alarm — wakes Android clients in background
            waterAlarm.enable = true
        } else {
            waterAlarm.enable = false
            kazamanager.notify("Water leak resolved")
        }
    }
}
Control port

Notifications can also be sent from the control port (43500) for testing or external scripting:

nc localhost 43500
notify Front door opened
notify /fabien Your washing machine finished

Client-Side QML

How the Client Shell Works

The KaZa Client is a shell that provides: - The SSL connection to the server - A manager singleton with a ready property - Automatic download of the .rcc bundle from the server - The KzObject type for binding to server objects

Your UI code lives entirely in the .rcc bundle loaded from qrc:/main.qml.

manager member Description
manager.ready true once connected and all objects received
manager.readyChanged Signal fired on connection state change

Typical main.qml Structure

import QtQuick 2.9
import QtQuick.Controls 2.15

Page {
    id: window
    width: 360
    height: 668
    title: "KaZa"

    function askClose(close) {
        if (stackView.depth > 1) {
            close.accepted = false
            stackView.pop()
        } else {
            close.accepted = true
        }
    }

    Component.onCompleted: {
        mainwindow.callBackClose = askClose
    }

    // Toolbar visible when connected
    header: Loader {
        active: manager.ready
        source: "KazaToolbar.qml"
    }

    // Sidebar drawer when connected
    Loader {
        active: manager.ready
        source: "KazaSidebar.qml"
    }

    // Waiting screen while connecting
    Loader {
        active: !manager.ready
        source: "WaitConnection.qml"
    }

    // Page stack — home page when connected
    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: Loader {
            active: manager.ready
            source: "HomePage.qml"
        }
    }
}

Using KzObject in Client QML

import org.kazoe.kaza 1.0

KzObject {
    id: insideTemp
    object: "helios.inInsideTemperature"
}

KzObject {
    id: outsideTemp
    object: "meas.temperature.porch"
}

Button {
    text: outsideTemp.value !== undefined
          ? "Outside: " + outsideTemp.value.toFixed(2) + " °C"
          : "Outside: —"
    onClicked: stackView.push("HistoryMinAvgMax.qml", {
        mainTitle: "Outside temperature",
        object: "meas.temperature.porch",
        min: 0,
        max: 35
    })
}

Writing from the client:

// Simple write
kLightSwitch.set(true)

// Write with confirmation (server echoes back after actuating)
kModeLounge.set(0, true)

// Example: TV mode button from KaZaExample HomePage.qml
Button {
    property bool modeTV: (!kModeLounge.value && !kModeKitchen.value)
    text: "Séance TV"
    highlighted: modeTV

    onClicked: {
        if (modeTV) {
            kModeLounge.set(1, true)
            kModeKitchen.set(1, true)
        } else {
            kBlindLounge.set(100)
            kModeLounge.set(0, true)
            kModeKitchen.set(0, true)
            kLightSwitchLounge.set(0)
            kLightSwitchDiner.set(0)
            kLightSwitchKitchen.set(0)
        }
    }
}

Navigation

Push and pop pages on the stack:

Button { text: "Ground floor"; onClicked: stackView.push("PlanRDC.qml") }
Button { text: "Upper floor";  onClicked: stackView.push("PlanEtage.qml") }
Button { text: "Shutters";     onClicked: stackView.push("Volets.qml") }
Button { text: "Energy";       onClicked: stackView.push("Consommation.qml") }

Pass properties to the pushed page:

stackView.push("HistoryMinAvgMax.qml", {
    mainTitle: "Indoor temperature",
    object:    "helios.inInsideTemperature",
    min:       18,
    max:       28
})

Reusable Widget Components

KaZaExample organizes reusable UI elements in a Widget/ directory. Each widget takes an object string property and creates its own KzObject:

// Widget/LightSwitch.qml
import QtQuick.Controls 2.3
import org.kazoe.kaza 1.0

Item {
    property string object: ""
    property string label: ""

    KzObject { id: kObj; object: parent.object }

    Switch {
        text: label
        checked: kObj.value === true
        onToggled: kObj.set(checked)
    }
}
// Widget/Sensor.qml
import QtQuick 2.9
import org.kazoe.kaza 1.0

Item {
    property string object: ""
    property string label: ""

    KzObject { id: kObj; object: parent.object }

    Text {
        text: label + ": " + (kObj.value !== undefined
              ? kObj.value.toFixed(1) + " " + kObj.unit
              : "—")
    }
}

Widgets used in a room plan:

import "Widget"

LightSwitch { object: "light.switch.lounge";  label: "Lounge" }
LightSwitch { object: "light.switch.kitchen"; label: "Kitchen" }
Sensor      { object: "meas.temperature.porch"; label: "Outside" }
Shutter     { object: "blind.goto.lounge";    label: "Lounge blind" }

The KaZaExample Widget/ directory contains:

Widget Purpose
LightSwitch.qml Toggle switch for a boolean object
LightControler.qml Switch + dimmer slider
LightControlerSimple.qml Compact switch
Dimmer.qml Standalone dimmer slider
Shutter.qml Blind with position indicator and goto control
ShutterLine.qml Compact single-line shutter control
ShutterPopup.qml Full-screen shutter control popup
Sensor.qml Read-only value + unit display
SensorWater.qml Water flow sensor display
SensorPower.qml Power meter with trend
Battery.qml Battery level bar with icon
Cuve.qml Tank fill level indicator
Presence.qml Presence dot (colored circle)

Connecting to Historical Data

Clients can run SQL queries via FRAME_DBQUERY. The kaza-client exposes a DbQuery type:

import org.kazoe.kaza 1.0

property string object: ""

DbQuery {
    id: historyQuery
    sql: "SELECT date_trunc('hour', time) AS t,
                 MIN(power), AVG(power), MAX(power)
          FROM power
          WHERE gad = (SELECT rawid FROM objects WHERE name = '" + object + "')
            AND time > NOW() - INTERVAL '48 hours'
          GROUP BY t ORDER BY t"
    onResultReady: {
        chartModel.clear()
        for (var i = 0; i < rows.length; i++) {
            chartModel.append({ t: rows[i][0], min: rows[i][1],
                                avg: rows[i][2], max: rows[i][3] })
        }
    }
}

Building and Deploying

Compile the Client Bundle

# client.qrc — list all QML and asset files
cat > client.qrc << 'EOF'
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/">
    <file>main.qml</file>
    <file>HomePage.qml</file>
    <file>Volets.qml</file>
    <file>Settings.qml</file>
    <file>WaitConnection.qml</file>
    <file>KazaToolbar.qml</file>
    <file>KazaSidebar.qml</file>
    <file>Widget/LightSwitch.qml</file>
    <file>Widget/Shutter.qml</file>
    <file>Widget/Sensor.qml</file>
    <file>Images/home.png</file>
</qresource>
</RCC>
EOF

rcc --binary -o app.rcc client.qrc

Deploy to Server

scp -r Server/   pi@home.local:/opt/kaza/
scp app.rcc      pi@home.local:/opt/kaza/app.rcc
ssh pi@home.local "sudo systemctl restart kazad"

Server Configuration

/etc/kazad.conf:

[ssl]
port=1756
keypassword="your_server_key_password"
hostname="home.example.com"

[control]
port=43500
password="your_admin_password"
enable=true

[database]
driver="QPSQL"
hostname="127.0.0.1"
port=5432
dbName="kaza"
username="kaza"
password="your_db_password"

[qml]
server=/opt/kaza/main.qml
client=/opt/kaza/app.rcc

Provisioning Client Certificates

Each new device needs its own SSL certificate. Use the control port from the server:

nc localhost 43500
clientconf? <admin_password> <username> <user_password>

The server returns XML with the hostname, port, CA certificate, signed client certificate, and encrypted private key. The KaZa Client app parses this XML when setting up a new device: enter the server address, username, and password — the app retrieves everything else automatically.


Debugging

Server Logs

sudo journalctl -u kazad -f

QML console.log(), console.debug(), console.warn() output appears here.

Object Inspection with kls

kls                          # All objects and current values
kls /temperature             # Fuzzy: objects matching *t*e*m*p*e*r*a*t*u*r*e*
kls /"meas.power"            # Substring: all power objects

Control Port

nc localhost 43500
obj?                              # List all objects
obj? meas.temperature.porch       # Query one object
refresh meas.temperature.porch    # Force hardware re-read

Common Pitfalls

value is undefined on first render — Objects registered by plugins may not exist yet when the first frame renders. Always guard display code:

text: kObj.value !== undefined ? kObj.value.toFixed(1) + " °C" : "—"

Writing value directly on KzObject — On the server you can set value on a KaZaObject. On a KzObject (both server and client), always use set().

Blocking the main thread — Use XMLHttpRequest for HTTP calls (async). Synchronous I/O in a QML handler will freeze the server for all clients.