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:
jango-blockchained
2025-01-30 10:51:25 +01:00
parent 732a727d27
commit 8152313f52
8 changed files with 561 additions and 200 deletions

View File

@@ -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);
}
}
}

View File

@@ -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
View 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;
};
}