import type { Logger } from "pino"; import type { Config } from "../config.js"; import type { NotificationOptions } from "../types/notification.js"; import type { ZigbeeDevice } from "../types/zigbee/bridge.js"; import type { HttpClient } from "./http/http-client.js"; import type { MqttService } from "./mqtt/mqtt-service.js"; import type { ServiceRegistry } from "./services/service-registry.js"; import type { StateManager } from "./state/state-manager.js"; import type { DeviceRegistry } from "./zigbee/device-registry.js"; /** * Trigger types for automations. * * - mqtt: Fires when a message is received on the given topic. * Use the Zigbee2MQTT friendly name as the topic suffix. * Supports MQTT wildcards (+ for single level, # for multi level). * An optional `filter` function can narrow which payloads trigger execution. * * - cron: Fires on a cron schedule (e.g. "0 7 * * *" = daily at 7 AM). * * - state: Fires when a state key changes value. Use this to react to state * changes made by other automations. * * - webhook: Fires when an HTTP request is received on the configured path. * The endpoint is `POST /webhook/` on the health server port. * Supports optional method restriction (default: POST only). * * - device_state: Fires when a tracked Zigbee2MQTT device's state changes. * Requires `DEVICE_REGISTRY_ENABLED=true`. The trigger receives the * full merged device state and device metadata. An optional filter * function can narrow which state changes trigger execution. * * - device_joined: Fires when a Zigbee device joins the network (is added to the * device registry). Requires `DEVICE_REGISTRY_ENABLED=true`. * Optionally scoped to a specific friendly name. * * - device_left: Fires when a Zigbee device leaves the network (is removed from * the device registry). Requires `DEVICE_REGISTRY_ENABLED=true`. * Optionally scoped to a specific friendly name. */ export type Trigger = { type: "mqtt"; topic: string; filter?: (payload: Record) => boolean; } | { type: "cron"; expression: string; } | { type: "state"; /** The state key to watch. */ key: string; /** Optional filter — return true to trigger, false to ignore. */ filter?: (newValue: unknown, oldValue: unknown) => boolean; } | { type: "webhook"; /** * URL path for the webhook (without leading slash). * The full endpoint will be `POST /webhook/`. * Example: "deploy" → POST /webhook/deploy */ path: string; /** HTTP methods to accept (default: ["POST"]). */ methods?: ("GET" | "POST" | "PUT" | "DELETE")[]; } | { type: "device_state"; /** Zigbee2MQTT friendly name of the device to watch. */ friendlyName: string; /** * Optional filter — return true to trigger, false to ignore. * Receives the full merged device state and the device metadata. */ filter?: (state: Record, device: ZigbeeDevice) => boolean; } | { type: "device_joined"; /** * If provided, only fires when this specific device joins. * Omit to fire whenever any device joins the network. */ friendlyName?: string; } | { type: "device_left"; /** * If provided, only fires when this specific device leaves. * Omit to fire whenever any device leaves the network. */ friendlyName?: string; }; /** * Context passed to an automation's execute method. * * For MQTT triggers, contains the topic that fired and the parsed payload. * For cron triggers, contains the cron expression and the time it fired. * For state triggers, contains the key, new value, and old value. * For webhook triggers, contains the path, HTTP method, headers, query, and body. */ export type TriggerContext = { type: "mqtt"; topic: string; payload: Record; } | { type: "cron"; expression: string; firedAt: Date; } | { type: "state"; key: string; newValue: unknown; oldValue: unknown; } | { type: "webhook"; /** The webhook path that was called. */ path: string; /** The HTTP method used. */ method: string; /** Request headers. */ headers: Record; /** URL query parameters. */ query: Record; /** Parsed request body (JSON object, or raw string). */ body: unknown; } | { type: "device_state"; /** The friendly name of the device whose state changed. */ friendlyName: string; /** Full merged device state at the time the trigger fired. */ state: Record; /** The ZigbeeDevice metadata from the registry. */ device: ZigbeeDevice; } | { type: "device_joined"; /** The device that joined the network. */ device: ZigbeeDevice; } | { type: "device_left"; /** The device that left the network. */ device: ZigbeeDevice; }; /** * Dependencies injected into every automation by the AutomationManager. * * Passed as a single context object to `_inject()` so that adding new * optional services in the future is a non-breaking extension. * * Optional services (notifications, weather, shelly, nanoleaf, and any custom * services) are available via `services.get(key)`. */ export interface AutomationContext { mqtt: MqttService; http: HttpClient; state: StateManager; logger: Logger; config: Config; /** * `null` when `DEVICE_REGISTRY_ENABLED` is `false` (the default). * Enable via config to get automatic Zigbee2MQTT device discovery and state tracking. */ deviceRegistry: DeviceRegistry | null; /** * The shared service registry. * * Use this to access any optional service registered with the engine, * including `shelly`, `nanoleaf`, or any custom service: * * ```ts * const shelly = this.services.get("shelly"); * if (shelly) await shelly.turnOn("plug"); * ``` */ services: ServiceRegistry; } /** * Abstract base class for all automations. * * To create a new automation: * 1. Create a new file in src/automations/ * 2. Export a class that extends Automation as the default export * 3. Implement `name`, `triggers`, and `execute` * * The base class provides access to: * - `this.mqtt` - Publish messages and interact with Zigbee2MQTT devices * - `this.notify` - Send push notifications (no-op if no NotificationService is configured) * - `this.state` - Shared state manager (get/set/delete, persisted across restarts) * - `this.http` - Make outbound HTTP requests * - `this.logger` - Structured logger (child logger scoped to this automation) * - `this.config` - Application configuration * - `this.services` - Generic registry for any optional service (shelly, nanoleaf, custom…) * - `this.require` - Non-null retrieval for services declared in `requiredServices` * * @example * ```ts * import { Automation, type Trigger, type TriggerContext } from "../core/automation.js"; * import type { OccupancyPayload } from "../types/zigbee/index.js"; * * export default class MotionLight extends Automation { * name = "motion-light"; * * triggers: Trigger[] = [ * { * type: "mqtt", * topic: "zigbee2mqtt/hallway_sensor", * filter: (p) => (p as OccupancyPayload).occupancy === true, * }, * ]; * * async execute(context: TriggerContext): Promise { * this.mqtt.publishToDevice("hallway_light", { state: "ON" }); * } * } * ``` */ export declare abstract class Automation { /** Unique name identifying this automation. Used in logs and job IDs. */ abstract readonly name: string; /** The trigger(s) that cause this automation to execute. */ abstract readonly triggers: Trigger[]; /** * Declare services that must be present when this automation starts. * * The `AutomationManager` checks every key listed here at registration time * (before `onStart` is called) and throws a descriptive error if any of them * are missing from the registry. This gives you fail-fast startup validation * instead of silent `null` surprises at the first trigger. * * Inside `execute()` you can then retrieve those services safely with * `this.require(key)`, which is a non-null assertion (no `if` guard needed). * * Optional services that are NOT listed here continue to use * `this.services.get(key)` with the usual null-check. * * **Important:** declare the value with `as const` so TypeScript infers * a readonly string-literal tuple rather than the wider `string[]` type: * * @example * ```ts * // "as const" gives you string-literal inference ("shelly"), not just string * readonly requiredServices = ["shelly"] as const; * * async execute(): Promise { * const shelly = this.require("shelly"); * await shelly.turnOff("tv_plug"); * } * ``` */ readonly requiredServices?: readonly string[]; protected mqtt: MqttService; protected http: HttpClient; protected state: StateManager; protected logger: Logger; protected config: Config; private deviceRegistryService; private servicesRegistry; /** * Called by the AutomationManager to inject dependencies. * Do not call this yourself. */ _inject(context: AutomationContext): void; /** * The shared service registry. * * Use this to access any optional service registered with the engine, * including custom services not exposed as named getters. * * Three retrieval styles are available — choose the one that fits: * * **`get`** — nullable; you handle the absent case: * ```ts * const svc = this.services.get("my-service"); * if (!svc) return; * await svc.doSomething(); * ``` * * **`getOrThrow`** — throws if not registered (use for required services): * ```ts * const svc = this.services.getOrThrow("my-service"); * await svc.doSomething(); * ``` * * **`use`** — callback wrapper; no-ops when absent (best for one-liners): * ```ts * await this.services.use("my-service", (s) => s.doSomething()); * ``` * * For services declared in `requiredServices`, prefer `this.require(key)` * which is validated at startup and has a non-null return type. */ protected get services(): ServiceRegistry; /** * Retrieve a service that was declared in `requiredServices`. * * The `AutomationManager` already verified at startup that this service is * registered, so this method never returns `null` — no null-check needed. * * Calling `require()` for a key that is **not** listed in `requiredServices` * is allowed but will throw at runtime if the service is absent, identical * to `this.services.getOrThrow(key)`. * * @example * ```ts * readonly requiredServices = ["shelly"] as const; * * async execute(): Promise { * const shelly = this.require("shelly"); * await shelly.turnOff("tv_plug"); * } * ``` */ protected require(key: string): T; /** * Send a push notification via the configured notification service. * * If no notification service is configured on the engine, this logs a * warning and does nothing. * * @example * ```ts * await this.notify({ * title: "Door opened", * message: "Front door was opened at 3 AM", * priority: "urgent", * tags: ["warning", "door"], * }); * ``` */ protected notify(options: NotificationOptions): Promise; /** * The Zigbee2MQTT device registry, or `null` if disabled (`DEVICE_REGISTRY_ENABLED=false`). * * Always null-check before use: * * @example * ```ts * const registry = this.deviceRegistry; * if (!registry) return; * const device = registry.getDevice("my_sensor"); * const state = registry.getDeviceState("my_sensor"); * ``` */ protected get deviceRegistry(): DeviceRegistry | null; /** * The automation logic. Called whenever one of the defined triggers fires. * * @param context Information about which trigger fired and its data. */ abstract execute(context: TriggerContext): Promise; /** * Optional lifecycle hook called when the automation is registered. * Override this to perform setup work (e.g. initialize state). */ onStart(): Promise; /** * Optional lifecycle hook called when the automation is stopped. * Override this to perform cleanup work (e.g. clear timers). */ onStop(): Promise; } //# sourceMappingURL=automation.d.ts.map