KaZa Home automation

Home automation

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-dev Debian package (provides kazaobject.h, kazamanager.h, and libKaZaLib.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

  1. Namespace your objects — always prefix with your plugin name: myplugin.*
  2. Register objects once — call KaZaManager::registerObject() once per object, typically in an initialize() method guarded by a null check
  3. Thread safety — call KaZaObject::setValue() only from the main thread; use signal/slot connections to marshal results from worker threads
  4. Handle disconnection gracefully — emit the error signal with a human-readable message; do not crash the server
  5. Expose configuration as Q_PROPERTY — let integrators configure your plugin from QML without recompiling
  6. Clean up in the destructor — stop timers, close connections, wait for threads to finish
  7. Version your CMake project — semantic versioning (1.0.0, 1.1.0, etc.) makes Debian dependency resolution reliable
  8. Document your QML API — write a short README listing every QML type, its properties, and example usage
  9. Log with categories — use qDebug() << "MyPlugin:" as a prefix so log lines are easy to grep in journalctl

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.