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 + ")" ) } }
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.