Overview

Plugins

Widgets

Reference

Publishing

Overview

Build a plugin to integrate a new device or service into Hiclaro.

Overview

A Hiclaro plugin is a zip archive containing a manifest, an optional service entrypoint, and optional widget bundles. Once installed, the service host manages the plugin as a subprocess and monitors its health.

Plugin zip layout

The zip must contain a plugin.json manifest at its root. Service code goes inside services/. Widget bundles go inside widgets/. Both directories are optional — a plugin can ship services only, widgets only, or both.

my-plugin-1.0.0.zip
├── plugin.json
├── services/
│   └── main.py
└── widgets/
    ├── desktop/
    │   └── bundle.js       ← desktop IIFE bundle (optional)
    └── mobile/
        └── bundle.js       ← mobile React Native bundle (optional)

Plugin manifest (plugin.json)

Every plugin must include a plugin.json at the root of the zip. The manifest declares the plugin identity, version, services, datasources, events, actions, and widgets. The services array defines how each service subprocess is launched — runtime (python-uv, node, or binary), entrypoint file, and any required environment variables.

See the manifest reference →

Service runtime

Each entry in the services array is launched by the service host as a subprocess. The host polls the API every 5 seconds, reconciles running processes against the registered state, and restarts crashed services automatically.

Services are considered running as soon as the process starts — the host does not wait for any handshake. If your plugin connects to a network device or remote API on startup, it should implement retry logic with backoff rather than crashing on the first failed attempt. The host will restart crashed processes, but repeated crash loops will eventually mark the service as errored and stop retrying.

Python dependencies (python-uv runtime)

If your service uses the python-uv runtime, a pyproject.toml must be present inside services/ in the zip, declaring your dependencies under [project] dependencies. On first install the service host creates a virtual environment with uv venv and installs dependencies with uv sync. After that, the service is launched with uv run --no-sync on every start.

The --no-sync flag is intentional — dependency installation is a one-time step that happens on first install, not on every restart. If your pyproject.toml is missing or does not declare a required package, the install step will succeed silently and the service will fail at runtime with a ModuleNotFoundError.

# pyproject.toml — lives at the plugin root, packed into services/ in the zip
[project]
name = "my-plugin"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
    "paho-mqtt>=2.1.0",
    "requests>=2.32.0",
]

Config fields

If your plugin needs user-supplied settings — a server URL, an API token, a device IP address — declare them in the config object in plugin.json. Each key is the exact environment variable name injected into your service subprocesses at runtime, so a field keyed HA_URL is available in your service as os.getenv("HA_URL").

After install, Hiclaro presents a settings form with the declared fields. When the user saves new values, all running services for the plugin are restarted automatically so they pick up the new environment.

// plugin.json
{
  "config": {
    "HA_URL": {
      "label": "Home Assistant URL",
      "type": "string",
      "required": true,
      "description": "e.g. http://homeassistant.local:8123"
    },
    "HA_TOKEN": {
      "label": "Long-Lived Access Token",
      "type": "string",
      "required": true,
      "secret": true
    }
  }
}
import os

ha_url   = os.getenv("HA_URL")
ha_token = os.getenv("HA_TOKEN")
See the full config field reference →

Auth credentials

For plugins that authenticate with a cloud service using a username and password, set hasAuth: true in plugin.json. Hiclaro will prompt the user to enter credentials and store them encrypted on the device.

When the service host starts your plugin subprocess, it decrypts the stored credentials and injects them as PLUGIN_USERNAME and PLUGIN_PASSWORD. Your service code simply reads:

import os

username = os.getenv("PLUGIN_USERNAME")
password = os.getenv("PLUGIN_PASSWORD")

No cryptographic code is required in your plugin. The encryption scheme and key material are internal to the platform and are never exposed to plugin processes.

hasAuth is a required manifest field — always include it. Set it to false if your plugin has no credential requirement. Hiclaro only shows the credentials form when the value is true.

Persistent data storage

The service host injects a PLUGIN_DATA_DIR environment variable into every plugin subprocess. This is a dedicated directory on the host filesystem where your plugin can read and write persistent files — device caches, state snapshots, configuration it manages itself. The path is stable across reinstalls because it is keyed by the plugin's manifest key, not by a transient service ID.

import os, pathlib

data_dir = pathlib.Path(os.getenv("PLUGIN_DATA_DIR", "."))
cache_file = data_dir / "devices.json"

On the standard deployment the root is /opt/eims/plugin-data/<plugin-key>/. The directory is created by the platform before the first launch — your plugin does not need to create it.

Publishing telemetry over MQTT

Plugins publish device data to eims/telemetry as a batched payload and register datasource metadata to eims/registry/datasource/register. See the MQTT reference for the full payload contract.

See the MQTT reference →

Packaging and installing

Build your plugin as a zip with the directory structure above. To install it locally for testing, open the desktop app, go to the Plugins page, and use the Install from file button to upload your zip directly.

© 2026 Hiclaro. All rights reserved.