Plugin Manifest
Every plugin zip must contain a plugin.json at its root. This file is the single source of truth for the plugin's identity, services, datasources, events, and actions.
Full example
Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Unique identifier for the plugin. Used as the plugin's primary key throughout the system. Use lowercase with hyphens (e.g. my-plugin). |
name | string | Yes | Human-readable display name shown in the dashboard and plugin registry. |
version | string | Yes | Semantic version string (e.g. 1.0.0). |
description | string | Yes | A short description of what the plugin integrates. Shown in the plugin registry. |
hasAuth | boolean | Yes | Whether the plugin requires a username and password. If true, the user is prompted to enter credentials after install and they are injected as PLUGIN_USERNAME / PLUGIN_PASSWORD. Use config instead for arbitrary named fields. |
config | object | No | Map of user-configurable fields presented to the user in the plugin settings UI. Each key is the exact environment variable name injected into service subprocesses at runtime. Omit if the plugin has no configurable settings. |
deviceManufacturer | string | Yes | Name of the hardware or service manufacturer (e.g. TP-Link Systems Inc.). |
services | array | Yes | List of service subprocesses this plugin runs. Can be empty if the plugin ships widgets only. |
datasources | array | Yes | List of datasource definitions this plugin will publish telemetry to. Can be empty. |
events | array | Yes | List of events this plugin can emit. Can be empty. |
actions | array | Yes | List of actions this plugin exposes for use in automations. Can be empty. |
widgets | array | No | List of widget manifest entries this plugin provides. Omit if the plugin ships no widgets. See the Widget SDK docs for the full field reference. |
hasDesktopBundle | boolean | No | Set to true if the plugin zip includes widgets/desktop/bundle.js. The desktop client uses this flag to know whether to fetch and inject the bundle. Defaults to false. |
widgetColor | string | No | Optional hex color (e.g. #4CAF50) used as the background of all dashboard widgets from this plugin. Helps users identify which plugin a widget belongs to at a glance. When omitted, widgets use the default theme background: white in light mode, #007399 in dark mode. |
services[]
Each entry in services is launched as a managed subprocess by the Hiclaro service host. The host restarts crashed services automatically and reports their status back to the API.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name for the service subprocess. |
runtime | string | Yes | Runtime used to launch the entrypoint. One of: python-uv, node, binary. |
entrypoint | string | Yes | Path to the entrypoint file, relative to the services/ directory inside the zip — not the zip root. So main.py refers to services/main.py. |
env | object | No | Static environment variables baked into the manifest and injected into the subprocess (e.g. PYTHONUNBUFFERED=1). For user-supplied values, use the top-level config field instead. |
Python dependencies (python-uv runtime)
A pyproject.toml declaring your dependencies under [project] dependencies must be present inside services/ in the zip. On first install the service host creates a virtual environment and runs uv sync to install them. Every subsequent launch uses uv run --no-sync — the --no-sync flag is intentional; installation is a one-time step, not repeated on every restart. A missing or incomplete pyproject.toml will not fail at install time but will cause a ModuleNotFoundError at runtime.
config{}
The optional config object declares user-configurable fields for your plugin. Each key is the exact environment variable name injected into your service subprocesses at runtime — so a field keyed ACME_API_URL is available in your service as os.getenv("ACME_API_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.
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Human-readable label displayed in the plugin settings form. |
type | string | Yes | Input type. One of: string, number, boolean, select. |
description | string | No | Helper text shown below the input field. |
required | boolean | No | If true, the field must be filled before the plugin services will run. Defaults to false. |
secret | boolean | No | If true, the value is masked in the UI (password input). Use for API tokens, passwords, and other sensitive values. Defaults to false. |
default | string | No | Default value pre-filled in the settings form. |
options | array | No | Required when type is select. Array of { label, value } objects listing the allowed choices. |
min | number | No | Minimum value. Only applicable when type is number. |
max | number | No | Maximum value. Only applicable when type is number. |
step | number | No | Increment step for number inputs. Only applicable when type is number. |
Example
Reading these in your service:
datasources[]
Datasource definitions tell Hiclaro what telemetry your plugin will publish. They are upserted by key on plugin install and whenever the service registers them over MQTT.
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Dotted-path identifier for the datasource (e.g. tapo.plug.current_power). Must be unique across the system. |
label | string | Yes | Human-readable name shown in the dashboard datasource picker. |
unit | string | No | Unit of measurement (e.g. W, °C, %). Omit if not applicable. |
type | string | Yes | One of: number, string, boolean. See type reference below. |
meta | object | No | Additional metadata. Currently supports { min, max } for datasources with a known range (used by gauge widgets). |
Datasource types
| Type | Description |
|---|---|
number | A numeric value. Stored in the telemetry table with hourly rollups (avg / min / max). Use for anything you want to chart historically — temperature, power draw, SOC, remaining capacity. |
string | A text value representing a mode or status (e.g. charging / idle, eco / comfort / boost). Stored in the event telemetry table; consecutive identical values are deduplicated. |
boolean | A true/false toggle (e.g. is_charging, is_heating, plug on/off). Stored as the strings "true" or "false" in the event telemetry table; consecutive duplicates are deduplicated. |
events[]
Events are discrete occurrences your plugin can emit, available as triggers in the automation builder. An event with hasPayload: true carries a value that can be used in automation conditions.
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Identifier used in automation triggers (e.g. plug_state_changed). |
label | string | Yes | Human-readable label shown in the automation builder. |
hasPayload | boolean | Yes | Whether this event carries a payload value. |
valueType | string | No | Type of the payload value: string, number, or boolean. Required when hasPayload is true. |
actions[]
Actions are operations the automation engine can invoke on your plugin. An action with hasValue: true accepts a parameter supplied by the automation builder. Set perDevice: true if the action targets a specific device instance rather than the plugin as a whole.
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Identifier used in automation actions (e.g. set_plug_state). |
label | string | Yes | Human-readable label shown in the automation builder. |
hasValue | boolean | Yes | Whether this action accepts a value parameter. |
valueType | string | No | Type of the value: string, number, or boolean. Required when hasValue is true. |
perDevice | boolean | No | If true, the automation builder prompts the user to select a specific device to target. |
acceptedValues | array | No | Fixed set of allowed values. When present, the automation builder shows a Select instead of a free-text input. Each item is either a plain string or an object { value: string; label: string } for a labelled option. |