import type { Logger } from "pino"; import type { Config } from "../config.js"; import { Automation, type TriggerContext } from "./automation.js"; import type { HttpClient } from "./http/http-client.js"; import type { HttpServer } from "./http/http-server.js"; import type { MqttService } from "./mqtt/mqtt-service.js"; import { type ExecutionRecord, ExecutionRecorder } from "./observability/execution-recorder.js"; import type { CronScheduler } from "./scheduling/cron-scheduler.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"; /** * Reserved-namespace prefix under which each automation's enabled preference * is stored, keyed by automation name (design.md D20, D30). */ export declare const AUTOMATION_ENABLED_PREFIX = "$internal:automation-enabled:"; /** Returns the reserved state key storing `name`'s enabled preference. */ export declare function automationEnabledKey(name: string): string; /** Result of a source-retrieval request, keyed by automation name only. */ export type AutomationSourceResult = { status: "found"; source: string; } | { status: "not_found"; } | { status: "error"; message: string; }; /** Result of a `start(name)` request. */ export type AutomationStartResult = { status: "started"; } | { status: "not_found"; } | { status: "error"; message: string; }; /** Result of a manual `triggerAutomation()` request. */ export type AutomationTriggerResult = "executed" | "not_found" | "disabled"; /** * A declared required service and whether it is currently registered * (design.md D11; task 8.5). */ export interface RequiredServiceStatus { name: string; registered: boolean; } /** * An automation's relationships, split into what is declared (and therefore * complete, without ever having run) and what has been observed at runtime * (and is therefore partial, growing only with use — design.md D11, R12; * task 8.5). */ export interface AutomationRelationships { declared: { requiredServices: RequiredServiceStatus[]; /** Device names referenced by device and MQTT triggers. */ relatedDevices: string[]; /** State keys watched by state triggers. */ watchedStateKeys: string[]; }; observed: { /** State keys observed being written, oldest-retained first. */ writtenStateKeys: string[]; /** `true` once the retained set has ever exceeded its bound (design.md R15). */ truncated: boolean; }; } /** * Discovers, registers, and manages the lifecycle of all automations. * * On startup it scans the automations directory, dynamically imports each file, * and wires up the declared triggers (MQTT subscriptions, cron jobs, * state change listeners, and webhook endpoints). */ export declare class AutomationManager { private readonly mqtt; private readonly cron; private readonly http; private readonly stateManager; private readonly httpServer; private readonly config; private readonly logger; private readonly services; private readonly deviceRegistry; private records; /** Track MQTT handlers so we can cleanly unsubscribe on shutdown. */ private mqttHandlers; /** Track state handlers so we can cleanly unsubscribe on shutdown. */ private stateHandlers; /** Track webhook paths so we can cleanly remove on shutdown. */ private webhookPaths; /** Track device state handlers so we can cleanly unsubscribe on shutdown. */ private deviceStateHandlers; /** Track device-joined handlers so we can cleanly unsubscribe on shutdown. */ private deviceJoinedHandlers; /** Track device-left handlers so we can cleanly unsubscribe on shutdown. */ private deviceLeftHandlers; /** * Execution history, observed state writes, and completion broadcasts for * every automation (design.md D11). Exposed for the observability * endpoints (task 8.6), the realtime event stream (task 8.7), and the * Prometheus counters (task 8.8) via {@link getExecutionRecorder}. */ private readonly executionRecorder; constructor(mqtt: MqttService, cron: CronScheduler, http: HttpClient, stateManager: StateManager, httpServer: HttpServer | null, config: Config, logger: Logger, services: ServiceRegistry, deviceRegistry: DeviceRegistry | null, executionRecorder?: ExecutionRecorder); /** * The shared execution recorder, for wiring the realtime event stream's * `automation_execution` category and the Prometheus execution/failure * counters (design.md D18; tasks 8.7, 8.8). */ getExecutionRecorder(): ExecutionRecorder; /** * Runs `automation.execute(context)` inside the execution context, * recording its outcome (design.md D11; tasks 8.1, 8.3). Shared by every * trigger dispatch path and by `triggerAutomation()`, so every run — MQTT, * cron, state, webhook, device, and manual — is recorded identically. */ private runExecution; /** * Discover and register all automations from the given directory. * Each file should have a default export that is a class extending Automation. * * A discovered automation whose stored preference is disabled is * registered — listed by the query API — but has no triggers wired and no * `onStart()` invoked (design.md D4, D20). * * @param automationsDir Path to the automations directory * @param recursive Whether to scan subdirectories recursively (default: false) */ discoverAndRegister(automationsDir: string, recursive?: boolean): Promise; /** * Discards stored enabled preferences naming no discovered automation, * logging each at warning level (design.md D30). Callers MUST only invoke * this when discovery yielded at least one automation — an empty scan is * indistinguishable from an unreadable directory, and reaping on it would * re-enable every deliberately disabled automation on the next start. */ private reapStaleEnabledPreferences; /** * Register a single automation instance. * Injects dependencies, wires up triggers, and calls onStart. * * A name that names an existing but currently disabled record is not a * duplicate — the record is replaced in place (task 3.6). */ register(automation: Automation, options?: { filePath?: string; }): Promise; /** * Disable an automation by name: deregisters every trigger and invokes * `onStop()`, then discards the live instance (design.md D4). Persists the * disabled preference under the reserved namespace (design.md D20). * * A no-op, without side effects, when the automation is already disabled. */ stop(name: string): Promise<"stopped" | "not_found">; /** * Enable a previously disabled automation by name: re-imports its source * file with a cache-busting suffix, constructs a fresh instance, validates * required services, wires triggers in declared order, and invokes * `onStart()` (design.md D4, D5). * * A no-op, without side effects, when the automation is already enabled. * A failed enable unwinds any partially wired triggers and leaves the * automation disabled, reporting a descriptive error (task 3.6). */ start(name: string): Promise; /** * Wire every declared trigger for `automation` and invoke `onStart()`. * Shared by `register()` and `start()`. * * If `onStart()` throws, any triggers wired during this call are unwound * and `onStop()` is invoked best-effort before the error is rethrown to * the caller — the caller decides what "stopped" means for its record. */ private wireAndStart; /** * Deregister every trigger wired for `automation` — MQTT subscriptions, * cron jobs, state listeners, webhook routes, and device joined/left/state * listeners — then invoke `onStop()` best-effort. Shared by the * onStart-failure rollback, `stop()`, and `stopAll()` (tasks 3.2, 3.3). * * Errors from `onStop()` are logged and swallowed; the automation is * considered stopped regardless (design.md R18/D4). */ private unwireAndStop; /** * Throws a descriptive error, before any wiring occurs, when `automation` * declares a webhook trigger whose path is already claimed by a different, * currently active automation — leaving the existing route intact * (design.md D5; task 3.7). */ private assertNoWebhookConflict; private validateRequiredServices; private buildContext; /** * Gracefully stop all automations. * Unsubscribes MQTT handlers, state handlers, webhook routes, stops cron jobs, and calls onStop. */ stopAll(): Promise; /** * List all registered automations with their trigger summaries. */ listAutomations(): { name: string; enabled: boolean; triggers: { type: string; [key: string]: unknown; }[]; }[]; /** * Get details for a single automation by name. * Returns null if not found. */ getAutomation(name: string): { name: string; enabled: boolean; triggers: { type: string; [key: string]: unknown; }[]; } | null; /** * Return `name`'s retained execution history, most recent first, or * `null` for an unknown automation — distinct from an automation that is * known but has never run, which returns an empty array (design.md D11; * task 8.3). */ getHistory(name: string): ExecutionRecord[] | null; /** * Return `name`'s declared and observed relationships, or `null` for an * unknown automation (design.md D11; task 8.5). * * Declared relationships — required services (with current registration * status), devices referenced by device and MQTT triggers, and state keys * watched by state triggers — are derived from the automation's * declarations, so they are reported in full even if it has never run. * Observed writes are runtime attribution and therefore partial, growing * only with use (design.md R12). */ getRelationships(name: string): AutomationRelationships | null; /** * Return the current contents of the file `name` was loaded from, * addressed strictly by automation name — a caller-supplied path is never * accepted (design.md D5; task 3.10). */ getSource(name: string): Promise; /** * Serialize an automation's triggers for the debug API. */ private serializeAutomation; /** * Manually trigger an automation with a synthetic context. * Used by the debug API for testing automations without real device events. * * A disabled automation is refused with `"disabled"` rather than executed * — disabling deregisters every trigger and discards the instance, so * running it on demand would act on the house through an automation the * system reports as off (design.md D27; task 3.8c). This is distinct from * `"not_found"`, so an operator can tell "switched off" from "gone". * * @param name Automation name * @param context The trigger context to pass to execute() */ triggerAutomation(name: string, context: TriggerContext): Promise; } //# sourceMappingURL=automation-manager.d.ts.map