<!--
.. title: Plugin Development
.. slug: 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:

```bash
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
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

```cpp
// 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;
};
```

```cpp
// 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

```cpp
// 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

```cpp
// 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

```cpp
#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:

```cpp
// 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:

```cpp
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:

```cpp
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:

```cpp
// 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:

```qml
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`:**
```cpp
#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`:**
```cpp
#include "plugin.h"
#include "relaycontroller.h"
#include <QtQml>
void RelayPlugin::registerTypes(const char *uri) {
    qmlRegisterType<RelayController>(uri, 1, 0, "RelayController");
}
```

**`src/relaycontroller.h`:**
```cpp
#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`:**
```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:**
```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

```bash
mkdir build && cd build
cmake ..
cmake --build . -j$(nproc)
sudo cmake --install .
sudo systemctl restart kazad
```

### Debian Package

```bash
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)

```bash
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

```cmake
# 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:

```bash
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:

```qml
// 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:

```ini
# /etc/kazad.conf
[qml]
server=/tmp/test-main.qml
```

```bash
sudo systemctl restart kazad
sudo journalctl -u kazad -f
```

### Control Port Verification

```bash
nc localhost 43500
obj?                              # Should list your registered objects
obj? myplugin.sensor.temperature  # Should return current value
```

### `kls` Tool

```bash
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.
