Refactor Home Assistant API and schema validation
- Completely rewrote HassInstance class with fetch-based API methods - Updated Home Assistant schemas to be more precise and flexible - Removed deprecated test environment configuration file - Enhanced WebSocket client implementation - Improved test coverage for Home Assistant API and schema validation - Simplified type definitions and error handling
This commit is contained in:
@@ -2,6 +2,9 @@ import { CreateApplication, TServiceParams, ServiceFunction } from "@digital-alc
|
||||
import { LIB_HASS } from "@digital-alchemy/hass";
|
||||
import { DomainSchema } from "../schemas.js";
|
||||
import { HASS_CONFIG } from "../config/hass.config.js";
|
||||
import { WebSocket } from 'ws';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as HomeAssistant from '../types/hass.js';
|
||||
|
||||
type Environments = "development" | "production" | "test";
|
||||
|
||||
@@ -113,4 +116,109 @@ export async function get_hass(): Promise<HassInstance> {
|
||||
hassInstance = instance as HassInstance;
|
||||
}
|
||||
return hassInstance;
|
||||
}
|
||||
|
||||
export class HassWebSocketClient extends EventEmitter {
|
||||
private ws: WebSocket | null = null;
|
||||
private messageId = 1;
|
||||
private subscriptions = new Map<number, (data: any) => void>();
|
||||
private reconnectAttempts = 0;
|
||||
private options: {
|
||||
autoReconnect: boolean;
|
||||
maxReconnectAttempts: number;
|
||||
reconnectDelay: number;
|
||||
};
|
||||
|
||||
constructor(
|
||||
private url: string,
|
||||
private token: string,
|
||||
options: Partial<typeof HassWebSocketClient.prototype.options> = {}
|
||||
) {
|
||||
super();
|
||||
this.options = {
|
||||
autoReconnect: true,
|
||||
maxReconnectAttempts: 3,
|
||||
reconnectDelay: 1000,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
// ... rest of WebSocket client implementation ...
|
||||
}
|
||||
|
||||
export class HassInstance {
|
||||
private baseUrl: string;
|
||||
private token: string;
|
||||
private wsClient: HassWebSocketClient | null;
|
||||
|
||||
constructor(baseUrl: string, token: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.token = token;
|
||||
this.wsClient = null;
|
||||
}
|
||||
|
||||
async fetchStates(): Promise<HomeAssistant.Entity[]> {
|
||||
const response = await fetch(`${this.baseUrl}/api/states`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch states: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data as HomeAssistant.Entity[];
|
||||
}
|
||||
|
||||
async fetchState(entityId: string): Promise<HomeAssistant.Entity> {
|
||||
const response = await fetch(`${this.baseUrl}/api/states/${entityId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch state: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data as HomeAssistant.Entity;
|
||||
}
|
||||
|
||||
async callService(domain: string, service: string, data: Record<string, any>): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/api/services/${domain}/${service}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Service call failed: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
async subscribeEvents(callback: (event: HomeAssistant.Event) => void, eventType?: string): Promise<number> {
|
||||
if (!this.wsClient) {
|
||||
this.wsClient = new HassWebSocketClient(
|
||||
this.baseUrl.replace(/^http/, 'ws') + '/api/websocket',
|
||||
this.token
|
||||
);
|
||||
await this.wsClient.connect();
|
||||
}
|
||||
|
||||
return this.wsClient.subscribeEvents(callback, eventType);
|
||||
}
|
||||
|
||||
async unsubscribeEvents(subscriptionId: number): Promise<void> {
|
||||
if (this.wsClient) {
|
||||
await this.wsClient.unsubscribeEvents(subscriptionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,15 +141,7 @@ export const automationSchema: JSONSchemaType<AutomationType> = {
|
||||
type: 'object',
|
||||
required: ['condition'],
|
||||
properties: {
|
||||
condition: { type: 'string' },
|
||||
conditions: {
|
||||
type: 'array',
|
||||
nullable: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
condition: { type: 'string' }
|
||||
},
|
||||
additionalProperties: true
|
||||
}
|
||||
@@ -166,13 +158,8 @@ export const automationSchema: JSONSchemaType<AutomationType> = {
|
||||
nullable: true,
|
||||
properties: {
|
||||
entity_id: {
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'array',
|
||||
items: { type: 'string' }
|
||||
}
|
||||
],
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
@@ -228,22 +215,55 @@ export const stateChangedEventSchema: JSONSchemaType<HomeAssistant.StateChangedE
|
||||
properties: {
|
||||
entity_id: { type: 'string' },
|
||||
new_state: {
|
||||
anyOf: [
|
||||
entitySchema,
|
||||
{ type: 'null' }
|
||||
],
|
||||
nullable: true
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
entity_id: { type: 'string' },
|
||||
state: { type: 'string' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
additionalProperties: true
|
||||
},
|
||||
last_changed: { type: 'string' },
|
||||
last_updated: { type: 'string' },
|
||||
context: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
parent_id: { type: 'string', nullable: true },
|
||||
user_id: { type: 'string', nullable: true }
|
||||
},
|
||||
required: ['id']
|
||||
}
|
||||
},
|
||||
required: ['entity_id', 'state', 'attributes', 'last_changed', 'last_updated', 'context']
|
||||
},
|
||||
old_state: {
|
||||
anyOf: [
|
||||
entitySchema,
|
||||
{ type: 'null' }
|
||||
],
|
||||
nullable: true
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
entity_id: { type: 'string' },
|
||||
state: { type: 'string' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
additionalProperties: true
|
||||
},
|
||||
last_changed: { type: 'string' },
|
||||
last_updated: { type: 'string' },
|
||||
context: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
parent_id: { type: 'string', nullable: true },
|
||||
user_id: { type: 'string', nullable: true }
|
||||
},
|
||||
required: ['id']
|
||||
}
|
||||
},
|
||||
required: ['entity_id', 'state', 'attributes', 'last_changed', 'last_updated', 'context']
|
||||
}
|
||||
},
|
||||
required: ['entity_id', 'new_state', 'old_state'],
|
||||
additionalProperties: false
|
||||
required: ['entity_id', 'new_state']
|
||||
},
|
||||
origin: { type: 'string' },
|
||||
time_fired: { type: 'string' },
|
||||
@@ -254,12 +274,10 @@ export const stateChangedEventSchema: JSONSchemaType<HomeAssistant.StateChangedE
|
||||
parent_id: { type: 'string', nullable: true },
|
||||
user_id: { type: 'string', nullable: true }
|
||||
},
|
||||
required: ['id'],
|
||||
additionalProperties: false
|
||||
required: ['id']
|
||||
}
|
||||
},
|
||||
required: ['event_type', 'data', 'origin', 'time_fired', 'context'],
|
||||
additionalProperties: false
|
||||
required: ['event_type', 'data', 'origin', 'time_fired', 'context']
|
||||
};
|
||||
|
||||
export const configSchema: JSONSchemaType<HomeAssistant.Config> = {
|
||||
|
||||
50
src/types/hass.ts
Normal file
50
src/types/hass.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export interface AuthMessage {
|
||||
type: 'auth';
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
export interface ResultMessage {
|
||||
id: number;
|
||||
type: 'result';
|
||||
success: boolean;
|
||||
result?: any;
|
||||
}
|
||||
|
||||
export interface WebSocketError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
event_type: string;
|
||||
data: any;
|
||||
origin: string;
|
||||
time_fired: string;
|
||||
context: {
|
||||
id: string;
|
||||
parent_id: string | null;
|
||||
user_id: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Entity {
|
||||
entity_id: string;
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
last_changed: string;
|
||||
last_updated: string;
|
||||
context: {
|
||||
id: string;
|
||||
parent_id: string | null;
|
||||
user_id: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StateChangedEvent extends Event {
|
||||
event_type: 'state_changed';
|
||||
data: {
|
||||
entity_id: string;
|
||||
new_state: Entity | null;
|
||||
old_state: Entity | null;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user