Troubleshooting & FAQ¶
Common issues, debugging tips, and frequently asked questions.
Setup issues¶
MQTT connection fails¶
Symptoms: Engine starts but logs MQTT connection error or MQTT offline repeatedly.
Checklist:
-
Verify the broker is running:
-
Check
MQTT_HOSTandMQTT_PORT—localhostwon't work from inside a Docker container; use the service name (e.g.mosquitto) or the host IP. -
Authentication — if your broker requires credentials, set
MQTT_USERNAMEandMQTT_PASSWORDin your.env. -
Firewall — ensure port 1883 (or your custom port) is open between the engine and the broker.
-
Zigbee2MQTT prefix — verify
ZIGBEE2MQTT_PREFIXmatches your Zigbee2MQTT configuration (default:zigbee2mqtt).
Engine starts but no automations load¶
Checklist:
-
automationsDirpath — must be an absolute path or resolvable relative path. Useimport.meta.urlfor reliable resolution: -
Default export — each automation file must export the class as the default export:
-
File extensions — the engine scans for
.tsand.jsfiles only. Files ending in.d.tsare excluded. -
Recursive scanning — if automations are in subdirectories, set
AUTOMATIONS_RECURSIVE=trueor passrecursive: truetocreateEngine(). -
Runtime errors — check the logs for import errors or constructor exceptions. Set
LOG_LEVEL=debugfor more detail.
TypeScript compilation errors¶
-
Missing
.jsextensions — relative imports must include the.jsextension: -
Node built-ins — use the
node:prefix: -
tsconfig.json — ensure
moduleResolutionis set to"bundler"andmoduleto"ESNext".
Runtime issues¶
Automation doesn't fire¶
MQTT trigger not firing:
-
Verify the topic is correct — use
mosquitto_subto confirm messages are arriving: -
Check the filter function — if defined, it must return
truefor the trigger to fire. Remove the filter temporarily to confirm the trigger itself works. -
Wildcard topics —
+matches exactly one level,#matches zero or more remaining levels.zigbee2mqtt/+/setmatcheszigbee2mqtt/bulb/setbut notzigbee2mqtt/room/bulb/set.
State trigger not firing:
StateManager.set()only fires listeners when the value actually changes. Primitives use strict equality (===); objects useJSON.stringifycomparison. Setting the same value again is a no-op.- Object key order matters:
{ a: 1, b: 2 }and{ b: 2, a: 1 }produce differentJSON.stringifyoutput and will trigger a change even though they are semantically equal.
Cron trigger not firing:
- Check the
TZenvironment variable — cron expressions are evaluated in this timezone. - Verify the expression at crontab.guru.
Device triggers not firing:
- Ensure
DEVICE_REGISTRY_ENABLED=trueis set. Without it,device_state,device_joined, anddevice_lefttriggers are silently skipped at startup (a warning is logged).
Web UI not accessible¶
- Enable it:
WEB_UI_ENABLED=true - Check the port:
HTTP_PORTmust be non-zero (default: 8080). Set to0disables the entire HTTP server. - Check the path: Default is
/status— navigate tohttp://localhost:8080/status. - Authentication: If
HTTP_TOKENis set, you must log in at/status/loginfirst. - Docker: Ensure the port is mapped (
-p 8080:8080).
HomeKit pairing fails¶
- Port conflict: The HomeKit bridge binds to port 47128 by default. Ensure nothing else is using it.
- mDNS on Docker/Linux: HAP-NodeJS uses mDNS (Bonjour) for discovery. In Docker, use
network_mode: hostor ensure mDNS is properly bridged. - Pairing code: Use the PIN code configured in the
HomekitServiceoptions (format:"XXX-XX-XXX"). - Reset pairing: Delete the
homekit-persist/directory and restart to clear stale pairing data.
Debugging¶
Enable debug logging¶
At debug level, the engine logs:
- Every MQTT message received and dispatched
- State changes with old and new values
- HTTP requests with URLs (sensitive params masked) and response times
- Automation lifecycle events (register, start, stop)
- Service plugin lifecycle events
Query the log buffer¶
The engine keeps the last 2500 log entries in memory. Query them via the API:
# All logs
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/logs
# Filter by automation
curl http://localhost:8080/api/logs?automation=motion-light&limit=50
# Filter by level (40 = warn and above)
curl http://localhost:8080/api/logs?level=40
CLI inspection¶
# Check if the engine is running and healthy
ts-ha config list
ts-ha state list
# View live logs with follow mode
ts-ha logs -f
# Filter logs by automation
ts-ha logs --automation motion-light --level warn
# Interactive dashboard
ts-ha dashboard
Manual trigger¶
Test an automation without waiting for its trigger:
# Via CLI
ts-ha automations trigger motion-light --type mqtt \
--topic "zigbee2mqtt/test" --payload '{"occupancy": true}'
# Via API
curl -X POST http://localhost:8080/api/automations/motion-light/trigger \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "mqtt", "topic": "manual/test", "payload": {"occupancy": true}}'
Performance tips¶
MQTT subscription efficiency¶
- Prefer exact topics over wildcard patterns. Exact-match subscriptions use O(1)
Maplookup; wildcard subscriptions require linear scanning. - Use
filterfunctions to discard irrelevant messages early, beforeexecute()is called.
State listener cleanup¶
-
Always remove event listeners in
onStop()to prevent memory leaks: -
The engine warns when more than 10 listeners are registered for a single state key — this usually indicates a leak.
Timer cleanup¶
Always clear timers in onStop():
private timer: ReturnType<typeof setTimeout> | null = null;
async onStop() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
Known limitations¶
- Single instance only — the engine is not designed for horizontal scaling. Running multiple instances against the same MQTT broker will cause duplicate message processing.
- No hot-reload of automations — adding or modifying automation files requires an engine restart (use
bun run devfor file-watch restart during development). - State persistence is not atomic during operation — state is only saved on shutdown. A crash or
kill -9may lose recent state changes. For critical state, consider writing to the state file more frequently by callingthis.state.save()manually (though this is not part of the public API contract). - Web UI is not a full SCADA system — it provides monitoring and basic control. For complex dashboards, use Grafana or Home Assistant alongside this framework.
- HomeKit accessory limit — HAP-NodeJS supports up to ~150 accessories per bridge. For larger setups, consider running multiple bridges on different ports.
FAQ¶
Can I use Node.js instead of Bun?¶
The framework is built for and tested on Bun. It uses Bun-specific APIs (Bun.serve(), Bun.file(), Bun.build()). Node.js is not supported.
Can I use this without Zigbee2MQTT?¶
Yes. The MQTT service connects to any MQTT broker. You can subscribe to arbitrary topics and publish to any topic — Zigbee2MQTT is not required. The publishToDevice() helper and device registry are Zigbee2MQTT-specific, but the core trigger system works with any MQTT data source.
How do I run multiple automation directories?¶
The engine supports a single automationsDir. To organise automations in subdirectories, set AUTOMATIONS_RECURSIVE=true or pass recursive: true to createEngine().
Can I register automations programmatically?¶
Yes. Instead of file-based discovery, instantiate automations and register them manually:
const engine = createEngine({ automationsDir: "./empty-dir" });
await engine.start();
await engine.manager.register(new MyAutomation());
How do I update to a new version?¶
Check the release notes for breaking changes. The package follows semantic versioning.
Where are logs stored?¶
Logs are written to stdout only. There is no built-in file logging. Use your OS or container runtime to capture stdout to files if needed. The in-memory log buffer (2500 entries) is for the API and web UI — it does not persist across restarts.
How do I add authentication to webhooks?¶
Webhook endpoints (/webhook/*) are unauthenticated by default. If you need authentication, validate the request inside your automation's execute() method:
async execute(context: TriggerContext): Promise<void> {
if (context.type !== "webhook") return;
const token = context.headers["x-webhook-secret"];
if (token !== "my-secret") {
this.logger.warn("Unauthorized webhook attempt");
return;
}
// ... handle the webhook
}
Can I use this with Home Assistant?¶
Yes, as a complement. Both can connect to the same MQTT broker. This framework handles automations in TypeScript while Home Assistant handles its own automations, dashboards, and integrations. They coexist without conflict as long as they don't send conflicting commands to the same devices simultaneously.