Skip to main content

Plugin System

Home Assistant Matter Hub supports plugins that register additional Matter devices on the bridge. Plugins can provide virtual devices or integrate third-party services.

Installing a Plugin

From npm

  1. Open the Plugins page in the HAMH web UI
  2. Enter the npm package name (e.g., hamh-plugin-example)
  3. Click Install
  4. Restart the bridge to load the plugin

From a local .tgz file

Upload a packaged plugin via the API:

curl -X POST http://localhost:8482/api/plugins/upload \
-H "Content-Type: application/octet-stream" \
--data-binary @hamh-plugin-example-1.0.0.tgz

From a local folder (development)

Link a local plugin directory:

curl -X POST http://localhost:8482/api/plugins/install-local \
-H "Content-Type: application/json" \
-d '{"path": "/path/to/your/plugin"}'

This creates a symlink, so changes to your plugin source apply on bridge restart. Note that locally linked plugins are not added to the internal package.json dependencies — they rely on the symlink persisting. This method is intended for development only.

Writing a Plugin

A plugin is an npm package that exports a class implementing the MatterHubPlugin interface.

Minimal Structure

my-plugin/
package.json
index.js

package.json:

{
"name": "hamh-plugin-my-plugin",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"hamhPluginApiVersion": 1
}

The hamhPluginApiVersion field declares which plugin API version your plugin targets. HAMH logs a warning if this doesn't match the current API version.

index.js:

export default class MyPlugin {
readonly name = "hamh-plugin-my-plugin";
readonly version = "1.0.0";

async onStart(context) {
await context.registerDevice({
id: "my-device-1",
name: "My Device",
deviceType: "temperature_sensor",
clusters: [
{
clusterId: "temperatureMeasurement",
attributes: { measuredValue: 2150 },
},
],
});
}

async onShutdown() {
// Clean up timers, connections, etc.
}
}

Plugin Lifecycle

HookWhenPurpose
onStart(context)Bridge startsRegister devices, set up connections
onConfigure()After all devices registeredRestore persistent state
onShutdown(reason?)Bridge stopsClean up resources
getConfigSchema()On demandProvide config UI schema
onConfigChanged(config)User updates configApply new configuration

PluginContext API

The context object passed to onStart provides:

  • registerDevice(device) — Register a Matter device on the bridge
  • unregisterDevice(deviceId) — Remove a previously registered device
  • updateDeviceState(deviceId, clusterId, attributes) — Push attribute updates to a device
  • registerDomainMapping(mapping) — Map an HA domain to a Matter device type (see Domain Mappings)
  • storage — Persistent key-value store (survives restarts)
  • log — Scoped logger (info, warn, error, debug)
  • bridgeId — ID of the bridge this plugin is attached to

Supported Device Types

KeyMatter Device
on_off_lightOn/Off Light (0x0100)
dimmable_lightDimmable Light (0x0101)
color_temperature_lightColor Temperature Light (0x0102)
extended_color_lightExtended Color Light (0x010D)
on_off_plugin_unitOn/Off Plug-in Unit (0x010A)
dimmable_plug_in_unitDimmable Plug-in Unit (0x010B)
temperature_sensorTemperature Sensor (0x0302)
humidity_sensorHumidity Sensor (0x0307)
pressure_sensorPressure Sensor (0x0305)
flow_sensorFlow Sensor (0x0306)
light_sensorLight Sensor (0x0106)
occupancy_sensorOccupancy Sensor (0x0107)
contact_sensorContact Sensor (0x0015)
air_quality_sensorAir Quality Sensor (0x002C)
thermostatThermostat (0x0301)
door_lockDoor Lock (0x000A)
fanFan (0x002B)
window_coveringWindow Covering (0x0202)
generic_switchGeneric Switch (0x000F)
water_leak_detectorWater Leak Detector (0x0043)

Cluster IDs

Use Matter.js behavior key names as cluster IDs. Common ones:

Cluster IDDescription
onOffOn/Off state
levelControlBrightness level
colorControlColor (hue/saturation/temperature)
pressureMeasurementPressure (in 0.1 kPa units)
flowMeasurementFlow rate (in 0.1 m³/h units)
windowCoveringWindow covering position and motion
temperatureMeasurementTemperature (in 0.01°C units)
relativeHumidityMeasurementRelative humidity (in 0.01% units)
booleanStateBinary state (open/closed)
occupancySensingOccupancy detection
fanControlFan speed and mode
doorLockLock state

