Plugin Development
Plugin Development
A KaZa plugin is a Qt QML extension module written in C++. It exposes new QML types to the integrator's server-side QML code and creates KaZaObject instances that are synchronized automatically to all connected clients.
You write a plugin when a new protocol or device type needs to be integrated: a bus (KNX, Modbus, CAN), a broker (MQTT), an HTTP-based appliance, a serial device (GPIO, I²C), or an external cloud API. Once the plugin is installed, the integrator simply imports its module and uses its types in QML — no C++ knowledge required on their side.
The existing official plugins (kaza-knx, kaza-mqtt, kaza-helios, kaza-palazzetti) all follow the same structure described in this guide.
Prerequisites
- Qt 6.2+ development packages
- CMake 3.16+
- The
kaza-server-devDebian package (provideskazaobject.h,kazamanager.h, andlibKaZaLib.so) - C++17 compiler
Install build dependencies on Debian/Ubuntu:
sudo apt install build-essential cmake \ qt6-base-dev qt6-declarative-dev \ kaza-server-dev
Plugin Structure
A plugin is a shared library that Qt's QML engine loads when an import statement matches its module name. The minimal set of files is:
kaza-myplugin/ ├── CMakeLists.txt # Build definition and packaging ├── qmldir # QML module declaration ├── README.md └── src/ ├── plugin.h # QQmlExtensionPlugin subclass ├── plugin.cpp ├── myhardware.h # The main QML type └── myhardware.cpp
For complex plugins with many exposed types, add one .h/.cpp pair per type.
Step-by-Step: Creating a Plugin
1. qmldir — Module Declaration
This file tells Qt's QML engine which module name maps to which shared library:
module org.kazoe.myplugin plugin MyPlugin
The module name (org.kazoe.myplugin) is what integrators write in their import statement. The plugin name (MyPlugin) must match the library filename (libMyPlugin.so).
2. CMakeLists.txt — Build and Package
cmake_minimum_required(VERSION 3.16) project(MyPlugin VERSION 1.0 LANGUAGES CXX) set(CMAKE_AUTOMOC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 6.2 COMPONENTS Quick REQUIRED) find_package(KaZa REQUIRED) # kaza-server-dev package include_directories(${KAZA_INCLUDE_DIR}) add_library(MyPlugin SHARED src/plugin.h src/plugin.cpp src/myhardware.h src/myhardware.cpp qmldir ) target_link_libraries(MyPlugin PRIVATE Qt6::Quick) # Detect the correct Qt QML installation path if(NOT DEFINED QML_MODULE_INSTALL_PATH) if(EXISTS /usr/lib64/qt6/qml/builtins.qmltypes) set(QML_MODULE_INSTALL_PATH "/usr/lib64/qt6/qml/") elseif(EXISTS /usr/lib/aarch64-linux-gnu/qt6/qml/builtins.qmltypes) set(QML_MODULE_INSTALL_PATH "/usr/lib/aarch64-linux-gnu/qt6/qml/") else() set(QML_MODULE_INSTALL_PATH ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/qml) endif() endif() install(TARGETS MyPlugin DESTINATION ${QML_MODULE_INSTALL_PATH}/org/kazoe/myplugin) install(FILES qmldir DESTINATION ${QML_MODULE_INSTALL_PATH}/org/kazoe/myplugin) # Debian packaging include(GNUInstallDirs) set(CPACK_GENERATOR "DEB") set(CPACK_PACKAGE_NAME "kaza-myplugin") set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") set(CPACK_DEBIAN_PACKAGE_DEPENDS "kaza-server-bin (>= 1.0), libqt6core6") set(CPACK_PACKAGE_DESCRIPTION "My hardware integration for KaZa Server") set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Your Name <your@email.com>") set(CPACK_DEBIAN_PACKAGE_SECTION "utils") include(CPack)
3. plugin.h / plugin.cpp — Plugin Entry Point
// src/plugin.h #pragma once #include <QQmlExtensionPlugin> class MyPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) public: void registerTypes(const char *uri) override; };
// src/plugin.cpp #include "plugin.h" #include "myhardware.h" #include <QtQml> void MyPlugin::registerTypes(const char *uri) { qmlRegisterType<MyHardware>(uri, 1, 0, "MyHardware"); // Register additional types here if the plugin exposes more than one }
Every type you call qmlRegisterType on becomes available in QML after import.
4. myhardware.h — The QML Type
// src/myhardware.h #pragma once #include <QObject> #include <QTimer> #include <kazaobject.h> // from kaza-server-dev class MyHardware : public QObject { Q_OBJECT // QML-configurable properties Q_PROPERTY(QString device READ device WRITE setDevice NOTIFY deviceChanged) Q_PROPERTY(int pollInterval READ pollInterval WRITE setPollInterval) Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) public: explicit MyHardware(QObject *parent = nullptr); ~MyHardware(); QString device() const; void setDevice(const QString &device); int pollInterval() const { return m_pollInterval; } void setPollInterval(int ms) { m_pollInterval = ms; } bool connected() const; public slots: void connectToDevice(); void disconnectFromDevice(); signals: void deviceChanged(); void connectedChanged(); void error(QString message); private slots: void poll(); private: void initialize(); void createObjects(); QString m_device; int m_pollInterval{5000}; bool m_connected{false}; QTimer m_timer; KaZaObject *m_temperature{nullptr}; KaZaObject *m_humidity{nullptr}; };
5. myhardware.cpp — Implementation
// src/myhardware.cpp #include "myhardware.h" #include <kazamanager.h> #include <QDebug> MyHardware::MyHardware(QObject *parent) : QObject(parent) { connect(&m_timer, &QTimer::timeout, this, &MyHardware::poll); } MyHardware::~MyHardware() { if (m_connected) disconnectFromDevice(); } QString MyHardware::device() const { return m_device; } void MyHardware::setDevice(const QString &device) { if (m_device == device) return; m_device = device; emit deviceChanged(); initialize(); } bool MyHardware::connected() const { return m_connected; } void MyHardware::initialize() { if (m_device.isEmpty()) return; createObjects(); // create KaZaObjects once connectToDevice(); } void MyHardware::createObjects() { if (m_temperature) return; // already created // Object names follow the convention: <plugin>.<category>.<name> m_temperature = new KaZaObject("myplugin.sensor.temperature", this); m_temperature->setUnit("°C"); KaZaManager::registerObject(m_temperature); m_humidity = new KaZaObject("myplugin.sensor.humidity", this); m_humidity->setUnit("%"); KaZaManager::registerObject(m_humidity); } void MyHardware::connectToDevice() { // Hardware-specific connection code here // Example: open a serial port, TCP socket, etc. m_connected = true; emit connectedChanged(); m_timer.start(m_pollInterval); qDebug() << "MyPlugin: connected to" << m_device; } void MyHardware::disconnectFromDevice() { m_timer.stop(); // Hardware-specific disconnect code m_connected = false; emit connectedChanged(); } void MyHardware::poll() { // Read from the hardware device // Replace with actual hardware access: float temperature = readTemperatureFromDevice(); float humidity = readHumidityFromDevice(); if (m_temperature) m_temperature->setValue(temperature); if (m_humidity) m_humidity->setValue(humidity); }
KaZaObject API
KaZaObject is the central class that bridges your plugin's data to the KaZa ecosystem. Once registered, its value is:
- synchronized to all connected clients over the SSL port
- accessible by name in the integrator's server-side QML via KzObject
- available for inspection via the kls command-line tool and the Control Panel
#include <kazaobject.h> #include <kazamanager.h> // Construct with name and parent KaZaObject *obj = new KaZaObject("myplugin.sensor.temperature", this); // Set unit (displayed in Control Panel and client UI) obj->setUnit("°C"); // Register — must be called before setValue will reach clients KaZaManager::registerObject(obj); // Update the value (call whenever the hardware reports a new reading) obj->setValue(23.5); // accepts bool, int, double, QString, QDateTime, … // The server can also change values (e.g. from integrator QML): // obj->changeValue(newValue); // same as setValue but also notifies the originating plugin
Key KaZaObject Members
| Method/Property | Description |
|---|---|
KaZaObject(name, parent) |
Constructor — name is the global identifier |
setUnit(string) |
Set the human-readable unit |
setValue(QVariant) |
Update value and push to all clients |
changeValue(QVariant) |
Update from an external write (client or QML) |
value() |
Read current value |
unit() |
Read current unit |
name() |
Read the registered name |
valueChanged |
Signal — emitted on every setValue call |
Object Naming Convention
Use <plugin>.<category>.<identifier>:
knx.lights.living_room_switch mqtt.sensors.bedroom.temperature myplugin.zone1.relay_a myplugin.meter.main.watts
Keeping the plugin name as the first segment avoids collisions between plugins and makes it easy to filter with kls /myplugin.
Threading
Qt's QML engine and all KaZaObject updates must happen on the main thread. However, many hardware interfaces are blocking — serial ports, TCP sockets, I²C buses. Use a QThread + worker pattern to isolate blocking I/O:
// Worker lives in a separate thread class MyWorker : public QObject { Q_OBJECT public slots: void start() { // Blocking read loop while (!m_stop) { float val = blockingReadFromHardware(); emit valueReady(val); } } void stop() { m_stop = true; } signals: void valueReady(float value); private: bool m_stop{false}; }; // In MyHardware: class MyHardware : public QObject { Q_OBJECT public: MyHardware(QObject *parent = nullptr) : QObject(parent) , m_thread(new QThread(this)) , m_worker(new MyWorker) { m_worker->moveToThread(m_thread); connect(m_thread, &QThread::started, m_worker, &MyWorker::start); connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater); // valueReady is emitted in the worker thread; handleValue runs in main thread connect(m_worker, &MyWorker::valueReady, this, &MyHardware::handleValue); m_thread->start(); } ~MyHardware() { m_worker->stop(); m_thread->quit(); m_thread->wait(); } private slots: void handleValue(float v) { // Back on main thread — safe to call setValue if (m_obj) m_obj->setValue(v); } private: QThread *m_thread; MyWorker *m_worker; KaZaObject *m_obj{nullptr}; };
Rule: Never call KaZaObject::setValue() from a thread other than the main thread. Use signals/slots with automatic connection (the default) to cross thread boundaries safely.
Bidirectional Updates
When a client or the integrator's QML writes to an object (e.g. switches a light), the server calls KaZaObject::changeValue() and emits valueChanged. Your plugin should connect to this signal and forward the new value to the hardware:
void MyHardware::createObjects() { m_relay = new KaZaObject("myplugin.relay.output", this); KaZaManager::registerObject(m_relay); // Forward client/QML writes to the hardware connect(m_relay, &KaZaObject::valueChanged, this, [this](const QVariant &v) { setRelayState(v.toBool()); }); } void MyHardware::setRelayState(bool on) { // Write to actual hardware writeToHardware(on); }
Dynamic Object Discovery
For protocols where the set of objects is not known until runtime (e.g. scanning a Modbus device or reading an ETS project file), create objects during the discovery phase:
void MyHardware::discoverDevices() { QStringList discovered = scanBus(); for (const QString &deviceId : discovered) { QString name = QString("myplugin.device.%1.status").arg(deviceId); KaZaObject *obj = new KaZaObject(name, this); KaZaManager::registerObject(obj); m_objects.insert(deviceId, obj); qDebug() << "Registered:" << name; } }
QML Properties for Integrator Configuration
Expose properties so the integrator can configure the plugin from QML without recompiling:
// In header Q_PROPERTY(QString host READ host WRITE setHost) Q_PROPERTY(int port READ port WRITE setPort) Q_PROPERTY(int pollMs READ pollMs WRITE setPollMs) Q_PROPERTY(QString configFile READ configFile WRITE setConfigFile)
The integrator then writes:
import org.kazoe.myplugin 1.0 KaZaElement { MyHardware { host: "192.168.1.10" port: 502 pollMs: 1000 configFile: "/etc/kaza/mydevice.conf" onConnectedChanged: console.log("MyHardware connected:", connected) onError: function(msg) { console.error("MyHardware error:", msg) } } }
Complete Minimal Example
kaza-relay/
├── CMakeLists.txt
├── qmldir
└── src/
├── plugin.h
├── plugin.cpp
├── relaycontroller.h
└── relaycontroller.cpp
qmldir:
module org.kazoe.relay plugin RelayPlugin
src/plugin.h:
#pragma once #include <QQmlExtensionPlugin> class RelayPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) public: void registerTypes(const char *uri) override; };
src/plugin.cpp:
#include "plugin.h" #include "relaycontroller.h" #include <QtQml> void RelayPlugin::registerTypes(const char *uri) { qmlRegisterType<RelayController>(uri, 1, 0, "RelayController"); }
src/relaycontroller.h:
#pragma once #include <QObject> #include <kazaobject.h> class RelayController : public QObject { Q_OBJECT Q_PROPERTY(QString device READ device WRITE setDevice NOTIFY deviceChanged) public: explicit RelayController(QObject *parent = nullptr); QString device() const; void setDevice(const QString &v); signals: void deviceChanged(); void error(QString message); private: void initialize(); QString m_device; KaZaObject *m_relay{nullptr}; KaZaObject *m_status{nullptr}; };
src/relaycontroller.cpp:
#include "relaycontroller.h" #include <kazamanager.h> #include <QDebug> RelayController::RelayController(QObject *parent) : QObject(parent) {} QString RelayController::device() const { return m_device; } void RelayController::setDevice(const QString &v) { if (m_device == v) return; m_device = v; emit deviceChanged(); initialize(); } void RelayController::initialize() { if (m_device.isEmpty() || m_relay) return; m_relay = new KaZaObject("relay.output.main", this); KaZaManager::registerObject(m_relay); connect(m_relay, &KaZaObject::valueChanged, this, [this](const QVariant &val) { qDebug() << "Relay set to:" << val.toBool(); // Write to actual GPIO/serial/etc here }); m_status = new KaZaObject("relay.status.main", this); m_status->setUnit(""); KaZaManager::registerObject(m_status); }
Usage in integrator QML:
import org.kazoe.relay 1.0 KaZaElement { RelayController { device: "/dev/ttyUSB0" } KzObject { object: "relay.output.main" onValueChanged: console.log("Relay state:", value) } }
Building
Development Build
mkdir build && cd build cmake .. cmake --build . -j$(nproc) sudo cmake --install . sudo systemctl restart kazad
Debian Package
mkdir build && cd build cmake .. cmake --build . -j$(nproc) cpack -G DEB sudo dpkg -i kaza-myplugin_1.0.0_amd64.deb
Cross-compile for Raspberry Pi (arm64)
mkdir build-arm64 && cd build-arm64 cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/aarch64-linux-gnu.cmake .. cmake --build . -j$(nproc) cpack -G DEB
Multi-Architecture Distribution
# In CMakeLists.txt, generate one package per architecture: set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "${CMAKE_SYSTEM_PROCESSOR}")
Build on each architecture (or use a cross-compilation sysroot) and host all .deb files in the same APT repository:
reprepro -b /srv/apt/kaza includedeb stable kaza-myplugin_1.0.0_amd64.deb reprepro -b /srv/apt/kaza includedeb stable kaza-myplugin_1.0.0_arm64.deb reprepro -b /srv/apt/kaza includedeb stable kaza-myplugin_1.0.0_armhf.deb
Testing
Minimal Test QML
Create a test main.qml that exercises your plugin:
// test-main.qml import QtQml 2.0 import org.kazoe.kaza 1.0 import org.kazoe.myplugin 1.0 KaZaElement { MyHardware { device: "/dev/ttyUSB0" pollMs: 2000 Component.onCompleted: console.log("Plugin loaded") onConnectedChanged: console.log("Connected:", connected) onError: function(msg) { console.error("Error:", msg) } } KzObject { object: "myplugin.sensor.temperature" onValueChanged: console.log("Temperature:", value, unit) } }
Point the server at it:
# /etc/kazad.conf [qml] server=/tmp/test-main.qml
sudo systemctl restart kazad sudo journalctl -u kazad -f
Control Port Verification
nc localhost 43500 obj? # Should list your registered objects obj? myplugin.sensor.temperature # Should return current value
kls Tool
kls /myplugin # All objects whose name matches *m*y*p*l*u*g*i*n*
Best Practices
-
Namespace your objects — always prefix with your plugin name:
myplugin.* -
Register objects once — call
KaZaManager::registerObject()once per object, typically in aninitialize()method guarded by a null check -
Thread safety — call
KaZaObject::setValue()only from the main thread; use signal/slot connections to marshal results from worker threads -
Handle disconnection gracefully — emit the
errorsignal with a human-readable message; do not crash the server - Expose configuration as Q_PROPERTY — let integrators configure your plugin from QML without recompiling
- Clean up in the destructor — stop timers, close connections, wait for threads to finish
-
Version your CMake project — semantic versioning (
1.0.0,1.1.0, etc.) makes Debian dependency resolution reliable - Document your QML API — write a short README listing every QML type, its properties, and example usage
-
Log with categories — use
qDebug() << "MyPlugin:"as a prefix so log lines are easy to grep injournalctl
Reference: Official Plugin Examples
The four official plugins ship with the KaZa-Builder repository and demonstrate common patterns:
| Plugin | Pattern | Key technique |
|---|---|---|
kaza-knx |
Protocol bus | Parses .knxproj, creates one KaZaObject per group address, connects to knxd TCP socket |
kaza-mqtt |
Broker subscription |
QMqttClient, maps topics to KaZaObject; bidirectional via valueChanged → publish |
kaza-helios |
HTTP polling |
QNetworkAccessManager GET every N seconds, parses JSON response |
kaza-palazzetti |
HTTP polling | ConnBox HTTP API, parses XML/JSON device status |
For a bus-style plugin (KNX, Modbus), use the KNX plugin as the reference. For an HTTP appliance, use Helios. For a pub/sub broker, use the MQTT plugin.