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
- Open the Plugins page in the HAMH web UI
- Enter the npm package name (e.g.,
hamh-plugin-example) - Click Install
- 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
| Hook | When | Purpose |
|---|---|---|
onStart(context) | Bridge starts | Register devices, set up connections |
onConfigure() | After all devices registered | Restore persistent state |
onShutdown(reason?) | Bridge stops | Clean up resources |
getConfigSchema() | On demand | Provide config UI schema |
onConfigChanged(config) | User updates config | Apply new configuration |
PluginContext API
The context object passed to onStart provides:
registerDevice(device), Register a Matter device on the bridgeunregisterDevice(deviceId), Remove a previously registered deviceupdateDeviceState(deviceId, clusterId, attributes), Push attribute updates to a deviceregisterDomainMapping(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
| Key | Matter Device |
|---|---|
on_off_light | On/Off Light (0x0100) |
dimmable_light | Dimmable Light (0x0101) |
color_temperature_light | Color Temperature Light (0x0102) |
extended_color_light | Extended Color Light (0x010D) |
on_off_plugin_unit | On/Off Plug-in Unit (0x010A) |
dimmable_plug_in_unit | Dimmable Plug-in Unit (0x010B) |
temperature_sensor | Temperature Sensor (0x0302) |
humidity_sensor | Humidity Sensor (0x0307) |
pressure_sensor | Pressure Sensor (0x0305) |
flow_sensor | Flow Sensor (0x0306) |
light_sensor | Light Sensor (0x0106) |
occupancy_sensor | Occupancy Sensor (0x0107) |
contact_sensor | Contact Sensor (0x0015) |
air_quality_sensor | Air Quality Sensor (0x002C) |
thermostat | Thermostat (0x0301) |
door_lock | Door Lock (0x000A) |
fan | Fan (0x002B) |
window_covering | Window Covering (0x0202) |
generic_switch | Generic Switch (0x000F) |
water_leak_detector | Water Leak Detector (0x0043) |
Cluster IDs
Use Matter.js behavior key names as cluster IDs. Common ones:
| Cluster ID | Description |
|---|---|
onOff | On/Off state |
levelControl | Brightness level |
colorControl | Color (hue/saturation/temperature) |
pressureMeasurement | Pressure (in 0.1 kPa units) |
flowMeasurement | Flow rate (in 0.1 m³/h units) |
windowCovering | Window covering position and motion |
temperatureMeasurement | Temperature (in 0.01°C units) |
relativeHumidityMeasurement | Relative humidity (in 0.01% units) |
booleanState | Binary state (open/closed) |
occupancySensing | Occupancy detection |
fanControl | Fan speed and mode |
doorLock | Lock 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
| Problem | Solution |
|---|---|
| Plugin not loading after install | Restart the bridge, plugins load on startup |
| "Circuit breaker tripped" | Check logs for the error, fix the issue, then click Reset |
| Device not appearing in controller | Verify deviceType is in the supported list above |
| Attribute updates ignored | Ensure clusterId matches a behavior key (e.g., onOff, not OnOff) |
| Plugin crashes on start | Check that onStart doesn't throw, wrap risky code in try/catch |
API Reference
| Endpoint | Method | Description |
|---|---|---|
/api/plugins | GET | List installed packages and active plugins per bridge |
/api/plugins/install | POST | Install from npm ({ packageName }) |
/api/plugins/upload | POST | Install from uploaded .tgz (binary body) |
/api/plugins/install-local | POST | Link local folder ({ path }) |
/api/plugins/uninstall | POST | Uninstall package ({ packageName }) |
/api/plugins/:bridgeId/:pluginName/enable | POST | Enable a plugin |
/api/plugins/:bridgeId/:pluginName/disable | POST | Disable a plugin |
/api/plugins/:bridgeId/:pluginName/reset | POST | Reset circuit breaker |
/api/plugins/:bridgeId/:pluginName/config-schema | GET | Get config schema |
/api/plugins/:bridgeId/:pluginName/config | POST | Update config ({ config }) |