Writing Automations¶
Every automation is a TypeScript class that extends Automation. It defines a unique name, one or more triggers, and an execute() method that runs when any trigger fires.
import { Automation, type Trigger, type TriggerContext } from "ts-home-automation";
export default class MyAutomation extends Automation {
readonly name = "my-automation";
readonly triggers: Trigger[] = [/* ... */];
async execute(context: TriggerContext): Promise<void> {
// your logic here
}
}
Files in the configured automationsDir are discovered automatically at startup — just export a default class.
Trigger types¶
MQTT trigger¶
Fires when a matching MQTT message arrives. Topics support + (one level) and # (all remaining levels) wildcards.
{
type: "mqtt",
topic: "zigbee2mqtt/hallway_sensor",
// optional: only trigger when this returns true
filter: (payload) => (payload as OccupancyPayload).occupancy === true,
}
The context in execute() provides:
context.type // "mqtt"
context.topic // the matched topic string
context.payload // parsed JSON payload as Record<string, unknown>
Cron trigger¶
Fires on a schedule using standard cron syntax. The TZ environment variable controls the timezone.
The context provides:
context.type // "cron"
context.expression // the cron expression string
context.firedAt // Date — when the job fired
State trigger¶
Fires when a state key changes. Any automation can set state with this.state.set().
{
type: "state",
key: "night_mode",
// optional filter — both newValue and oldValue are available
filter: (newValue, oldValue) => newValue === true && oldValue !== true,
}
The context provides:
context.type // "state"
context.key // the state key
context.newValue // the new value
context.oldValue // the previous value
Webhook trigger¶
Fires when an HTTP request hits POST /webhook/<path> (or another method if configured). Requires the HTTP server to be enabled (HTTP_PORT != 0).
{
type: "webhook",
path: "deploy", // → POST /webhook/deploy
methods: ["POST"], // optional, defaults to ["POST"]
}
The context provides:
context.type // "webhook"
context.path // the path segment
context.method // HTTP method
context.headers // request headers
context.query // query string params
context.body // parsed request body
Device state trigger¶
Fires when a tracked Zigbee2MQTT device's state changes. Requires DEVICE_REGISTRY_ENABLED=true. The trigger receives the full merged device state — if a light sends only brightness, that value is merged on top of the previously known state so context.state always reflects the complete picture.
{
type: "device_state",
friendlyName: "living_room_bulb",
// Optional — only fire when this returns true
filter: (state, device) => state.state === "ON",
}
The context provides:
context.type // "device_state"
context.friendlyName // "living_room_bulb"
context.state // full merged state: { state: "ON", brightness: 200, ... }
context.device // ZigbeeDevice — type, ieee_address, definition, ...
Device joined trigger¶
Fires when a new device joins the Zigbee network. Requires DEVICE_REGISTRY_ENABLED=true. Optionally scoped to a specific friendlyName; omit to fire for any joining device.
{ type: "device_joined" } // any device
{ type: "device_joined", friendlyName: "new_sensor" } // specific device only
The context provides:
Device left trigger¶
Fires when a device leaves the Zigbee network. Requires DEVICE_REGISTRY_ENABLED=true. Same scoping options as device_joined.
The context provides:
When the registry is disabled:
device_state,device_joined, anddevice_lefttriggers are skipped with a warning at startup. The automation still registers and its other triggers remain active.
See Device Registry for the full feature guide.
Multiple triggers¶
An automation can declare as many triggers as needed. The context.type discriminant tells you which one fired:
readonly triggers: Trigger[] = [
{ type: "mqtt", topic: "zigbee2mqtt/button" },
{ type: "cron", expression: "0 22 * * *" },
];
async execute(context: TriggerContext): Promise<void> {
if (context.type === "mqtt") {
// button pressed
} else if (context.type === "cron") {
// scheduled run
}
}
Available services¶
Inside execute(), onStart(), and onStop() the following are available on this:
MQTT¶
this.mqtt.publishToDevice(name, payload)
// Publishes to zigbee2mqtt/<name>/set
this.mqtt.publish(topic, payload)
// Publish to any arbitrary MQTT topic
Accessing optional services (Shelly, Nanoleaf, custom…)¶
Optional services registered with the engine (e.g. shelly, nanoleaf, or any custom service) are accessed through this.services. Import the service type at the top of your automation file to use it in type parameters:
import type { ShellyService } from "ts-home-automation";
import type { NanoleafService } from "ts-home-automation";
Four retrieval styles are available — choose the one that fits your use case:
get<T>(key) — returns null when absent; you handle the missing case:
const shelly = this.services.get<ShellyService>("shelly");
if (!shelly) return;
await shelly.turnOn("living_room_plug");
getOrThrow<T>(key) — throws at runtime if the service was not registered (use when you know it will always be present):
use<T>(key, fn) — callback wrapper that no-ops silently when absent (best for one-liners):
this.require<T>(key) — non-null retrieval for services declared in requiredServices (validated at startup):
export default class MyAutomation extends Automation {
readonly requiredServices = ["shelly"] as const;
async execute(): Promise<void> {
const shelly = this.require<ShellyService>("shelly"); // never null
await shelly.turnOn("living_room_plug");
}
}
Shelly devices¶
Retrieve the service and call any method on it:
const shelly = this.services.get<ShellyService>("shelly");
if (!shelly) return;
// Switch control
await shelly.turnOn(name)
await shelly.turnOff(name)
await shelly.toggle(name)
await shelly.isOn(name) // → Promise<boolean>
await shelly.getPower(name) // → Promise<number> (Watts)
await shelly.getStatus(name) // → full switch status
// Cover / shutter control
await shelly.coverOpen(name)
await shelly.coverClose(name)
await shelly.coverStop(name)
await shelly.coverGoToPosition(name, 50) // 0–100%
Devices must be registered first. See Shelly for the full method list including cover status and relative movement.
Nanoleaf¶
const nanoleaf = this.services.get<NanoleafService>("nanoleaf");
if (!nanoleaf) return;
await nanoleaf.turnOn(name)
await nanoleaf.setBrightness(name, 80, 2) // 80%, 2s transition
await nanoleaf.setColor(name, 120, 100) // hue, saturation
await nanoleaf.setEffect(name, "Aurora")
See Nanoleaf for pairing and full method list.
Weather¶
Requires configuration. Returns
nullwhen noWeatherServiceis configured. Always null-check before use:
import type { WeatherService } from "ts-home-automation";
const weather = this.services.get<WeatherService>("weather");
if (!weather) {
this.logger.warn("Weather service not configured");
return;
}
const current = await weather.getCurrent();
// current.temperature, current.condition, current.wind.speed, ...
const forecast = await weather.getForecast(3);
// forecast[0].tempHigh, forecast[0].precipitationChance, ...
See Weather for setup.
Notifications¶
await this.notify({
title: "Front door opened",
message: "Nobody should be home",
priority: "urgent",
tags: ["warning"],
});
If no notification service is configured, this.notify() logs a warning and does nothing. See Notifications.
State¶
this.state.set<boolean>("night_mode", true)
this.state.get<boolean>("night_mode", false) // second arg is default
this.state.has("night_mode")
this.state.delete("night_mode")
Setting state fires state triggers in other automations. See State Management.
HTTP client¶
await this.http.get("https://api.example.com/data")
await this.http.post("https://api.example.com/action", { key: "value" })
await this.http.put(url, body)
await this.http.request(url, { method: "PATCH", body: "..." })
Device Registry¶
Requires
DEVICE_REGISTRY_ENABLED=true.this.deviceRegistryreturnsnullwhen disabled. Always null-check before use.
const registry = this.deviceRegistry;
if (!registry) return;
// Query tracked devices
registry.getDevices() // ZigbeeDevice[]
registry.getDevice("living_room_bulb") // ZigbeeDevice | undefined
registry.hasDevice("living_room_bulb") // boolean
// Current merged state for a device
registry.getDeviceState("living_room_bulb") // Record<string, unknown> | undefined
// Human-readable name (from DeviceNiceNames mapping)
registry.getNiceName("living_room_bulb") // string
// Listen for state changes
registry.onDeviceStateChange("living_room_bulb", (state, prev) => {
this.logger.info({ brightness: state.brightness }, "Bulb changed");
});
See Device Registry for the complete API and nice-name configuration.
Logger and config¶
this.logger.info({ sensor: "hallway" }, "Motion detected")
this.logger.warn("Unexpected state")
this.config // full application Config object
Lifecycle hooks¶
Override onStart() and onStop() for setup and teardown. Both have empty default implementations.
async onStart(): Promise<void> {
// Called when the automation is registered at engine startup.
// Good for: initialising state, setting up timers.
this.state.set("lights_on", false);
}
async onStop(): Promise<void> {
// Called on engine shutdown.
// Good for: clearing timers, releasing resources.
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
Recommended patterns¶
Named private constants¶
export default class MotionLight extends Automation {
readonly name = "motion-light";
private readonly SENSOR_TOPIC = "zigbee2mqtt/hallway_sensor";
private readonly LIGHT_NAME = "hallway_light";
private readonly TIMEOUT_MS = 5 * 60 * 1000;
private timer: ReturnType<typeof setTimeout> | null = null;
// ...
}
State-scoped keys¶
Prefix state keys with the automation name to avoid collisions:
this.state.set("motion-light:lights_on", true);
this.state.get<boolean>("motion-light:lights_on", false);
Error handling¶
Log errors and continue — never re-throw non-critical failures: