feat(smarthome): add smart home features and Home Assistant integration (WebSocket protocol, discovery, factories, interfaces)
This commit is contained in:
@@ -54,3 +54,6 @@ export {
|
||||
type TUpsTestResult,
|
||||
type IUpsSnmpStatus,
|
||||
} from './protocol.upssnmp.js';
|
||||
|
||||
// Home Assistant WebSocket protocol
|
||||
export { HomeAssistantProtocol } from './protocol.homeassistant.js';
|
||||
|
||||
737
ts/protocols/protocol.homeassistant.ts
Normal file
737
ts/protocols/protocol.homeassistant.ts
Normal file
@@ -0,0 +1,737 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type {
|
||||
IHomeAssistantInstanceConfig,
|
||||
IHomeAssistantEntity,
|
||||
IHomeAssistantConfig,
|
||||
IHomeAssistantStateChangedEvent,
|
||||
IHomeAssistantMessage,
|
||||
IHomeAssistantAuthRequired,
|
||||
IHomeAssistantAuthOk,
|
||||
IHomeAssistantAuthInvalid,
|
||||
IHomeAssistantResult,
|
||||
IHomeAssistantEvent,
|
||||
THomeAssistantProtocolEvents,
|
||||
IHomeAssistantLightServiceData,
|
||||
IHomeAssistantClimateServiceData,
|
||||
IHomeAssistantCoverServiceData,
|
||||
IHomeAssistantFanServiceData,
|
||||
IHomeAssistantMediaPlayerServiceData,
|
||||
} from '../interfaces/homeassistant.interfaces.js';
|
||||
|
||||
/**
|
||||
* Home Assistant WebSocket Protocol Handler
|
||||
* Connects to HA via WebSocket, handles authentication, state subscriptions, and service calls
|
||||
*/
|
||||
export class HomeAssistantProtocol extends plugins.events.EventEmitter {
|
||||
private ws: InstanceType<typeof plugins.WebSocket> | null = null;
|
||||
private config: IHomeAssistantInstanceConfig;
|
||||
private messageId: number = 1;
|
||||
private pendingRequests: Map<number, {
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeout: ReturnType<typeof setTimeout>;
|
||||
}> = new Map();
|
||||
private isAuthenticated: boolean = false;
|
||||
private haConfig: IHomeAssistantConfig | null = null;
|
||||
private reconnectAttempt: number = 0;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private stateSubscriptionId: number | null = null;
|
||||
private entityStates: Map<string, IHomeAssistantEntity> = new Map();
|
||||
private intentionalDisconnect: boolean = false;
|
||||
|
||||
constructor(config: IHomeAssistantInstanceConfig) {
|
||||
super();
|
||||
this.config = {
|
||||
port: 8123,
|
||||
secure: false,
|
||||
autoReconnect: true,
|
||||
reconnectDelay: 5000,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WebSocket URL for this HA instance
|
||||
*/
|
||||
private get wsUrl(): string {
|
||||
const protocol = this.config.secure ? 'wss' : 'ws';
|
||||
return `${protocol}://${this.config.host}:${this.config.port}/api/websocket`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection state
|
||||
*/
|
||||
public get isConnected(): boolean {
|
||||
return this.ws !== null && this.ws.readyState === plugins.WebSocket.OPEN && this.isAuthenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HA config if authenticated
|
||||
*/
|
||||
public get homeAssistantConfig(): IHomeAssistantConfig | null {
|
||||
return this.haConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached entity states
|
||||
*/
|
||||
public get entities(): Map<string, IHomeAssistantEntity> {
|
||||
return this.entityStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Home Assistant
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
if (this.ws && this.ws.readyState === plugins.WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.intentionalDisconnect = false;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.ws = new plugins.WebSocket(this.wsUrl);
|
||||
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
reject(new Error(`Connection timeout to ${this.wsUrl}`));
|
||||
}, 10000);
|
||||
|
||||
this.ws.on('open', () => {
|
||||
// Connection established, waiting for auth_required
|
||||
});
|
||||
|
||||
this.ws.on('message', async (data: Buffer | string) => {
|
||||
const message = JSON.parse(data.toString()) as IHomeAssistantMessage;
|
||||
|
||||
if (message.type === 'auth_required') {
|
||||
// Send authentication
|
||||
await this.sendAuth();
|
||||
} else if (message.type === 'auth_ok') {
|
||||
clearTimeout(connectionTimeout);
|
||||
this.isAuthenticated = true;
|
||||
this.reconnectAttempt = 0;
|
||||
|
||||
// Get HA config
|
||||
try {
|
||||
this.haConfig = await this.getConfig();
|
||||
this.emit('authenticated', this.haConfig);
|
||||
} catch (err) {
|
||||
// Non-fatal, continue anyway
|
||||
}
|
||||
|
||||
this.emit('connected');
|
||||
resolve();
|
||||
} else if (message.type === 'auth_invalid') {
|
||||
clearTimeout(connectionTimeout);
|
||||
const authInvalid = message as IHomeAssistantAuthInvalid;
|
||||
this.emit('auth:failed', authInvalid.message);
|
||||
reject(new Error(`Authentication failed: ${authInvalid.message}`));
|
||||
} else {
|
||||
// Handle other messages
|
||||
this.handleMessage(message);
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('error', (error: Error) => {
|
||||
this.emit('error', error);
|
||||
});
|
||||
|
||||
this.ws.on('close', () => {
|
||||
this.isAuthenticated = false;
|
||||
this.stateSubscriptionId = null;
|
||||
|
||||
// Reject all pending requests
|
||||
for (const [id, request] of this.pendingRequests) {
|
||||
clearTimeout(request.timeout);
|
||||
request.reject(new Error('Connection closed'));
|
||||
this.pendingRequests.delete(id);
|
||||
}
|
||||
|
||||
this.emit('disconnected');
|
||||
|
||||
// Auto-reconnect if not intentional
|
||||
if (this.config.autoReconnect && !this.intentionalDisconnect) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from Home Assistant
|
||||
*/
|
||||
public async disconnect(): Promise<void> {
|
||||
this.intentionalDisconnect = true;
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
// Clear all pending requests
|
||||
for (const [id, request] of this.pendingRequests) {
|
||||
clearTimeout(request.timeout);
|
||||
request.reject(new Error('Disconnecting'));
|
||||
this.pendingRequests.delete(id);
|
||||
}
|
||||
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
this.isAuthenticated = false;
|
||||
this.stateSubscriptionId = null;
|
||||
this.entityStates.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send authentication message
|
||||
*/
|
||||
private async sendAuth(): Promise<void> {
|
||||
if (!this.ws) return;
|
||||
|
||||
const authMessage = {
|
||||
type: 'auth',
|
||||
access_token: this.config.token,
|
||||
};
|
||||
|
||||
this.ws.send(JSON.stringify(authMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming messages
|
||||
*/
|
||||
private handleMessage(message: IHomeAssistantMessage): void {
|
||||
if (message.type === 'result') {
|
||||
const result = message as IHomeAssistantResult;
|
||||
const pending = this.pendingRequests.get(result.id);
|
||||
|
||||
if (pending) {
|
||||
clearTimeout(pending.timeout);
|
||||
this.pendingRequests.delete(result.id);
|
||||
|
||||
if (result.success) {
|
||||
pending.resolve(result.result);
|
||||
} else {
|
||||
pending.reject(new Error(result.error?.message || 'Unknown error'));
|
||||
}
|
||||
}
|
||||
} else if (message.type === 'event') {
|
||||
const event = message as IHomeAssistantEvent;
|
||||
|
||||
if (event.event.event_type === 'state_changed') {
|
||||
const stateChanged = event.event.data as IHomeAssistantStateChangedEvent;
|
||||
|
||||
// Update cached state
|
||||
if (stateChanged.new_state) {
|
||||
this.entityStates.set(stateChanged.entity_id, stateChanged.new_state);
|
||||
} else {
|
||||
this.entityStates.delete(stateChanged.entity_id);
|
||||
}
|
||||
|
||||
this.emit('state:changed', stateChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request and wait for response
|
||||
*/
|
||||
private async sendRequest<T>(type: string, data: Record<string, unknown> = {}): Promise<T> {
|
||||
if (!this.ws || !this.isAuthenticated) {
|
||||
throw new Error('Not connected to Home Assistant');
|
||||
}
|
||||
|
||||
const id = this.messageId++;
|
||||
const message = { id, type, ...data };
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(id);
|
||||
reject(new Error(`Request timeout: ${type}`));
|
||||
}, 30000);
|
||||
|
||||
this.pendingRequests.set(id, {
|
||||
resolve: resolve as (value: unknown) => void,
|
||||
reject,
|
||||
timeout,
|
||||
});
|
||||
|
||||
this.ws!.send(JSON.stringify(message));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule reconnection attempt
|
||||
*/
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectTimer) return;
|
||||
|
||||
this.reconnectAttempt++;
|
||||
const delay = Math.min(
|
||||
this.config.reconnectDelay! * Math.pow(1.5, this.reconnectAttempt - 1),
|
||||
60000 // Max 60 seconds
|
||||
);
|
||||
|
||||
this.emit('reconnecting', this.reconnectAttempt);
|
||||
|
||||
this.reconnectTimer = setTimeout(async () => {
|
||||
this.reconnectTimer = null;
|
||||
|
||||
try {
|
||||
await this.connect();
|
||||
|
||||
// Re-subscribe to state changes
|
||||
if (this.isConnected) {
|
||||
await this.subscribeToStateChanges();
|
||||
}
|
||||
} catch {
|
||||
// connect() will schedule another reconnect on failure
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Public API - State
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Get HA config
|
||||
*/
|
||||
public async getConfig(): Promise<IHomeAssistantConfig> {
|
||||
return this.sendRequest<IHomeAssistantConfig>('get_config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to state change events
|
||||
*/
|
||||
public async subscribeToStateChanges(): Promise<number> {
|
||||
const result = await this.sendRequest<{ context: { id: string } }>('subscribe_events', {
|
||||
event_type: 'state_changed',
|
||||
});
|
||||
|
||||
// Get all current states after subscribing
|
||||
const states = await this.getStates();
|
||||
for (const entity of states) {
|
||||
this.entityStates.set(entity.entity_id, entity);
|
||||
}
|
||||
this.emit('states:loaded', states);
|
||||
|
||||
this.stateSubscriptionId = this.messageId - 1;
|
||||
return this.stateSubscriptionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entity states
|
||||
*/
|
||||
public async getStates(): Promise<IHomeAssistantEntity[]> {
|
||||
return this.sendRequest<IHomeAssistantEntity[]>('get_states');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific entity state
|
||||
*/
|
||||
public async getState(entityId: string): Promise<IHomeAssistantEntity | null> {
|
||||
// First check cache
|
||||
const cached = this.entityStates.get(entityId);
|
||||
if (cached) return cached;
|
||||
|
||||
// Otherwise fetch all states and find it
|
||||
const states = await this.getStates();
|
||||
return states.find((s) => s.entity_id === entityId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entities by domain
|
||||
*/
|
||||
public async getEntitiesByDomain(domain: string): Promise<IHomeAssistantEntity[]> {
|
||||
const states = await this.getStates();
|
||||
return states.filter((s) => s.entity_id.startsWith(`${domain}.`));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Public API - Service Calls
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Call a Home Assistant service
|
||||
*/
|
||||
public async callService(
|
||||
domain: string,
|
||||
service: string,
|
||||
target?: { entity_id?: string | string[]; device_id?: string | string[]; area_id?: string | string[] },
|
||||
serviceData?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
await this.sendRequest('call_service', {
|
||||
domain,
|
||||
service,
|
||||
target,
|
||||
service_data: serviceData,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn on an entity
|
||||
*/
|
||||
public async turnOn(entityId: string, data?: Record<string, unknown>): Promise<void> {
|
||||
const domain = entityId.split('.')[0];
|
||||
await this.callService(domain, 'turn_on', { entity_id: entityId }, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn off an entity
|
||||
*/
|
||||
public async turnOff(entityId: string): Promise<void> {
|
||||
const domain = entityId.split('.')[0];
|
||||
await this.callService(domain, 'turn_off', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle an entity
|
||||
*/
|
||||
public async toggle(entityId: string): Promise<void> {
|
||||
const domain = entityId.split('.')[0];
|
||||
await this.callService(domain, 'toggle', { entity_id: entityId });
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Light Services
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Control a light
|
||||
*/
|
||||
public async lightTurnOn(entityId: string, options?: IHomeAssistantLightServiceData): Promise<void> {
|
||||
await this.callService('light', 'turn_on', { entity_id: entityId }, options);
|
||||
}
|
||||
|
||||
public async lightTurnOff(entityId: string, options?: { transition?: number }): Promise<void> {
|
||||
await this.callService('light', 'turn_off', { entity_id: entityId }, options);
|
||||
}
|
||||
|
||||
public async lightToggle(entityId: string, options?: IHomeAssistantLightServiceData): Promise<void> {
|
||||
await this.callService('light', 'toggle', { entity_id: entityId }, options);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Climate Services
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Set HVAC mode
|
||||
*/
|
||||
public async climateSetHvacMode(entityId: string, hvacMode: string): Promise<void> {
|
||||
await this.callService('climate', 'set_hvac_mode', { entity_id: entityId }, { hvac_mode: hvacMode });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set target temperature
|
||||
*/
|
||||
public async climateSetTemperature(entityId: string, options: IHomeAssistantClimateServiceData): Promise<void> {
|
||||
await this.callService('climate', 'set_temperature', { entity_id: entityId }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fan mode
|
||||
*/
|
||||
public async climateSetFanMode(entityId: string, fanMode: string): Promise<void> {
|
||||
await this.callService('climate', 'set_fan_mode', { entity_id: entityId }, { fan_mode: fanMode });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set preset mode
|
||||
*/
|
||||
public async climateSetPresetMode(entityId: string, presetMode: string): Promise<void> {
|
||||
await this.callService('climate', 'set_preset_mode', { entity_id: entityId }, { preset_mode: presetMode });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set swing mode
|
||||
*/
|
||||
public async climateSetSwingMode(entityId: string, swingMode: string): Promise<void> {
|
||||
await this.callService('climate', 'set_swing_mode', { entity_id: entityId }, { swing_mode: swingMode });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set aux heat
|
||||
*/
|
||||
public async climateSetAuxHeat(entityId: string, auxHeat: boolean): Promise<void> {
|
||||
await this.callService('climate', 'set_aux_heat', { entity_id: entityId }, { aux_heat: auxHeat });
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Cover Services
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Open cover
|
||||
*/
|
||||
public async coverOpen(entityId: string): Promise<void> {
|
||||
await this.callService('cover', 'open_cover', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Close cover
|
||||
*/
|
||||
public async coverClose(entityId: string): Promise<void> {
|
||||
await this.callService('cover', 'close_cover', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop cover
|
||||
*/
|
||||
public async coverStop(entityId: string): Promise<void> {
|
||||
await this.callService('cover', 'stop_cover', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cover position
|
||||
*/
|
||||
public async coverSetPosition(entityId: string, position: number): Promise<void> {
|
||||
await this.callService('cover', 'set_cover_position', { entity_id: entityId }, { position });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cover tilt position
|
||||
*/
|
||||
public async coverSetTiltPosition(entityId: string, tiltPosition: number): Promise<void> {
|
||||
await this.callService('cover', 'set_cover_tilt_position', { entity_id: entityId }, { tilt_position: tiltPosition });
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Fan Services
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Turn on fan
|
||||
*/
|
||||
public async fanTurnOn(entityId: string, options?: IHomeAssistantFanServiceData): Promise<void> {
|
||||
await this.callService('fan', 'turn_on', { entity_id: entityId }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn off fan
|
||||
*/
|
||||
public async fanTurnOff(entityId: string): Promise<void> {
|
||||
await this.callService('fan', 'turn_off', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fan percentage
|
||||
*/
|
||||
public async fanSetPercentage(entityId: string, percentage: number): Promise<void> {
|
||||
await this.callService('fan', 'set_percentage', { entity_id: entityId }, { percentage });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fan preset mode
|
||||
*/
|
||||
public async fanSetPresetMode(entityId: string, presetMode: string): Promise<void> {
|
||||
await this.callService('fan', 'set_preset_mode', { entity_id: entityId }, { preset_mode: presetMode });
|
||||
}
|
||||
|
||||
/**
|
||||
* Oscillate fan
|
||||
*/
|
||||
public async fanOscillate(entityId: string, oscillating: boolean): Promise<void> {
|
||||
await this.callService('fan', 'oscillate', { entity_id: entityId }, { oscillating });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fan direction
|
||||
*/
|
||||
public async fanSetDirection(entityId: string, direction: 'forward' | 'reverse'): Promise<void> {
|
||||
await this.callService('fan', 'set_direction', { entity_id: entityId }, { direction });
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Lock Services
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Lock
|
||||
*/
|
||||
public async lockLock(entityId: string): Promise<void> {
|
||||
await this.callService('lock', 'lock', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock
|
||||
*/
|
||||
public async lockUnlock(entityId: string): Promise<void> {
|
||||
await this.callService('lock', 'unlock', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Open (if supported)
|
||||
*/
|
||||
public async lockOpen(entityId: string): Promise<void> {
|
||||
await this.callService('lock', 'open', { entity_id: entityId });
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Switch Services
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Turn on switch
|
||||
*/
|
||||
public async switchTurnOn(entityId: string): Promise<void> {
|
||||
await this.callService('switch', 'turn_on', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn off switch
|
||||
*/
|
||||
public async switchTurnOff(entityId: string): Promise<void> {
|
||||
await this.callService('switch', 'turn_off', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle switch
|
||||
*/
|
||||
public async switchToggle(entityId: string): Promise<void> {
|
||||
await this.callService('switch', 'toggle', { entity_id: entityId });
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Media Player Services
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Play media
|
||||
*/
|
||||
public async mediaPlayerPlay(entityId: string): Promise<void> {
|
||||
await this.callService('media_player', 'media_play', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause media
|
||||
*/
|
||||
public async mediaPlayerPause(entityId: string): Promise<void> {
|
||||
await this.callService('media_player', 'media_pause', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop media
|
||||
*/
|
||||
public async mediaPlayerStop(entityId: string): Promise<void> {
|
||||
await this.callService('media_player', 'media_stop', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Next track
|
||||
*/
|
||||
public async mediaPlayerNext(entityId: string): Promise<void> {
|
||||
await this.callService('media_player', 'media_next_track', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Previous track
|
||||
*/
|
||||
public async mediaPlayerPrevious(entityId: string): Promise<void> {
|
||||
await this.callService('media_player', 'media_previous_track', { entity_id: entityId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set volume
|
||||
*/
|
||||
public async mediaPlayerSetVolume(entityId: string, volumeLevel: number): Promise<void> {
|
||||
await this.callService('media_player', 'volume_set', { entity_id: entityId }, { volume_level: volumeLevel });
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute/unmute
|
||||
*/
|
||||
public async mediaPlayerMute(entityId: string, isMuted: boolean): Promise<void> {
|
||||
await this.callService('media_player', 'volume_mute', { entity_id: entityId }, { is_volume_muted: isMuted });
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to position
|
||||
*/
|
||||
public async mediaPlayerSeek(entityId: string, position: number): Promise<void> {
|
||||
await this.callService('media_player', 'media_seek', { entity_id: entityId }, { seek_position: position });
|
||||
}
|
||||
|
||||
/**
|
||||
* Select source
|
||||
*/
|
||||
public async mediaPlayerSelectSource(entityId: string, source: string): Promise<void> {
|
||||
await this.callService('media_player', 'select_source', { entity_id: entityId }, { source });
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Camera Services
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Get camera snapshot URL
|
||||
*/
|
||||
public getCameraSnapshotUrl(entityId: string): string {
|
||||
const protocol = this.config.secure ? 'https' : 'http';
|
||||
const entity = this.entityStates.get(entityId);
|
||||
const accessToken = (entity?.attributes as { access_token?: string })?.access_token || '';
|
||||
return `${protocol}://${this.config.host}:${this.config.port}/api/camera_proxy/${entityId}?token=${accessToken}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get camera stream URL
|
||||
*/
|
||||
public getCameraStreamUrl(entityId: string): string {
|
||||
const protocol = this.config.secure ? 'https' : 'http';
|
||||
return `${protocol}://${this.config.host}:${this.config.port}/api/camera_proxy_stream/${entityId}`;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Static Helpers
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Probe if a Home Assistant instance is reachable
|
||||
*/
|
||||
public static async probe(host: string, port: number = 8123, secure: boolean = false, timeout: number = 5000): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const protocol = secure ? 'wss' : 'ws';
|
||||
const url = `${protocol}://${host}:${port}/api/websocket`;
|
||||
|
||||
try {
|
||||
const ws = new plugins.WebSocket(url);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
ws.close();
|
||||
resolve(false);
|
||||
}, timeout);
|
||||
|
||||
ws.on('open', () => {
|
||||
// Wait for auth_required message
|
||||
});
|
||||
|
||||
ws.on('message', (data: Buffer | string) => {
|
||||
const message = JSON.parse(data.toString()) as IHomeAssistantMessage;
|
||||
if (message.type === 'auth_required') {
|
||||
clearTimeout(timer);
|
||||
ws.close();
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', () => {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user