@ajclarkson/homerun 0.0.1-edge.0131fc6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/dist/scripts/generate-ha-services.d.ts +16 -0
  4. package/dist/scripts/generate-ha-services.js +111 -0
  5. package/dist/scripts/generate-ha-types.d.ts +8 -0
  6. package/dist/scripts/generate-ha-types.js +75 -0
  7. package/dist/src/framework/action-runtime.d.ts +39 -0
  8. package/dist/src/framework/action-runtime.js +193 -0
  9. package/dist/src/framework/api-server.d.ts +35 -0
  10. package/dist/src/framework/api-server.js +132 -0
  11. package/dist/src/framework/config.d.ts +34 -0
  12. package/dist/src/framework/config.js +93 -0
  13. package/dist/src/framework/event-publisher.d.ts +65 -0
  14. package/dist/src/framework/event-publisher.js +85 -0
  15. package/dist/src/framework/ha-client.d.ts +85 -0
  16. package/dist/src/framework/ha-client.js +201 -0
  17. package/dist/src/framework/hot-reload.d.ts +9 -0
  18. package/dist/src/framework/hot-reload.js +88 -0
  19. package/dist/src/framework/metrics-prom.d.ts +18 -0
  20. package/dist/src/framework/metrics-prom.js +93 -0
  21. package/dist/src/framework/metrics.d.ts +10 -0
  22. package/dist/src/framework/metrics.js +5 -0
  23. package/dist/src/framework/pipeline.d.ts +14 -0
  24. package/dist/src/framework/pipeline.js +85 -0
  25. package/dist/src/framework/registry.d.ts +10 -0
  26. package/dist/src/framework/registry.js +24 -0
  27. package/dist/src/framework/scheduler.d.ts +13 -0
  28. package/dist/src/framework/scheduler.js +55 -0
  29. package/dist/src/framework/timer-manager.d.ts +9 -0
  30. package/dist/src/framework/timer-manager.js +28 -0
  31. package/dist/src/framework/trigger-engine.d.ts +28 -0
  32. package/dist/src/framework/trigger-engine.js +321 -0
  33. package/dist/src/index.d.ts +1 -0
  34. package/dist/src/index.js +157 -0
  35. package/dist/src/lib.d.ts +6 -0
  36. package/dist/src/lib.js +2 -0
  37. package/dist/src/services.d.ts +178 -0
  38. package/dist/src/services.js +106 -0
  39. package/dist/src/testing.d.ts +18 -0
  40. package/dist/src/testing.js +60 -0
  41. package/dist/src/types/actions.d.ts +23 -0
  42. package/dist/src/types/actions.js +1 -0
  43. package/dist/src/types/automation.d.ts +32 -0
  44. package/dist/src/types/automation.js +44 -0
  45. package/dist/src/types/triggers.d.ts +77 -0
  46. package/dist/src/types/triggers.js +26 -0
  47. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Clarkson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # Homerun
