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 }) |