@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
@@ -0,0 +1,178 @@
1
+ import type { Action } from './types/actions.js';
2
+ type Target = {
3
+ entity_id: string;
4
+ };
5
+ export interface LightTurnOnData {
6
+ transition?: number;
7
+ brightness_pct?: number;
8
+ brightness?: number;
9
+ color_temp_kelvin?: number;
10
+ rgb_color?: [number, number, number];
11
+ effect?: string;
12
+ }
13
+ export type HvacMode = 'off' | 'heat' | 'cool' | 'heat_cool' | 'auto' | 'dry' | 'fan_only';
14
+ export interface ClimateSetTemperatureData {
15
+ temperature?: number;
16
+ target_temp_high?: number;
17
+ target_temp_low?: number;
18
+ hvac_mode?: HvacMode;
19
+ }
20
+ export interface CoverSetPositionData {
21
+ position: number;
22
+ }
23
+ export interface CoverSetTiltPositionData {
24
+ tilt_position: number;
25
+ }
26
+ export type MediaPlayerEnqueue = 'next' | 'add' | 'play' | 'replace';
27
+ export type MediaPlayerRepeat = 'off' | 'all' | 'one';
28
+ export interface MediaPlayerPlayMediaData {
29
+ media_content_id: string;
30
+ media_content_type: string;
31
+ enqueue?: MediaPlayerEnqueue;
32
+ announce?: boolean;
33
+ }
34
+ export type AlarmCode = {
35
+ code?: string;
36
+ };
37
+ export declare const HomeAssistant: {
38
+ readonly light: {
39
+ readonly turn_on: (target: Target, data?: LightTurnOnData) => Action;
40
+ readonly turn_off: (target: Target, data?: {
41
+ transition?: number;
42
+ }) => Action;
43
+ readonly toggle: (target: Target, data?: LightTurnOnData) => Action;
44
+ };
45
+ readonly switch: {
46
+ readonly turn_on: (target: Target) => Action;
47
+ readonly turn_off: (target: Target) => Action;
48
+ readonly toggle: (target: Target) => Action;
49
+ };
50
+ readonly cover: {
51
+ readonly open_cover: (target: Target) => Action;
52
+ readonly close_cover: (target: Target) => Action;
53
+ readonly stop_cover: (target: Target) => Action;
54
+ readonly toggle: (target: Target) => Action;
55
+ readonly set_cover_position: (target: Target, data: CoverSetPositionData) => Action;
56
+ readonly set_cover_tilt_position: (target: Target, data: CoverSetTiltPositionData) => Action;
57
+ };
58
+ readonly fan: {
59
+ readonly turn_on: (target: Target, data?: {
60
+ percentage?: number;
61
+ preset_mode?: string;
62
+ }) => Action;
63
+ readonly turn_off: (target: Target) => Action;
64
+ readonly toggle: (target: Target) => Action;
65
+ readonly set_percentage: (target: Target, data: {
66
+ percentage: number;
67
+ }) => Action;
68
+ readonly set_preset_mode: (target: Target, data: {
69
+ preset_mode: string;
70
+ }) => Action;
71
+ readonly increase_speed: (target: Target) => Action;
72
+ readonly decrease_speed: (target: Target) => Action;
73
+ };
74
+ readonly lock: {
75
+ readonly lock: (target: Target, data?: AlarmCode) => Action;
76
+ readonly unlock: (target: Target, data?: AlarmCode) => Action;
77
+ readonly open: (target: Target, data?: AlarmCode) => Action;
78
+ };
79
+ readonly climate: {
80
+ readonly turn_on: (target: Target) => Action;
81
+ readonly turn_off: (target: Target) => Action;
82
+ readonly set_hvac_mode: (target: Target, data: {
83
+ hvac_mode: HvacMode;
84
+ }) => Action;
85
+ readonly set_temperature: (target: Target, data: ClimateSetTemperatureData) => Action;
86
+ readonly set_preset_mode: (target: Target, data: {
87
+ preset_mode: string;
88
+ }) => Action;
89
+ readonly set_fan_mode: (target: Target, data: {
90
+ fan_mode: string;
91
+ }) => Action;
92
+ readonly set_humidity: (target: Target, data: {
93
+ humidity: number;
94
+ }) => Action;
95
+ };
96
+ readonly media_player: {
97
+ readonly turn_on: (target: Target) => Action;
98
+ readonly turn_off: (target: Target) => Action;
99
+ readonly toggle: (target: Target) => Action;
100
+ readonly media_play: (target: Target) => Action;
101
+ readonly media_pause: (target: Target) => Action;
102
+ readonly media_play_pause: (target: Target) => Action;
103
+ readonly media_stop: (target: Target) => Action;
104
+ readonly media_next_track: (target: Target) => Action;
105
+ readonly media_previous_track: (target: Target) => Action;
106
+ readonly volume_up: (target: Target) => Action;
107
+ readonly volume_down: (target: Target) => Action;
108
+ readonly volume_set: (target: Target, data: {
109
+ volume_level: number;
110
+ }) => Action;
111
+ readonly volume_mute: (target: Target, data: {
112
+ is_volume_muted: boolean;
113
+ }) => Action;
114
+ readonly select_source: (target: Target, data: {
115
+ source: string;
116
+ }) => Action;
117
+ readonly shuffle_set: (target: Target, data: {
118
+ shuffle: boolean;
119
+ }) => Action;
120
+ readonly repeat_set: (target: Target, data: {
121
+ repeat: MediaPlayerRepeat;
122
+ }) => Action;
123
+ readonly play_media: (target: Target, data: MediaPlayerPlayMediaData) => Action;
124
+ };
125
+ readonly alarm_control_panel: {
126
+ readonly arm_away: (target: Target, data?: AlarmCode) => Action;
127
+ readonly arm_home: (target: Target, data?: AlarmCode) => Action;
128
+ readonly arm_night: (target: Target, data?: AlarmCode) => Action;
129
+ readonly arm_vacation: (target: Target, data?: AlarmCode) => Action;
130
+ readonly disarm: (target: Target, data?: AlarmCode) => Action;
131
+ readonly trigger: (target: Target, data?: AlarmCode) => Action;
132
+ };
133
+ readonly scene: {
134
+ readonly turn_on: (target: Target, data?: {
135
+ transition?: number;
136
+ }) => Action;
137
+ };
138
+ readonly input_boolean: {
139
+ readonly turn_on: (target: Target) => Action;
140
+ readonly turn_off: (target: Target) => Action;
141
+ readonly toggle: (target: Target) => Action;
142
+ };
143
+ readonly input_number: {
144
+ readonly set_value: (target: Target, data: {
145
+ value: number;
146
+ }) => Action;
147
+ readonly increment: (target: Target) => Action;
148
+ readonly decrement: (target: Target) => Action;
149
+ readonly set_min: (target: Target) => Action;
150
+ readonly set_max: (target: Target) => Action;
151
+ };
152
+ readonly input_select: {
153
+ readonly select_option: (target: Target, data: {
154
+ option: string;
155
+ }) => Action;
156
+ readonly select_next: (target: Target) => Action;
157
+ readonly select_previous: (target: Target) => Action;
158
+ readonly set_options: (target: Target, data: {
159
+ options: string[];
160
+ }) => Action;
161
+ };
162
+ readonly button: {
163
+ readonly press: (target: Target) => Action;
164
+ };
165
+ readonly number: {
166
+ readonly set_value: (target: Target, data: {
167
+ value: number;
168
+ }) => Action;
169
+ };
170
+ readonly select: {
171
+ readonly select_option: (target: Target, data: {
172
+ option: string;
173
+ }) => Action;
174
+ readonly select_next: (target: Target) => Action;
175
+ readonly select_previous: (target: Target) => Action;
176
+ };
177
+ };
178
+ export {};
@@ -0,0 +1,106 @@
1
+ function svc(domain, service, target, data) {
2
+ return { type: 'ha.call_service', domain, service, ...(target && { target }), ...(data !== undefined ? { data: data } : {}) };
3
+ }
4
+ // ---------- HomeAssistant ----------
5
+ export const HomeAssistant = {
6
+ light: {
7
+ turn_on: (target, data) => svc('light', 'turn_on', target, data),
8
+ turn_off: (target, data) => svc('light', 'turn_off', target, data),
9
+ toggle: (target, data) => svc('light', 'toggle', target, data),
10
+ },
11
+ switch: {
12
+ turn_on: (target) => svc('switch', 'turn_on', target),
13
+ turn_off: (target) => svc('switch', 'turn_off', target),
14
+ toggle: (target) => svc('switch', 'toggle', target),
15
+ },
16
+ cover: {
17
+ open_cover: (target) => svc('cover', 'open_cover', target),
18
+ close_cover: (target) => svc('cover', 'close_cover', target),
19
+ stop_cover: (target) => svc('cover', 'stop_cover', target),
20
+ toggle: (target) => svc('cover', 'toggle', target),
21
+ set_cover_position: (target, data) => svc('cover', 'set_cover_position', target, data),
22
+ set_cover_tilt_position: (target, data) => svc('cover', 'set_cover_tilt_position', target, data),
23
+ },
24
+ fan: {
25
+ turn_on: (target, data) => svc('fan', 'turn_on', target, data),
26
+ turn_off: (target) => svc('fan', 'turn_off', target),
27
+ toggle: (target) => svc('fan', 'toggle', target),
28
+ set_percentage: (target, data) => svc('fan', 'set_percentage', target, data),
29
+ set_preset_mode: (target, data) => svc('fan', 'set_preset_mode', target, data),
30
+ increase_speed: (target) => svc('fan', 'increase_speed', target),
31
+ decrease_speed: (target) => svc('fan', 'decrease_speed', target),
32
+ },
33
+ lock: {
34
+ lock: (target, data) => svc('lock', 'lock', target, data),
35
+ unlock: (target, data) => svc('lock', 'unlock', target, data),
36
+ open: (target, data) => svc('lock', 'open', target, data),
37
+ },
38
+ climate: {
39
+ turn_on: (target) => svc('climate', 'turn_on', target),
40
+ turn_off: (target) => svc('climate', 'turn_off', target),
41
+ set_hvac_mode: (target, data) => svc('climate', 'set_hvac_mode', target, data),
42
+ set_temperature: (target, data) => svc('climate', 'set_temperature', target, data),
43
+ set_preset_mode: (target, data) => svc('climate', 'set_preset_mode', target, data),
44
+ set_fan_mode: (target, data) => svc('climate', 'set_fan_mode', target, data),
45
+ set_humidity: (target, data) => svc('climate', 'set_humidity', target, data),
46
+ },
47
+ media_player: {
48
+ turn_on: (target) => svc('media_player', 'turn_on', target),
49
+ turn_off: (target) => svc('media_player', 'turn_off', target),
50
+ toggle: (target) => svc('media_player', 'toggle', target),
51
+ media_play: (target) => svc('media_player', 'media_play', target),
52
+ media_pause: (target) => svc('media_player', 'media_pause', target),
53
+ media_play_pause: (target) => svc('media_player', 'media_play_pause', target),
54
+ media_stop: (target) => svc('media_player', 'media_stop', target),
55
+ media_next_track: (target) => svc('media_player', 'media_next_track', target),
56
+ media_previous_track: (target) => svc('media_player', 'media_previous_track', target),
57
+ volume_up: (target) => svc('media_player', 'volume_up', target),
58
+ volume_down: (target) => svc('media_player', 'volume_down', target),
59
+ volume_set: (target, data) => svc('media_player', 'volume_set', target, data),
60
+ volume_mute: (target, data) => svc('media_player', 'volume_mute', target, data),
61
+ select_source: (target, data) => svc('media_player', 'select_source', target, data),
62
+ shuffle_set: (target, data) => svc('media_player', 'shuffle_set', target, data),
63
+ repeat_set: (target, data) => svc('media_player', 'repeat_set', target, data),
64
+ play_media: (target, data) => svc('media_player', 'play_media', target, data),
65
+ },
66
+ alarm_control_panel: {
67
+ arm_away: (target, data) => svc('alarm_control_panel', 'arm_away', target, data),
68
+ arm_home: (target, data) => svc('alarm_control_panel', 'arm_home', target, data),
69
+ arm_night: (target, data) => svc('alarm_control_panel', 'arm_night', target, data),
70
+ arm_vacation: (target, data) => svc('alarm_control_panel', 'arm_vacation', target, data),
71
+ disarm: (target, data) => svc('alarm_control_panel', 'disarm', target, data),
72
+ trigger: (target, data) => svc('alarm_control_panel', 'trigger', target, data),
73
+ },
74
+ scene: {
75
+ turn_on: (target, data) => svc('scene', 'turn_on', target, data),
76
+ },
77
+ input_boolean: {
78
+ turn_on: (target) => svc('input_boolean', 'turn_on', target),
79
+ turn_off: (target) => svc('input_boolean', 'turn_off', target),
80
+ toggle: (target) => svc('input_boolean', 'toggle', target),
81
+ },
82
+ input_number: {
83
+ set_value: (target, data) => svc('input_number', 'set_value', target, data),
84
+ increment: (target) => svc('input_number', 'increment', target),
85
+ decrement: (target) => svc('input_number', 'decrement', target),
86
+ set_min: (target) => svc('input_number', 'min', target),
87
+ set_max: (target) => svc('input_number', 'max', target),
88
+ },
89
+ input_select: {
90
+ select_option: (target, data) => svc('input_select', 'select_option', target, data),
91
+ select_next: (target) => svc('input_select', 'select_next', target),
92
+ select_previous: (target) => svc('input_select', 'select_previous', target),
93
+ set_options: (target, data) => svc('input_select', 'set_options', target, data),
94
+ },
95
+ button: {
96
+ press: (target) => svc('button', 'press', target),
97
+ },
98
+ number: {
99
+ set_value: (target, data) => svc('number', 'set_value', target, data),
100
+ },
101
+ select: {
102
+ select_option: (target, data) => svc('select', 'select_option', target, data),
103
+ select_next: (target) => svc('select', 'select_next', target),
104
+ select_previous: (target) => svc('select', 'select_previous', target),
105
+ },
106
+ };
@@ -0,0 +1,18 @@
1
+ import type { Automation, Decision, Abort } from './types/automation.js';
2
+ import type { TriggerEvent } from './types/triggers.js';
3
+ import type { HAContext } from './framework/ha-client.js';
4
+ type TestStateEntry = {
5
+ state: string;
6
+ attributes?: Record<string, unknown>;
7
+ last_changed?: string;
8
+ last_updated?: string;
9
+ };
10
+ interface TestOptions {
11
+ event: TriggerEvent;
12
+ state?: Record<string, TestStateEntry>;
13
+ ha?: Partial<HAContext>;
14
+ }
15
+ export declare function testAutomation<C>(automation: Automation<C>, options: TestOptions): Decision;
16
+ export declare function testAbort<C>(automation: Automation<C>, options: TestOptions): Abort;
17
+ export declare function testUnavailable<C>(automation: Automation<C>, options: TestOptions): string;
18
+ export {};
@@ -0,0 +1,60 @@
1
+ import { isAbort, UnavailableInputError } from './types/automation.js';
2
+ function buildStateAndHa(options) {
3
+ const { state = {}, ha = {} } = options;
4
+ const stateFunc = (entityId) => {
5
+ const entry = state[entityId];
6
+ if (!entry)
7
+ return undefined;
8
+ return {
9
+ entity_id: entityId,
10
+ state: entry.state,
11
+ attributes: entry.attributes ?? {},
12
+ last_changed: entry.last_changed ?? '',
13
+ last_updated: entry.last_updated ?? '',
14
+ };
15
+ };
16
+ const haContext = {
17
+ entitiesByLabel: ha.entitiesByLabel ?? (() => []),
18
+ labelsFor: ha.labelsFor ?? (() => []),
19
+ entitiesByArea: ha.entitiesByArea ?? (() => []),
20
+ };
21
+ return { stateFunc, haContext };
22
+ }
23
+ function run(automation, options) {
24
+ const { stateFunc, haContext } = buildStateAndHa(options);
25
+ const ctx = automation.context(stateFunc, haContext, options.event);
26
+ if (isAbort(ctx))
27
+ return ctx;
28
+ const result = automation.reduce(ctx);
29
+ // Mirrors runPipeline's default in src/framework/pipeline.ts, so tests observe the same
30
+ // `conditions` a real run would publish.
31
+ return { ...result, conditions: result.conditions ?? ctx };
32
+ }
33
+ export function testAutomation(automation, options) {
34
+ const result = run(automation, options);
35
+ if (isAbort(result))
36
+ throw new Error(`automation aborted: ${result.reason}`);
37
+ return result;
38
+ }
39
+ export function testAbort(automation, options) {
40
+ const result = run(automation, options);
41
+ if (!isAbort(result))
42
+ throw new Error(`expected abort but got decision: ${result.decision}`);
43
+ return result;
44
+ }
45
+ // For automations using requireState()/requireNumericState(), which throw
46
+ // UnavailableInputError rather than returning Abort — testAbort() can't observe these since
47
+ // run() never catches an exception thrown out of context(). Returns the entity id the
48
+ // automation required, so tests can assert on which input was missing.
49
+ export function testUnavailable(automation, options) {
50
+ const { stateFunc, haContext } = buildStateAndHa(options);
51
+ try {
52
+ automation.context(stateFunc, haContext, options.event);
53
+ }
54
+ catch (err) {
55
+ if (err instanceof UnavailableInputError)
56
+ return err.entityId;
57
+ throw err;
58
+ }
59
+ throw new Error('expected UnavailableInputError but context() completed normally');
60
+ }
@@ -0,0 +1,23 @@
1
+ export interface HaCallServiceAction {
2
+ type: 'ha.call_service';
3
+ domain: string;
4
+ service: string;
5
+ target?: {
6
+ entity_id: string;
7
+ };
8
+ data?: Record<string, unknown>;
9
+ }
10
+ export type Action = HaCallServiceAction | {
11
+ type: 'mqtt.publish';
12
+ topic: string;
13
+ payload: string;
14
+ retain?: boolean;
15
+ impliesEntity?: string;
16
+ } | {
17
+ type: 'timer.start';
18
+ timerKey: string;
19
+ delayMs: number;
20
+ } | {
21
+ type: 'timer.cancel';
22
+ timerKey: string;
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import type { HAState, HAContext } from '../framework/ha-client.js';
2
+ import type { Trigger, TriggerEvent } from './triggers.js';
3
+ import type { Action } from './actions.js';
4
+ export type { HAState, HAContext };
5
+ export interface Decision {
6
+ decision: string;
7
+ reason?: string;
8
+ actions: Action[];
9
+ conditions?: Record<string, unknown>;
10
+ }
11
+ export type Abort = {
12
+ abort: true;
13
+ reason: string;
14
+ };
15
+ export declare const abort: (reason: string) => Abort;
16
+ export declare function isAbort(value: unknown): value is Abort;
17
+ export declare class UnavailableInputError extends Error {
18
+ readonly entityId: string;
19
+ constructor(entityId: string);
20
+ }
21
+ export declare function requireState(state: HAState, entityId: Parameters<HAState>[0]): string;
22
+ export declare function requireNumericState(state: HAState, entityId: Parameters<HAState>[0]): number;
23
+ export interface Automation<C> {
24
+ id: string;
25
+ location: string;
26
+ subsystem: string;
27
+ enabled?: boolean;
28
+ triggers: Trigger[];
29
+ context: (state: HAState, ha: HAContext, event: TriggerEvent) => C | Abort;
30
+ reduce: (ctx: C) => Decision;
31
+ }
32
+ export declare function defineAutomation<C>(automation: Automation<C>): Automation<C>;
@@ -0,0 +1,44 @@
1
+ export const abort = (reason) => ({ abort: true, reason });
2
+ export function isAbort(value) {
3
+ return typeof value === 'object' && value !== null && value.abort === true;
4
+ }
5
+ // ---------- Required state ----------
6
+ // Covers the dominant abort() pattern found across real automations (~70% of call sites,
7
+ // per #142's audit): a required entity's state is missing or invalid, so context() can't
8
+ // proceed. Thrown, not returned — pipeline.ts already wraps context() in try/catch, so this
9
+ // collapses `const x = state(id)?.state; if (x === undefined) return abort(...)` at every call
10
+ // site down to one line, with no per-call guard needed. Caught specially in pipeline.ts and
11
+ // classified as abort_kind: 'unavailable_input' with the entity name, distinct from a genuine
12
+ // bug (abort_kind: 'unhandled_error').
13
+ export class UnavailableInputError extends Error {
14
+ entityId;
15
+ constructor(entityId) {
16
+ super(`required entity unavailable: ${entityId}`);
17
+ this.entityId = entityId;
18
+ this.name = 'UnavailableInputError';
19
+ }
20
+ }
21
+ // HA's own sentinel values for "entity is registered but not producing readings" — an
22
+ // offline zigbee device, an integration that hasn't polled yet. Distinct from the entity
23
+ // key being absent from the state cache entirely, but the same "unavailable" in practice:
24
+ // every real audited call site treated these three cases identically.
25
+ const HA_UNAVAILABLE_STATES = new Set(['unavailable', 'unknown']);
26
+ export function requireState(state, entityId) {
27
+ const value = state(entityId)?.state;
28
+ if (value === undefined || HA_UNAVAILABLE_STATES.has(value)) {
29
+ throw new UnavailableInputError(String(entityId));
30
+ }
31
+ return value;
32
+ }
33
+ export function requireNumericState(state, entityId) {
34
+ const raw = requireState(state, entityId);
35
+ const parsed = parseFloat(raw);
36
+ if (!Number.isFinite(parsed))
37
+ throw new UnavailableInputError(String(entityId));
38
+ return parsed;
39
+ }
40
+ // Identity function — provides type inference on C so the reduce argument
41
+ // is typed correctly without the user annotating the context shape explicitly.
42
+ export function defineAutomation(automation) {
43
+ return automation;
44
+ }
@@ -0,0 +1,77 @@
1
+ import type { EntityState } from '../framework/ha-client.js';
2
+ type _HAEntityKey = keyof HAEntities extends never ? string : keyof HAEntities;
3
+ export type Trigger = {
4
+ type: 'state_changed';
5
+ entity: _HAEntityKey | RegExp;
6
+ to?: string | string[];
7
+ duration?: number;
8
+ } | {
9
+ type: 'schedule';
10
+ cron: string;
11
+ } | {
12
+ type: 'on_start';
13
+ } | {
14
+ type: 'timer_expired';
15
+ timerKey: string;
16
+ } | {
17
+ type: 'button';
18
+ entity: _HAEntityKey | RegExp;
19
+ gesture: 'single_press' | 'double_press' | 'hold';
20
+ button?: string;
21
+ } | {
22
+ type: 'mqtt_in';
23
+ topic: string;
24
+ };
25
+ type TriggerEventBase = {
26
+ correlation_id: string;
27
+ parent_correlation_id?: string;
28
+ root_correlation_id?: string;
29
+ parent_automation_id?: string;
30
+ };
31
+ export type TriggerEvent = TriggerEventBase & ({
32
+ type: 'state_changed';
33
+ entity_id: string;
34
+ old_state: EntityState | undefined;
35
+ new_state: EntityState;
36
+ } | {
37
+ type: 'schedule';
38
+ cron: string;
39
+ } | {
40
+ type: 'on_start';
41
+ } | {
42
+ type: 'timer_expired';
43
+ timerKey: string;
44
+ } | {
45
+ type: 'button';
46
+ entity_id: string;
47
+ gesture: 'single_press' | 'double_press' | 'hold';
48
+ button?: string;
49
+ } | {
50
+ type: 'mqtt_in';
51
+ topic: string;
52
+ payload: string;
53
+ });
54
+ export type TriggerSummary = {
55
+ type: 'state_changed';
56
+ entity_id: string;
57
+ to: string;
58
+ from?: string;
59
+ } | {
60
+ type: 'schedule';
61
+ cron: string;
62
+ } | {
63
+ type: 'on_start';
64
+ } | {
65
+ type: 'timer_expired';
66
+ timerKey: string;
67
+ } | {
68
+ type: 'button';
69
+ entity_id: string;
70
+ gesture: 'single_press' | 'double_press' | 'hold';
71
+ button?: string;
72
+ } | {
73
+ type: 'mqtt_in';
74
+ topic: string;
75
+ };
76
+ export declare function summarizeTrigger(event: TriggerEvent): TriggerSummary;
77
+ export {};
@@ -0,0 +1,26 @@
1
+ export function summarizeTrigger(event) {
2
+ switch (event.type) {
3
+ case 'state_changed':
4
+ return {
5
+ type: 'state_changed',
6
+ entity_id: event.entity_id,
7
+ to: event.new_state.state,
8
+ ...(event.old_state && { from: event.old_state.state }),
9
+ };
10
+ case 'schedule':
11
+ return { type: 'schedule', cron: event.cron };
12
+ case 'on_start':
13
+ return { type: 'on_start' };
14
+ case 'timer_expired':
15
+ return { type: 'timer_expired', timerKey: event.timerKey };
16
+ case 'button':
17
+ return {
18
+ type: 'button',
19
+ entity_id: event.entity_id,
20
+ gesture: event.gesture,
21
+ ...(event.button !== undefined && { button: event.button }),
22
+ };
23
+ case 'mqtt_in':
24
+ return { type: 'mqtt_in', topic: event.topic };
25
+ }
26
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@ajclarkson/homerun",
3
+ "type": "module",
4
+ "version": "0.0.1-edge.0131fc6",
5
+ "description": "TypeScript automation framework for Home Assistant",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "main": "./dist/src/lib.js",
10
+ "types": "./dist/src/lib.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/src/lib.d.ts",
14
+ "default": "./dist/src/lib.js"
15
+ },
16
+ "./testing": {
17
+ "types": "./dist/src/testing.d.ts",
18
+ "default": "./dist/src/testing.js"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "dev": "tsx watch src/index.ts",
23
+ "build": "tsc",
24
+ "test": "vitest",
25
+ "test:run": "vitest run",
26
+ "generate:ha-types": "tsx scripts/generate-ha-types.ts",
27
+ "generate:ha-types:compiled": "homerun-generate-ha-types",
28
+ "generate:ha-services": "tsx scripts/generate-ha-services.ts",
29
+ "generate:ha-services:compiled": "homerun-generate-ha-services"
30
+ },
31
+ "bin": {
32
+ "homerun": "./dist/src/index.js",
33
+ "homerun-generate-ha-types": "./dist/scripts/generate-ha-types.js",
34
+ "homerun-generate-ha-services": "./dist/scripts/generate-ha-services.js"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/ajclarkson/homerun"
39
+ },
40
+ "keywords": [],
41
+ "author": "Adam Clarkson",
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "chokidar": "^5.0.0",
45
+ "dotenv": "^17.4.2",
46
+ "esbuild": "^0.28.1",
47
+ "home-assistant-js-websocket": "^9.6.0",
48
+ "js-yaml": "^5.2.1",
49
+ "mqtt": "^5.15.1",
50
+ "node-cron": "^4.4.1",
51
+ "prom-client": "^15.1.3",
52
+ "zod": "^4.4.3"
53
+ },
54
+ "devDependencies": {
55
+ "@types/js-yaml": "^4.0.9",
56
+ "@types/node": "^26.0.0",
57
+ "tsx": "^4.22.4",
58
+ "typescript": "^7.0.0",
59
+ "vitest": "^4.1.9"
60
+ }
61
+ }