2
+
3
+ **Typed, testable Home Assistant automations in TypeScript.**
4
+
5
+ > **Status: early development.** The framework is not yet published to npm and the API is not stable. This README documents the intended design; some parts are still being built.
6
+
7
+ Homerun is a framework that replaces HA automations and Node-RED flows with pure functions. Every automation is a context builder and a reducer — no side effects, no hidden state, no YAML. The pipeline handles observability, error isolation, and action execution.
8
+
9
+ ---
10
+
11
+ ## The problem
12
+
13
+ Home Assistant automations are YAML. Node-RED flows are JSON blobs. Both are hard to test, hard to review, and impossible to type-check. A bad deploy silently breaks your lights at 2am.
14
+
15
+ Homerun brings the same discipline you'd apply to application code: pure functions, typed inputs, and a test suite that runs in milliseconds.
16
+
17
+ ---
18
+
19
+ ## How it works
20
+
21
+ Every automation follows the same pipeline:
22
+
23
+ ```
24
+ Event → context() → reduce() → actions[]
25
+ ```
26
+
27
+ **`context()`** reads from the HA state cache and returns a typed snapshot — or aborts early if the guard condition isn't met. It is the only place that touches external state.
28
+
29
+ **`reduce()`** is a pure function. It receives the context and returns a decision with a list of declarative actions. No async, no side effects, no HA calls.
30
+
31
+ **The pipeline** handles everything else: correlation IDs, observability snapshots, error isolation, and action execution.
32
+
33
+ ---
34
+
35
+ ## Example
36
+
37
+ ```typescript
38
+ import { defineAutomation, abort } from './src/types/automation.js';
39
+
40
+ export const kitchenLights = defineAutomation({
41
+ id: 'kitchen:lighting',
42
+ location: 'kitchen',
43
+ subsystem: 'lighting',
44
+
45
+ triggers: [
46
+ { type: 'state_changed', entity: 'binary_sensor.kitchen_motion' },
47
+ { type: 'state_changed', entity: 'binary_sensor.kitchen_door' },
48
+ { type: 'on_start' },
49
+ ],
50
+
51
+ context(state, ha) {
52
+ const enabled = state('input_boolean.kitchen_automation_lights_enabled');
53
+ if (enabled?.state !== 'on') return abort('automation_disabled');
54
+
55
+ return {
56
+ motion: state('binary_sensor.kitchen_motion')?.state === 'on',
57
+ lux: Number(state('sensor.kitchen_sensor_lux')?.state ?? 0),
58
+ luxThreshold: Number(state('input_number.kitchen_automation_lux_threshold_dark')?.state ?? 40),
59
+ };
60
+ },
61
+
62
+ reduce(ctx) {
63
+ const shouldLight = ctx.motion && ctx.lux < ctx.luxThreshold;
64
+
65
+ return {
66
+ decision: shouldLight ? 'lights_on' : 'lights_off',
67
+ inputs: ctx,
68
+ actions: [
69
+ {
70
+ type: 'ha.call_service',
71
+ domain: 'light',
72
+ service: shouldLight ? 'turn_on' : 'turn_off',
73
+ target: { entity_id: 'light.kitchen_light_ceiling' },
74
+ },
75
+ ],
76
+ };
77
+ },
78
+ });
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Testing
84
+
85
+ Because `reduce()` is a pure function, tests are just function calls:
86
+
87
+ ```typescript
88
+ import { describe, it, expect } from 'vitest';
89
+ import { kitchenLights } from './kitchen-lights';
90
+
91
+ describe('kitchenLights reducer', () => {
92
+ it('turns lights on when motion is detected below lux threshold', () => {
93
+ const result = kitchenLights.reduce({ motion: true, lux: 20, luxThreshold: 40 });
94
+ expect(result.decision).toBe('lights_on');
95
+ expect(result.actions[0]).toMatchObject({ service: 'turn_on' });
96
+ });
97
+
98
+ it('turns lights off when lux is already high', () => {
99
+ const result = kitchenLights.reduce({ motion: true, lux: 80, luxThreshold: 40 });
100
+ expect(result.decision).toBe('lights_off');
101
+ });
102
+
103
+ it('turns lights off when motion clears', () => {
104
+ const result = kitchenLights.reduce({ motion: false, lux: 10, luxThreshold: 40 });
105
+ expect(result.decision).toBe('lights_off');
106
+ });
107
+ });
108
+ ```
109
+
110
+ No mocking, no HA connection, no async. The context builder can be tested separately with a plain function as the state accessor:
111
+
112
+ ```typescript
113
+ const mockState = (entity: string) => ({
114
+ 'input_boolean.kitchen_automation_lights_enabled': { state: 'on' },
115
+ 'binary_sensor.kitchen_motion': { state: 'on' },
116
+ 'sensor.kitchen_sensor_lux': { state: '20' },
117
+ 'input_number.kitchen_automation_lux_threshold_dark': { state: '40' },
118
+ })[entity] as any;
119
+
120
+ const ctx = kitchenLights.context(mockState, mockHAContext);
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Trigger types
126
+
127
+ | Trigger | Fires when |
128
+ |---------|-----------|
129
+ | `state_changed` | An entity's state or attributes change. Accepts a string or `RegExp` for the entity ID. |
130
+ | `schedule` | A cron expression fires. |
131
+ | `on_start` | The system is ready and the state cache is fully populated. |
132
+ | `timer_expired` | A named timer set by a previous `timer.start` action expires. |
133
+ | `button` | A Zigbee button entity emits a `single_press`, `double_press`, or `hold` gesture. |
134
+ | `mqtt_in` | A message arrives on a subscribed MQTT topic. |
135
+
136
+ ```typescript
137
+ triggers: [
138
+ { type: 'state_changed', entity: 'binary_sensor.parlour_motion' },
139
+ { type: 'state_changed', entity: /^binary_sensor\..+_motion$/ },
140
+ { type: 'schedule', cron: '0 22 * * *' },
141
+ { type: 'on_start' },
142
+ { type: 'timer_expired', timerKey: 'kitchen:lights:off-delay' },
143
+ { type: 'button', entity: 'sensor.hallway_button', gesture: 'double_press' },
144
+ ]
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Action types
150
+
151
+ Actions are returned from `reduce()` as a plain array. The runtime executes them after the observability snapshot is published.
152
+
153
+ | Action | Effect |
154
+ |--------|--------|
155
+ | `ha.call_service` | Calls a Home Assistant service |
156
+ | `mqtt.publish` | Publishes to an MQTT topic |
157
+ | `timer.start` | Starts (or restarts) a named timer |
158
+ | `timer.cancel` | Cancels a named timer |
159
+
160
+ ```typescript
161
+ actions: [
162
+ {
163
+ type: 'ha.call_service',
164
+ domain: 'climate',
165
+ service: 'set_temperature',
166
+ target: { entity_id: 'climate.bedroom_trv' },
167
+ data: { temperature: 20 },
168
+ },
169
+ {
170
+ type: 'timer.start',
171
+ timerKey: 'kitchen:lights:off-delay',
172
+ delayMs: 120_000,
173
+ },
174
+ {
175
+ type: 'mqtt.publish',
176
+ topic: 'homerun/decisions/kitchen',
177
+ payload: JSON.stringify({ decision: 'lights_on' }),
178
+ retain: false,
179
+ },
180
+ ]
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Observability
186
+
187
+ Every pipeline run publishes a decision snapshot to MQTT — whether it completed, aborted, or threw. Each snapshot carries a `correlation_id` minted at the event source, so you can trace a HA state change through to the TRV setpoint that resulted from it.
188
+
189
+ ```json
190
+ {
191
+ "schema": "home.events.v1",
192
+ "correlation_id": "b3d2f1a0-...",
193
+ "automation_id": "kitchen:lighting",
194
+ "location": "kitchen",
195
+ "subsystem": "lighting",
196
+ "event_type": "decision",
197
+ "decision": "lights_on",
198
+ "inputs": { "motion": true, "lux": 20, "luxThreshold": 40 },
199
+ "actions": [{ "type": "ha.call_service", "domain": "light", "service": "turn_on" }],
200
+ "timestamp": "2026-06-23T18:00:00.000Z"
201
+ }
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Environment variables
207
+
208
+ | Variable | Purpose |
209
+ |----------|---------|
210
+ | `HA_URL` | Home Assistant base URL (e.g. `http://homeassistant.local:8123`) |
211
+ | `HA_TOKEN` | Long-lived access token |
212
+ | `MQTT_URL` | MQTT broker URL (e.g. `mqtt://localhost:1883`) |
213
+ | `AUTOMATIONS_DIR` | Directory of automation files (hot-reloaded on change) |
214
+ | `DRY_RUN` | Set to `true` to log actions without executing them |
215
+
216
+ ---
217
+
218
+ ## Running
219
+
220
+ > Homerun is not yet published to npm. Clone the repository and run directly from source.
221
+
222
+ ```bash
223
+ # Development — hot-reloads automations on file change
224
+ npm run dev
225
+
226
+ # Run tests
227
+ npm test
228
+ ```
229
+
230
+ Set `DRY_RUN=true` to run the full pipeline — context, reduce, observability — without making any HA service calls. This is the default for local development.
231
+
232
+ ---
233
+
234
+ ## API
235
+
236
+ The server exposes an HTTP API on port `7070` by default.
237
+
238
+ | Method | Path | Description |
239
+ |--------|------|-------------|
240
+ | `GET` | `/health/live` | Always returns `200 { status: "live" }` — used for liveness probes. |
241
+ | `GET` | `/health/ready` | Returns `200` when the HA state cache is populated and automations are loaded, `503` while starting. Response includes `entities`, `automations` counts, and `dry_run: true` if running in dry-run mode. |
242
+ | `GET` | `/automations` | Lists all registered automations with their `id`, `location`, `subsystem`, and trigger types. |
243
+ | `POST` | `/automations/:id/trigger` | Manually fires an `on_start` event for the given automation. |
244
+ | `POST` | `/reload` | Rescans the automations directory and hot-reloads changed files. |
245
+ | `GET` | `/events` | Server-sent event stream of all pipeline decisions and action events in real time. |
246
+
247
+ ---
248
+
249
+ ## Architecture
250
+
251
+ ```
252
+ HAClient ──────────────────────────────────────┐
253
+ └─ state_changed (per entity, with corr. ID) │
254
+ ▼
255
+ Scheduler (cron / on_start) ──────► TriggerEngine.dispatch()
256
+ TimerManager (setTimeout) ──────► │
257
+ ▼
258
+ matchAndFire()
259
+ │
260
+ ┌───────────▼──────────┐
261
+ │ Pipeline Runner │
262
+ │ context → reduce │
263
+ │ → validate → fanout │
264
+ └──────────┬────────────┘
265
+ │
266
+ ┌──────────────┴──────────────┐
267
+ ▼ ▼
268
+ Observability ActionRuntime
269
+ (MQTT snapshot) (HA calls / timers / MQTT)
270
+ ```
271
+
272
+ ---
273
+
274
+ ## License
275
+
276
+ MIT
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ export interface HAServicePayload {
3
+ domain: string;
4
+ services: Record<string, HAServiceDef>;
5
+ }
6
+ export interface HAServiceDef {
7
+ fields: Record<string, HAServiceField>;
8
+ target?: unknown;
9
+ }
10
+ export interface HAServiceField {
11
+ required?: boolean;
12
+ selector?: Record<string, unknown>;
13
+ advanced?: boolean;
14
+ }
15
+ export declare function inferFieldType(field: HAServiceField): string;
16
+ export declare function generateFileContent(services: HAServicePayload[], domains?: string[]): string;
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import { writeFile, mkdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ // ---------- Type inference ----------
5
+ export function inferFieldType(field) {
6
+ const selector = field.selector;
7
+ if (!selector)
8
+ return 'unknown';
9
+ const key = Object.keys(selector)[0];
10
+ if (!key)
11
+ return 'unknown';
12
+ switch (key) {
13
+ case 'number':
14
+ case 'color_temp':
15
+ return 'number';
16
+ case 'boolean':
17
+ return 'boolean';
18
+ case 'color_rgb':
19
+ return '[number, number, number]';
20
+ case 'duration':
21
+ return '{ hours?: number; minutes?: number; seconds?: number }';
22
+ case 'object':
23
+ return 'Record<string, unknown>';
24
+ default:
25
+ return 'string';
26
+ }
27
+ }
28
+ // ---------- Service builder generation ----------
29
+ function generateServiceBuilder(domain, service, def) {
30
+ const hasTarget = !!def.target;
31
+ const fieldEntries = Object.entries(def.fields ?? {});
32
+ const hasFields = fieldEntries.length > 0;
33
+ const params = [];
34
+ if (hasTarget)
35
+ params.push('target: { entity_id: string }');
36
+ let anyRequired = false;
37
+ if (hasFields) {
38
+ anyRequired = fieldEntries.some(([, f]) => f.required);
39
+ const fieldDefs = fieldEntries
40
+ .map(([name, field]) => `${name}${field.required ? '' : '?'}: ${inferFieldType(field)}`)
41
+ .join('; ');
42
+ params.push(`data${anyRequired ? '' : '?'}: { ${fieldDefs} }`);
43
+ }
44
+ const bodyParts = [
45
+ `type: 'ha.call_service'`,
46
+ `domain: '${domain}'`,
47
+ `service: '${service}'`,
48
+ ];
49
+ if (hasTarget)
50
+ bodyParts.push('target');
51
+ if (hasFields) {
52
+ bodyParts.push(`data: data as Record<string, unknown>${anyRequired ? '' : ' | undefined'}`);
53
+ }
54
+ return ` ${service}: (${params.join(', ')}): Action => ({ ${bodyParts.join(', ')} })`;
55
+ }
56
+ // ---------- File generation ----------
57
+ export function generateFileContent(services, domains) {
58
+ const filtered = domains
59
+ ? services.filter(({ domain }) => domains.includes(domain))
60
+ : services;
61
+ const domainBlocks = filtered.map(({ domain, services: svcMap }) => {
62
+ const builders = Object.entries(svcMap)
63
+ .map(([service, def]) => generateServiceBuilder(domain, service, def))
64
+ .join(',\n');
65
+ return ` ${domain}: {\n${builders},\n }`;
66
+ });
67
+ const domainsNote = domains ? `\n// domains: ${domains.join(', ')}` : '';
68
+ const blockStr = domainBlocks.length > 0 ? `\n${domainBlocks.join(',\n')},\n` : '';
69
+ return `// generated — do not edit — run: npm run generate:ha-services${domainsNote}
70
+ import type { Action } from '@ajclarkson/homerun';
71
+
72
+ export const Services = {${blockStr}};
73
+ `;
74
+ }
75
+ // ---------- CLI ----------
76
+ async function main() {
77
+ const url = process.env.HA_URL;
78
+ const token = process.env.HA_TOKEN;
79
+ if (!url || !token) {
80
+ console.error('Error: HA_URL and HA_TOKEN must be set');
81
+ process.exit(1);
82
+ }
83
+ const domainsArg = process.argv.find((a) => a.startsWith('--domains='));
84
+ const domains = domainsArg ? domainsArg.slice('--domains='.length).split(',') : undefined;
85
+ const res = await fetch(`${url}/api/services`, {
86
+ headers: { Authorization: `Bearer ${token}` },
87
+ });
88
+ if (!res.ok) {
89
+ console.error(`Error: HA API returned ${res.status} ${res.statusText}`);
90
+ process.exit(1);
91
+ }
92
+ const allServices = (await res.json());
93
+ const content = generateFileContent(allServices, domains);
94
+ const outPath = path.join(process.cwd(), 'types', 'ha-services.ts');
95
+ await mkdir(path.dirname(outPath), { recursive: true });
96
+ await writeFile(outPath, content, 'utf8');
97
+ const filtered = domains
98
+ ? allServices.filter(({ domain }) => domains.includes(domain))
99
+ : allServices;
100
+ const serviceCount = filtered.reduce((n, { services: s }) => n + Object.keys(s).length, 0);
101
+ const domainLabel = domains ? `${domains.length} selected` : `all ${allServices.length}`;
102
+ console.log(`Written ${serviceCount} services across ${domainLabel} domains to ${outPath}`);
103
+ }
104
+ if (process.argv[1]?.endsWith('generate-ha-services.ts') ||
105
+ process.argv[1]?.endsWith('generate-ha-services.js') ||
106
+ process.argv[1]?.endsWith('homerun-generate-ha-services')) {
107
+ main().catch((err) => {
108
+ console.error('Fatal:', err);
109
+ process.exit(1);
110
+ });
111
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ export interface HAStatePayload {
3
+ entity_id: string;
4
+ state: string;
5
+ attributes: Record<string, unknown>;
6
+ }
7
+ export declare function inferStateType(entity: HAStatePayload, allObservedStates?: string[]): string;
8
+ export declare function generateFileContent(states: HAStatePayload[]): string;
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ import { writeFile, mkdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ // ---------- Inference ----------
5
+ export function inferStateType(entity, allObservedStates) {
6
+ const domain = entity.entity_id.split('.')[0];
7
+ switch (domain) {
8
+ case 'binary_sensor':
9
+ case 'input_boolean':
10
+ case 'switch':
11
+ return "'on' | 'off' | 'unavailable' | 'unknown'";
12
+ case 'input_select': {
13
+ const options = entity.attributes.options;
14
+ if (Array.isArray(options) && options.length > 0) {
15
+ return options.map((o) => `'${o}'`).join(' | ') + " | 'unavailable' | 'unknown'";
16
+ }
17
+ return 'string';
18
+ }
19
+ default:
20
+ return 'string';
21
+ }
22
+ }
23
+ // ---------- File generation ----------
24
+ export function generateFileContent(states) {
25
+ const personStates = new Map();
26
+ for (const s of states) {
27
+ if (s.entity_id.startsWith('person.')) {
28
+ const existing = personStates.get(s.entity_id) ?? [];
29
+ existing.push(s.state);
30
+ personStates.set(s.entity_id, existing);
31
+ }
32
+ }
33
+ const entries = states
34
+ .map((s) => {
35
+ const stateType = inferStateType(s, personStates.get(s.entity_id));
36
+ return ` '${s.entity_id}': { state: ${stateType} };`;
37
+ })
38
+ .join('\n');
39
+ return `// generated — do not edit — run: npm run generate:ha-types
40
+ declare global {
41
+ interface HAEntities {
42
+ ${entries}
43
+ }
44
+ }
45
+ export {};
46
+ `;
47
+ }
48
+ // ---------- CLI ----------
49
+ async function main() {
50
+ const url = process.env.HA_URL;
51
+ const token = process.env.HA_TOKEN;
52
+ if (!url || !token) {
53
+ console.error('Error: HA_URL and HA_TOKEN must be set');
54
+ process.exit(1);
55
+ }
56
+ const res = await fetch(`${url}/api/states`, {
57
+ headers: { Authorization: `Bearer ${token}` },
58
+ });
59
+ if (!res.ok) {
60
+ console.error(`Error: HA API returned ${res.status} ${res.statusText}`);
61
+ process.exit(1);
62
+ }
63
+ const states = (await res.json());
64
+ const content = generateFileContent(states);
65
+ const outPath = path.join(process.cwd(), 'types', 'ha-entities.ts');
66
+ await mkdir(path.dirname(outPath), { recursive: true });
67
+ await writeFile(outPath, content, 'utf8');
68
+ console.log(`Written ${states.length} entities to ${outPath}`);
69
+ }
70
+ if (process.argv[1]?.endsWith('generate-ha-types.ts') || process.argv[1]?.endsWith('generate-ha-types.js') || process.argv[1]?.endsWith('homerun-generate-ha-types')) {
71
+ main().catch((err) => {
72
+ console.error('Fatal:', err);
73
+ process.exit(1);
74
+ });
75
+ }
@@ -0,0 +1,39 @@
1
+ import type { MqttClient } from 'mqtt';
2
+ import type { Action } from '../types/actions.js';
3
+ import type { HAClient } from './ha-client.js';
4
+ import type { TimerManager } from './timer-manager.js';
5
+ import type { EventPublisher } from './event-publisher.js';
6
+ import type { MetricsBackend } from './metrics.js';
7
+ export interface ExecutionContext {
8
+ correlationId: string;
9
+ automationId: string;
10
+ location: string;
11
+ subsystem: string;
12
+ rootCorrelationId?: string;
13
+ parentCorrelationId?: string;
14
+ parentAutomationId?: string;
15
+ }
16
+ interface Deps {
17
+ haClient: HAClient;
18
+ mqttClient: MqttClient;
19
+ timerManager: TimerManager;
20
+ eventPublisher: EventPublisher;
21
+ dryRun: boolean;
22
+ metrics?: MetricsBackend;
23
+ commandAck?: {
24
+ enabled: boolean;
25
+ timeoutMs: number;
26
+ };
27
+ }
28
+ export declare class ActionRuntime {
29
+ private readonly deps;
30
+ constructor(deps: Deps);
31
+ private handleAckTimeout;
32
+ execute(actions: Action[], ctx: ExecutionContext): Promise<void>;
33
+ private runAction;
34
+ private dispatch;
35
+ private baseFields;
36
+ private makeStartedEvent;
37
+ private makeResultEvent;
38
+ }
39
+ export {};