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:
-
Server-side QML — a
main.qml(and optional companion files) loaded bykazadat startup. This code runs continuously on the server. It declares plugins, wires up automation rules, schedules actions, and logs data. -
Client-side QML — a set of QML files compiled into a
.rccresource 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.