Handling Controller Commands

When a Matter controller writes an attribute (e.g., turns a light on), your device's onAttributeWrite callback is called:

await context.registerDevice({
id: "my-light",
name: "My Light",
deviceType: "on_off_light",
clusters: [
{ clusterId: "onOff", attributes: { onOff: false } },
],
onAttributeWrite: async (clusterId, attribute, value) => {
if (clusterId === "onOff" && attribute === "onOff") {
console.log(`Light turned ${value ? "on" : "off"}`);
// Forward to your actual hardware/service
}
},
});

Persistent Storage

Use context.storage to persist data across restarts:

// Save
await context.storage.set("lastState", { temperature: 21.5 });

// Restore
const saved = await context.storage.get("lastState");

Plugin Config Schema

Plugins can provide a JSON-schema-like config for the UI:

getConfigSchema() {
return {
title: "My Plugin Config",
properties: {
pollingInterval: { type: "number", title: "Polling Interval (ms)" },
apiKey: { type: "string", title: "API Key" },
},
};
}

async onConfigChanged(config) {
this.pollingInterval = config.pollingInterval ?? 30000;
}

Domain Mappings

Plugins can register domain mappings to tell HAMH how to handle HA entity domains that are not natively supported. Call context.registerDomainMapping() during onStart:

async onStart(context) {
// Map all "number" entities to dimmable lights
context.registerDomainMapping({
domain: "number",
matterDeviceType: "dimmable_light",
});
}

The matterDeviceType must be one of the Supported Device Types. Plugin domain mappings are checked after user-configured overrides but before the built-in domain table — they only apply to domains that HAMH does not already handle.

If multiple plugins register the same domain, the last one wins (a warning is logged).

Cloud Provider / Device Source Plugins

Plugins can integrate external cloud services by discovering devices, polling for state, and forwarding controller commands. See examples/hamh-plugin-cloud-mock/ for a full working example that demonstrates:

  • Device discovery from an external API
  • Periodic polling for state changes
  • Forwarding Matter controller commands to the cloud API
  • Storing API tokens securely via context.storage (never logged)
  • Config schema for polling interval and credentials

Replace the MockCloudApi class with your real provider's SDK to build a production plugin.

Error Handling

Plugins run in-process with a safety wrapper:

  • Timeout: Each lifecycle call has a 10-second timeout
  • Circuit breaker: 3 consecutive failures auto-disable the plugin
  • Recovery: Use the Reset button in the Plugins UI to re-enable a disabled plugin
  • Unhandled rejections: Fire-and-forget promises from plugins are caught at the process level and logged without crashing HAMH

The bridge continues running even if a plugin fails. See examples/hamh-plugin-broken/ for a test plugin that exercises various failure modes.

Troubleshooting

ProblemSolution
Plugin not loading after installRestart the bridge — plugins load on startup
"Circuit breaker tripped"Check logs for the error, fix the issue, then click Reset
Device not appearing in controllerVerify deviceType is in the supported list above
Attribute updates ignoredEnsure clusterId matches a behavior key (e.g., onOff, not OnOff)
Plugin crashes on startCheck that onStart doesn't throw — wrap risky code in try/catch

API Reference

EndpointMethodDescription
/api/pluginsGETList installed packages and active plugins per bridge
/api/plugins/installPOSTInstall from npm ({ packageName })
/api/plugins/uploadPOSTInstall from uploaded .tgz (binary body)
/api/plugins/install-localPOSTLink local folder ({ path })
/api/plugins/uninstallPOSTUninstall package ({ packageName })
/api/plugins/:bridgeId/:pluginName/enablePOSTEnable a plugin
/api/plugins/:bridgeId/:pluginName/disablePOSTDisable a plugin
/api/plugins/:bridgeId/:pluginName/resetPOSTReset circuit breaker
/api/plugins/:bridgeId/:pluginName/config-schemaGETGet config schema
/api/plugins/:bridgeId/:pluginName/configPOSTUpdate config ({ config })