import type { Clock } from '../ports/clock.js'; import type { CommandRunner } from '../ports/command-runner.js'; import type { Logger } from '../ports/logger.js'; import type { CommandSpec } from '../command/command-spec.js'; import { DoorState } from './door-state.js'; import type { GarageStateParser } from '../parsers/garage-state-parser.js'; export type DoorTarget = 'open' | 'closed'; export type AutoCloseMode = 'execute' | 'simulated'; export interface GarageTiming { readonly openTravelTimeMs: number; readonly closeTravelTimeMs: number; readonly autoCloseTimeoutMs: number; readonly autoCloseMode: AutoCloseMode; readonly statePollIntervalMs: number; /** * Optional fast-poll cadence used while the door is in a transient state (Opening/Closing). * 0 = use `statePollIntervalMs` regardless of state. Default in the schema: 1000ms. * Lets HomeKit see real progress without spamming SSH while the door is parked. */ readonly transientPollIntervalMs?: number; /** * Grace period to wait after issuing an open/close command before the next state * poll fires. Prevents the poll from racing the remote script: a freshly-issued * open hasn't had time to write OPENING to the state file yet, and an early poll * would read the previous CLOSED value and incorrectly drift the orchestrator * back. 0 = no grace, fall back to the regular cadence. Default: 5000ms. */ readonly postCommandPollDelayMs?: number; } export interface GarageOrchestratorConfig { readonly runner: CommandRunner; readonly clock: Clock; readonly logger: Logger; readonly onChange: (current: DoorState, target: DoorTarget) => void; readonly openCommand: CommandSpec; readonly closeCommand: CommandSpec; readonly stateCommand?: CommandSpec | undefined; readonly stateParser?: GarageStateParser | undefined; readonly timing: GarageTiming; readonly initialState?: DoorState | undefined; } export declare class GarageOrchestrator { private readonly cfg; private currentState; private targetState; private lastStable; private settleHandle; private autoCloseHandle; private autoCloseSettleHandle; private pollHandle; private stopped; /** * Watchdog timestamp: when auto-close fired (in ms since epoch via Clock). * The next state poll that observes Open while we expect to be closing falls back * to running the close command. Cleared on success (Closed observed) or stop(). */ private autoCloseFiredAt; /** * Count of consecutive state-poll failures (SSH-level, not parse-level). * Drives exponential backoff in `nextPollDelayMs`. Reset to 0 on the first * successful state command. */ private consecutiveFailures; /** * Serialises pollOnce. If a poll is already in flight when another is * triggered (eg. the auto-close watchdog poll firing while the regular * loop is mid-call), the second call is a no-op. Stops parallel polls * from piling up against a slow remote and spamming the log on timeout. */ private pollInFlight; /** Watchdog poll timer (one-shot, scheduled per auto-close cycle). */ private watchdogPollHandle; constructor(cfg: GarageOrchestratorConfig); current(): DoorState; target(): DoorTarget; start(): void; stop(): void; setTarget(target: DoorTarget): Promise; private requestOpen; private requestClose; private cancelPendingPoll; /** * Push the next state poll out by `postCommandPollDelayMs`. Called right after * a successful open/close exec so the immediate transient poll cadence doesn't * race the remote script writing the new state to its state file. */ private deferPollAfterCommand; private rollbackToLastStable; private scheduleSettle; private scheduleAutoClose; /** * Force a single state poll once `closeTravelTimeMs` has elapsed since auto-close * fired. Composes with the regular poll loop: if polling is already running, this * is just one extra check; if polling was disabled, it provides the watchdog signal. * The watchdog action (firing close as fallback) lives in `checkAutoCloseWatchdog`. */ private scheduleAutoCloseWatchdogPoll; private pollIntervalForCurrent; /** * Cadence for the *next* poll. While SSH is healthy this matches the * configured cadence. After consecutive failures (typically because the * persistent SSH connection is poisoned and every exec is failing fast), * the delay grows exponentially up to `POLL_BACKOFF_MAX_MS` — capping log * noise and giving the connection time to recycle before we retry. */ private nextPollDelayMs; private scheduleNextPoll; /** Invalidates any pending poll and (re)schedules one with the cadence for the current state. */ private rescheduleNextPoll; private pollOnce; /** * Auto-close watchdog: if auto-close fired and the gate is observed to still be * Open after enough time has elapsed for the close to have completed, run the * close command as fallback. Works in both `execute` and `simulated` modes. */ private checkAutoCloseWatchdog; private reconcile; private transitionTo; private cancelMotionTimers; private cancelAllTimers; private logCommandFailure; }