@ajclarkson/homerun 0.0.1-edge.90fa3ca → 0.0.1-edge.a233fa3

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.
@@ -3,7 +3,6 @@ import type { Action } from '../types/actions.js';
3
3
  import type { HAClient } from './ha-client.js';
4
4
  import type { TimerManager } from './timer-manager.js';
5
5
  import type { EventPublisher } from './event-publisher.js';
6
- import type { MetricsBackend } from './metrics.js';
7
6
  export interface ExecutionContext {
8
7
  correlationId: string;
9
8
  automationId: string;
@@ -16,7 +15,6 @@ interface Deps {
16
15
  timerManager: TimerManager;
17
16
  eventPublisher: EventPublisher;
18
17
  dryRun: boolean;
19
- metrics?: MetricsBackend;
20
18
  }
21
19
  export declare class ActionRuntime {
22
20
  private readonly deps;
@@ -9,23 +9,14 @@ export class ActionRuntime {
9
9
  }
10
10
  }
11
11
  async runAction(action, ctx) {
12
- const labels = { location: ctx.location, action_type: action.type };
13
12
  this.deps.eventPublisher.publishActionEvent(this.makeEvent(ctx, 'action_started', action));
14
- this.deps.metrics?.incrementCounter('homerun_actions_dispatched_total', labels);
15
- const start = performance.now();
16
13
  try {
17
14
  if (!this.deps.dryRun) {
18
15
  await this.dispatch(action);
19
16
  }
20
- const duration = (performance.now() - start) / 1000;
21
- this.deps.metrics?.observeHistogram('homerun_action_duration_seconds', duration, labels);
22
- this.deps.metrics?.incrementCounter('homerun_actions_succeeded_total', labels);
23
17
  this.deps.eventPublisher.publishActionEvent(this.makeEvent(ctx, 'action_result', action, { reason: 'ok' }));
24
18
  }
25
19
  catch (err) {
26
- const duration = (performance.now() - start) / 1000;
27
- this.deps.metrics?.observeHistogram('homerun_action_duration_seconds', duration, labels);
28
- this.deps.metrics?.incrementCounter('homerun_actions_failed_total', labels);
29
20
  const reason = err instanceof Error ? err.message : String(err);
30
21
  this.deps.eventPublisher.publishActionEvent(this.makeEvent(ctx, 'action_result', action, { reason }));
31
22
  }
@@ -2,10 +2,6 @@ import type { AutomationRegistry } from './registry.js';
2
2
  import type { EventPublisher } from './event-publisher.js';
3
3
  import type { Automation } from '../types/automation.js';
4
4
  import type { TriggerEvent } from '../types/triggers.js';
5
- export interface MetricsProvider {
6
- getMetrics(): Promise<string>;
7
- contentType: string;
8
- }
9
5
  export interface ApiServerDeps {
10
6
  registry: AutomationRegistry;
11
7
  onTrigger: (automation: Automation<unknown>, event: TriggerEvent) => void;
@@ -14,7 +10,6 @@ export interface ApiServerDeps {
14
10
  entityCount: () => number;
15
11
  eventPublisher: EventPublisher;
16
12
  dryRun?: boolean;
17
- metrics?: MetricsProvider;
18
13
  }
19
14
  export declare class ApiServer {
20
15
  private readonly deps;
@@ -30,6 +25,5 @@ export declare class ApiServer {
30
25
  private postReload;
31
26
  private getHealthLive;
32
27
  private getHealthReady;
33
- private getMetrics;
34
28
  private getEvents;
35
29
  }
@@ -41,8 +41,6 @@ export class ApiServer {
41
41
  return this.getHealthReady(res);
42
42
  if (method === 'GET' && url === '/events')
43
43
  return this.getEvents(req, res);
44
- if (method === 'GET' && url === '/metrics')
45
- return this.getMetrics(res);
46
44
  const triggerMatch = method === 'POST' && url.match(/^\/automations\/(.+)\/trigger$/);
47
45
  if (triggerMatch)
48
46
  return this.postTrigger(triggerMatch[1], res);
@@ -90,21 +88,6 @@ export class ApiServer {
90
88
  ...(this.deps.dryRun && { dry_run: true }),
91
89
  });
92
90
  }
93
- getMetrics(res) {
94
- if (!this.deps.metrics) {
95
- json(res, 404, { error: 'metrics not enabled' });
96
- return;
97
- }
98
- this.deps.metrics.getMetrics()
99
- .then((output) => {
100
- res.writeHead(200, { 'Content-Type': this.deps.metrics.contentType });
101
- res.end(output);
102
- })
103
- .catch((err) => {
104
- console.error('[ApiServer] metrics scrape failed:', err);
105
- json(res, 500, { error: 'metrics scrape failed' });
106
- });
107
- }
108
91
  getEvents(req, res) {
109
92
  res.writeHead(200, {
110
93
  'Content-Type': 'text/event-stream',
@@ -17,9 +17,6 @@ declare const ConfigSchema: z.ZodObject<{
17
17
  options: z.ZodDefault<z.ZodObject<{
18
18
  dry_run: z.ZodDefault<z.ZodBoolean>;
19
19
  }, z.core.$strip>>;
20
- metrics: z.ZodDefault<z.ZodObject<{
21
- enabled: z.ZodDefault<z.ZodBoolean>;
22
- }, z.core.$strip>>;
23
20
  }, z.core.$strip>;
24
21
  export type HomerunConfig = z.infer<typeof ConfigSchema>;
25
22
  export declare function parseConfig(configContent: string, secretsContent?: string): HomerunConfig;
@@ -29,9 +29,6 @@ const ConfigSchema = z.object({
29
29
  options: z.object({
30
30
  dry_run: z.boolean().default(false),
31
31
  }).default({ dry_run: false }),
32
- metrics: z.object({
33
- enabled: z.boolean().default(false),
34
- }).default({ enabled: false }),
35
32
  });
36
33
  function resolveSecrets(value, secrets) {
37
34
  if (isSecretRef(value)) {
@@ -3,12 +3,10 @@ import type { TriggerEvent } from '../types/triggers.js';
3
3
  import type { HAClient } from './ha-client.js';
4
4
  import type { EventPublisher } from './event-publisher.js';
5
5
  import type { ActionRuntime } from './action-runtime.js';
6
- import type { MetricsBackend } from './metrics.js';
7
6
  interface Deps {
8
7
  eventPublisher: EventPublisher;
9
8
  actionRuntime: ActionRuntime;
10
9
  dryRun?: boolean;
11
- metrics?: MetricsBackend;
12
10
  }
13
11
  export declare function runPipeline(automation: Automation<unknown>, event: TriggerEvent, haClient: HAClient, deps: Deps): Promise<void>;
14
12
  export {};
@@ -1,9 +1,5 @@
1
1
  import { isAbort } from '../types/automation.js';
2
2
  export async function runPipeline(automation, event, haClient, deps) {
3
- deps.metrics?.incrementCounter('homerun_pipeline_runs_total', {
4
- location: automation.location,
5
- trigger_type: event.type,
6
- });
7
3
  const correlationId = event.correlation_id;
8
4
  const timestamp = new Date().toISOString();
9
5
  const base = {
@@ -3,7 +3,6 @@ import type { Automation } from '../types/automation.js';
3
3
  import type { TriggerEvent } from '../types/triggers.js';
4
4
  import type { HAClient } from './ha-client.js';
5
5
  import type { AutomationRegistry } from './registry.js';
6
- import type { MetricsBackend } from './metrics.js';
7
6
  export declare function parseButtonAction(state: string): {
8
7
  button?: string;
9
8
  pressType: 'short' | 'hold';
@@ -13,11 +12,10 @@ export declare class TriggerEngine {
13
12
  private readonly haClient;
14
13
  private readonly onMatch;
15
14
  private readonly mqttClient?;
16
- private readonly metrics?;
17
15
  private readonly buttonHandlers;
18
16
  private regexButtonTriggers;
19
17
  private readonly durationTimers;
20
- constructor(registry: AutomationRegistry, haClient: HAClient, onMatch: (automation: Automation<unknown>, event: TriggerEvent) => void, mqttClient?: MqttClient | undefined, metrics?: MetricsBackend | undefined);
18
+ constructor(registry: AutomationRegistry, haClient: HAClient, onMatch: (automation: Automation<unknown>, event: TriggerEvent) => void, mqttClient?: MqttClient | undefined);
21
19
  private rebuildButtonHandlers;
22
20
  start(): void;
23
21
  dispatch(event: TriggerEvent): void;
@@ -91,16 +91,14 @@ export class TriggerEngine {
91
91
  haClient;
92
92
  onMatch;
93
93
  mqttClient;
94
- metrics;
95
94
  buttonHandlers = new Map();
96
95
  regexButtonTriggers = [];
97
96
  durationTimers = new Map();
98
- constructor(registry, haClient, onMatch, mqttClient, metrics) {
97
+ constructor(registry, haClient, onMatch, mqttClient) {
99
98
  this.registry = registry;
100
99
  this.haClient = haClient;
101
100
  this.onMatch = onMatch;
102
101
  this.mqttClient = mqttClient;
103
- this.metrics = metrics;
104
102
  this.rebuildButtonHandlers();
105
103
  registry.onChange(() => this.rebuildButtonHandlers());
106
104
  }
@@ -136,7 +134,6 @@ export class TriggerEngine {
136
134
  this.haClient.ready
137
135
  .then(() => {
138
136
  this.haClient.on('state_changed', (event) => {
139
- this.metrics?.incrementCounter('homerun_ha_events_received_total', { event_type: 'state_changed' });
140
137
  let handler = this.buttonHandlers.get(event.entity_id);
141
138
  if (!handler) {
142
139
  const matchingGestures = new Set();
package/dist/src/index.js CHANGED
@@ -11,8 +11,6 @@ import { Scheduler } from './framework/scheduler.js';
11
11
  import { rescanAutomations, startHotReload } from './framework/hot-reload.js';
12
12
  import { runPipeline } from './framework/pipeline.js';
13
13
  import { ApiServer } from './framework/api-server.js';
14
- import { PromMetricsBackend } from './framework/metrics-prom.js';
15
- import { NoopMetricsBackend } from './framework/metrics.js';
16
14
  process.on('uncaughtException', (err) => {
17
15
  console.error('[homerun] uncaughtException:', err);
18
16
  });
@@ -22,7 +20,6 @@ process.on('unhandledRejection', (reason) => {
22
20
  // 0. Load and validate configuration before anything else.
23
21
  const config = await loadConfig();
24
22
  const { dry_run: dryRun } = config.options;
25
- const metricsBackend = config.metrics.enabled ? new PromMetricsBackend(true) : new NoopMetricsBackend();
26
23
  const lwtTopic = dryRun ? 'homerun/dev/status' : 'homerun/status';
27
24
  const lwtPayload = JSON.stringify({ status: 'offline', timestamp: new Date().toISOString() });
28
25
  // 1. Connect MQTT before anything else (EventPublisher and ActionRuntime need it).
@@ -47,7 +44,6 @@ const actionRuntime = new ActionRuntime({
47
44
  timerManager,
48
45
  eventPublisher,
49
46
  dryRun,
50
- metrics: metricsBackend,
51
47
  });
52
48
  // 3. Initial automation load — must complete before the engine and scheduler start.
53
49
  const automationsDir = path.resolve(config.automations.dir);
@@ -62,7 +58,7 @@ function dispatchPipeline(automation, event) {
62
58
  if (shuttingDown)
63
59
  return;
64
60
  inFlight++;
65
- runPipeline(automation, event, haClient, { eventPublisher, actionRuntime, dryRun, metrics: metricsBackend })
61
+ runPipeline(automation, event, haClient, { eventPublisher, actionRuntime, dryRun })
66
62
  .catch((err) => { console.error('[homerun] pipeline error:', err); })
67
63
  .finally(() => {
68
64
  inFlight--;
@@ -72,7 +68,7 @@ function dispatchPipeline(automation, event) {
72
68
  }
73
69
  });
74
70
  }
75
- engine = new TriggerEngine(registry, haClient, dispatchPipeline, mqtt, metricsBackend);
71
+ engine = new TriggerEngine(registry, haClient, dispatchPipeline, mqtt);
76
72
  const scheduler = new Scheduler(registry.getAll(), (e) => engine.dispatch(e), haClient.ready);
77
73
  engine.start();
78
74
  scheduler.start();
@@ -100,7 +96,6 @@ const apiServer = new ApiServer({
100
96
  entityCount: () => haClient.entityCount,
101
97
  eventPublisher,
102
98
  dryRun,
103
- metrics: config.metrics.enabled ? metricsBackend : undefined,
104
99
  });
105
100
  await apiServer.start(config.server.port);
106
101
  // 7. Connect to HA last — state_changed events start flowing once ready resolves.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ajclarkson/homerun",
3
3
  "type": "module",
4
- "version": "0.0.1-edge.90fa3ca",
4
+ "version": "0.0.1-edge.a233fa3",
5
5
  "description": "TypeScript automation framework for Home Assistant",
6
6
  "files": [
7
7
  "dist"
@@ -24,14 +24,11 @@
24
24
  "test": "vitest",
25
25
  "test:run": "vitest run",
26
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"
27
+ "generate:ha-types:compiled": "homerun-generate-ha-types"
30
28
  },
31
29
  "bin": {
32
30
  "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"
31
+ "homerun-generate-ha-types": "./dist/scripts/generate-ha-types.js"
35
32
  },
36
33
  "repository": {
37
34
  "type": "git",
@@ -48,7 +45,6 @@
48
45
  "js-yaml": "^5.2.1",
49
46
  "mqtt": "^5.15.1",
50
47
  "node-cron": "^4.4.1",
51
- "prom-client": "^15.1.3",
52
48
  "zod": "^4.4.3"
53
49
  },
54
50
  "devDependencies": {
@@ -1,16 +0,0 @@
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;
@@ -1,111 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
1
- import { Registry } from 'prom-client';
2
- import type { MetricsBackend } from './metrics.js';
3
- export declare class PromMetricsBackend implements MetricsBackend {
4
- readonly registry: Registry<"text/plain; version=0.0.4; charset=utf-8">;
5
- private readonly haEventsTotal;
6
- private readonly pipelineRunsTotal;
7
- private readonly actionsDispatchedTotal;
8
- private readonly actionsSucceededTotal;
9
- private readonly actionsFailedTotal;
10
- private readonly actionDurationSeconds;
11
- constructor(collectDefaults?: boolean);
12
- incrementCounter(name: string, labels?: Record<string, string>): void;
13
- observeHistogram(name: string, value: number, labels?: Record<string, string>): void;
14
- getMetrics(): Promise<string>;
15
- get contentType(): string;
16
- }
@@ -1,82 +0,0 @@
1
- import { Registry, Counter, Histogram, collectDefaultMetrics } from 'prom-client';
2
- export class PromMetricsBackend {
3
- registry = new Registry();
4
- haEventsTotal;
5
- pipelineRunsTotal;
6
- actionsDispatchedTotal;
7
- actionsSucceededTotal;
8
- actionsFailedTotal;
9
- actionDurationSeconds;
10
- constructor(collectDefaults = false) {
11
- if (collectDefaults) {
12
- collectDefaultMetrics({ register: this.registry });
13
- }
14
- this.haEventsTotal = new Counter({
15
- name: 'homerun_ha_events_received_total',
16
- help: 'Total HA and MQTT events received by homerun',
17
- labelNames: ['event_type'],
18
- registers: [this.registry],
19
- });
20
- this.pipelineRunsTotal = new Counter({
21
- name: 'homerun_pipeline_runs_total',
22
- help: 'Total automation pipeline runs',
23
- labelNames: ['location', 'trigger_type'],
24
- registers: [this.registry],
25
- });
26
- this.actionsDispatchedTotal = new Counter({
27
- name: 'homerun_actions_dispatched_total',
28
- help: 'Total actions dispatched by the action runtime',
29
- labelNames: ['location', 'action_type'],
30
- registers: [this.registry],
31
- });
32
- this.actionsSucceededTotal = new Counter({
33
- name: 'homerun_actions_succeeded_total',
34
- help: 'Total actions that completed successfully',
35
- labelNames: ['location', 'action_type'],
36
- registers: [this.registry],
37
- });
38
- this.actionsFailedTotal = new Counter({
39
- name: 'homerun_actions_failed_total',
40
- help: 'Total actions that failed with an error',
41
- labelNames: ['location', 'action_type'],
42
- registers: [this.registry],
43
- });
44
- this.actionDurationSeconds = new Histogram({
45
- name: 'homerun_action_duration_seconds',
46
- help: 'Duration of action execution in seconds',
47
- labelNames: ['location', 'action_type'],
48
- buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
49
- registers: [this.registry],
50
- });
51
- }
52
- incrementCounter(name, labels = {}) {
53
- switch (name) {
54
- case 'homerun_ha_events_received_total':
55
- this.haEventsTotal.inc(labels);
56
- break;
57
- case 'homerun_pipeline_runs_total':
58
- this.pipelineRunsTotal.inc(labels);
59
- break;
60
- case 'homerun_actions_dispatched_total':
61
- this.actionsDispatchedTotal.inc(labels);
62
- break;
63
- case 'homerun_actions_succeeded_total':
64
- this.actionsSucceededTotal.inc(labels);
65
- break;
66
- case 'homerun_actions_failed_total':
67
- this.actionsFailedTotal.inc(labels);
68
- break;
69
- }
70
- }
71
- observeHistogram(name, value, labels = {}) {
72
- if (name === 'homerun_action_duration_seconds') {
73
- this.actionDurationSeconds.observe(labels, value);
74
- }
75
- }
76
- async getMetrics() {
77
- return this.registry.metrics();
78
- }
79
- get contentType() {
80
- return this.registry.contentType;
81
- }
82
- }
@@ -1,8 +0,0 @@
1
- export interface MetricsBackend {
2
- incrementCounter(name: string, labels?: Record<string, string>): void;
3
- observeHistogram(name: string, value: number, labels?: Record<string, string>): void;
4
- }
5
- export declare class NoopMetricsBackend implements MetricsBackend {
6
- incrementCounter(): void;
7
- observeHistogram(): void;
8
- }
@@ -1,4 +0,0 @@
1
- export class NoopMetricsBackend {
2
- incrementCounter() { }
3
- observeHistogram() { }
4
- }