diff --git a/src/hass/index.ts b/src/hass/index.ts index fba1d9a..29e9718 100644 --- a/src/hass/index.ts +++ b/src/hass/index.ts @@ -1,10 +1,11 @@ import { CreateApplication, TServiceParams, ServiceFunction, AlsExtension, GetApisResult, ILogger, InternalDefinition, TContext, TInjectedConfig, TLifecycleBase, TScheduler } from "@digital-alchemy/core"; import { Area, Backup, CallProxy, Configure, Device, EntityManager, EventsService, FetchAPI, FetchInternals, Floor, IDByExtension, Label, LIB_HASS, ReferenceService, Registry, WebsocketAPI, Zone } from "@digital-alchemy/hass"; import { DomainSchema } from "../schemas.js"; -import { HASS_CONFIG } from "../config/hass.config.js"; -import { WebSocket } from 'ws'; +import { HASS_CONFIG } from "../config/index.js"; +import WebSocket from 'ws'; import { EventEmitter } from 'events'; import * as HomeAssistant from '../types/hass.js'; +import { HassEntity, HassEvent, HassService } from '../interfaces/hass.js'; type Environments = "development" | "production" | "test"; @@ -18,38 +19,23 @@ type HassServices = { }; // Define the type for Home Assistant instance -interface HassInstance extends TServiceParams { - baseUrl: string; - token: string; - wsClient: HassWebSocketClient | undefined; - services: HassServices; - als: AlsExtension; - context: TContext; - event: EventEmitter<[never]>; - internal: InternalDefinition; - lifecycle: TLifecycleBase; - logger: ILogger; - scheduler: TScheduler; - config: TInjectedConfig; - params: TServiceParams; - hass: GetApisResult<{ - area: typeof Area; - backup: typeof Backup; - call: typeof CallProxy; - configure: typeof Configure; - device: typeof Device; - entity: typeof EntityManager; - events: typeof EventsService; - fetch: typeof FetchAPI; - floor: typeof Floor; - idBy: typeof IDByExtension; - internals: typeof FetchInternals; - label: typeof Label; - refBy: typeof ReferenceService; - registry: typeof Registry; - socket: typeof WebsocketAPI; - zone: typeof Zone; - }>; +interface HassInstance { + states: { + get: () => Promise; + subscribe: (callback: (states: HassEntity[]) => void) => Promise; + unsubscribe: (subscription: number) => void; + }; + services: { + get: () => Promise>>; + call: (domain: string, service: string, serviceData?: Record) => Promise; + }; + connection: { + socket: WebSocket; + subscribeEvents: (callback: (event: HassEvent) => void, eventType?: string) => Promise; + unsubscribeEvents: (subscription: number) => void; + }; + subscribeEvents: (callback: (event: HassEvent) => void, eventType?: string) => Promise; + unsubscribeEvents: (subscription: number) => void; } // Configuration type for application with more specific constraints @@ -416,14 +402,208 @@ export class HassInstanceImpl implements HassInstance { } } -let hassInstance: HassInstance | null = null; +class HomeAssistantInstance implements HassInstance { + private messageId = 1; + private messageCallbacks = new Map void>(); + private eventCallbacks = new Map void>(); + private stateCallbacks = new Map void>(); + private _authenticated = false; + private socket: WebSocket; + private readonly _states: HassInstance['states']; + private readonly _services: HassInstance['services']; + private readonly _connection: HassInstance['connection']; + + constructor() { + if (!HASS_CONFIG.TOKEN) { + throw new Error('Home Assistant token is required'); + } + + this.socket = new WebSocket(HASS_CONFIG.SOCKET_URL); + + this._states = { + get: async (): Promise => { + const message = { + type: 'get_states' + }; + return this.sendMessage(message); + }, + + subscribe: async (callback: (states: HassEntity[]) => void): Promise => { + const id = this.messageId++; + this.stateCallbacks.set(id, callback); + + const message = { + type: 'subscribe_events', + event_type: 'state_changed' + }; + + await this.sendMessage(message); + return id; + }, + + unsubscribe: (subscription: number): void => { + this.stateCallbacks.delete(subscription); + } + }; + + this._services = { + get: async (): Promise>> => { + const message = { + type: 'get_services' + }; + return this.sendMessage(message); + }, + + call: async (domain: string, service: string, serviceData?: Record): Promise => { + const message = { + type: 'call_service', + domain, + service, + service_data: serviceData + }; + await this.sendMessage(message); + } + }; + + this._connection = { + socket: this.socket, + subscribeEvents: this.subscribeEvents.bind(this), + unsubscribeEvents: this.unsubscribeEvents.bind(this) + }; + + this.setupWebSocket(); + } + + get authenticated(): boolean { + return this._authenticated; + } + + get states(): HassInstance['states'] { + return this._states; + } + + get services(): HassInstance['services'] { + return this._services; + } + + get connection(): HassInstance['connection'] { + return this._connection; + } + + private setupWebSocket() { + this.socket.on('open', () => { + this.authenticate(); + }); + + this.socket.on('message', (data: WebSocket.Data) => { + if (typeof data === 'string') { + const message = JSON.parse(data); + this.handleMessage(message); + } + }); + + this.socket.on('close', () => { + console.log('WebSocket connection closed'); + // Implement reconnection logic here + }); + + this.socket.on('error', (error) => { + console.error('WebSocket error:', error); + }); + } + + private authenticate() { + const auth = { + type: 'auth', + access_token: HASS_CONFIG.TOKEN + }; + this.socket.send(JSON.stringify(auth)); + } + + private handleMessage(message: any) { + if (message.type === 'auth_ok') { + this._authenticated = true; + console.log('Authenticated with Home Assistant'); + return; + } + + if (message.type === 'auth_invalid') { + console.error('Authentication failed:', message.message); + return; + } + + if (message.type === 'event') { + const callback = this.eventCallbacks.get(message.id); + if (callback) { + callback(message.event); + } + return; + } + + if (message.type === 'result') { + const callback = this.messageCallbacks.get(message.id); + if (callback) { + callback(message.result); + this.messageCallbacks.delete(message.id); + } + return; + } + } + + private async sendMessage(message: any): Promise { + if (!this._authenticated) { + throw new Error('Not authenticated with Home Assistant'); + } + + return new Promise((resolve, reject) => { + const id = this.messageId++; + message.id = id; + + this.messageCallbacks.set(id, resolve); + this.socket.send(JSON.stringify(message)); + + // Add timeout + setTimeout(() => { + this.messageCallbacks.delete(id); + reject(new Error('Message timeout')); + }, 10000); + }); + } + + public async subscribeEvents(callback: (event: HassEvent) => void, eventType?: string): Promise { + const id = this.messageId++; + this.eventCallbacks.set(id, callback); + + const message = { + type: 'subscribe_events', + event_type: eventType + }; + + await this.sendMessage(message); + return id; + } + + public unsubscribeEvents(subscription: number): void { + this.eventCallbacks.delete(subscription); + } +} + +let hassInstance: HomeAssistantInstance | null = null; export async function get_hass(): Promise { if (!hassInstance) { - // Safely get configuration keys, providing an empty object as fallback - const _sortedConfigKeys = Object.keys(MY_APP.configuration ?? {}).sort(); - const instance = await MY_APP.bootstrap(); - hassInstance = instance as HassInstance; + hassInstance = new HomeAssistantInstance(); + // Wait for authentication + await new Promise((resolve) => { + const checkAuth = () => { + if (hassInstance?.authenticated) { + resolve(); + } else { + setTimeout(checkAuth, 100); + } + }; + checkAuth(); + }); } return hassInstance; } \ No newline at end of file diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 99d71f8..49ad9eb 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -54,6 +54,7 @@ export interface HassState { }; } +// Add-on interfaces export interface HassAddon { name: string; slug: string; @@ -178,4 +179,7 @@ export interface AutomationConfigParams { condition?: any[]; action: any[]; }; -} \ No newline at end of file +} + +// Re-export Home Assistant types +export * from './hass.js'; \ No newline at end of file diff --git a/src/services/tools.ts b/src/services/tools.ts index 953fe93..9e65f90 100644 --- a/src/services/tools.ts +++ b/src/services/tools.ts @@ -1,8 +1,7 @@ import { z } from 'zod'; -import { Tool } from '../interfaces/index.js'; +import { Tool, HassEntity } from '../interfaces/index.js'; import { get_hass } from '../hass/index.js'; import { DomainSchema } from '../schemas.js'; -import { HassEntity, HassState } from '../interfaces/index.js'; // Define tools array export const tools: Tool[] = [