Files
integrations/ts/integrations/esphome/esphome.classes.client.ts
T

117 lines
4.2 KiB
TypeScript
Raw Normal View History

2026-05-05 14:57:06 +00:00
import type { IEsphomeClientCommand, IEsphomeCommandResult, IEsphomeConfig, IEsphomeEvent, IEsphomeSnapshot } from './esphome.types.js';
import { EsphomeMapper } from './esphome.mapper.js';
type TEsphomeEventHandler = (eventArg: IEsphomeEvent) => void;
export class EsphomeClient {
private readonly events: IEsphomeEvent[] = [];
private readonly eventHandlers = new Set<TEsphomeEventHandler>();
constructor(private readonly config: IEsphomeConfig) {}
public async getSnapshot(): Promise<IEsphomeSnapshot> {
return EsphomeMapper.toSnapshot(this.config, undefined, this.events);
}
public onEvent(handlerArg: TEsphomeEventHandler): () => void {
this.eventHandlers.add(handlerArg);
return () => this.eventHandlers.delete(handlerArg);
}
public async sendCommand(commandArg: IEsphomeClientCommand): Promise<IEsphomeCommandResult> {
let result: IEsphomeCommandResult;
if (this.config.commandExecutor) {
result = this.commandResult(await this.config.commandExecutor(commandArg), commandArg);
} else {
result = {
success: false,
error: this.unsupportedLiveControlMessage(),
data: { command: commandArg },
};
}
this.emit({
type: result.success ? 'command_mapped' : 'command_failed',
command: commandArg,
data: result,
timestamp: Date.now(),
deviceId: this.stringValue(commandArg.deviceId),
entityId: commandArg.entityId,
});
return result;
}
public async connectLive(): Promise<void> {
await new EsphomeNativeApiConnection(this.config).connect();
}
public async destroy(): Promise<void> {
this.eventHandlers.clear();
}
private emit(eventArg: IEsphomeEvent): void {
this.events.push(eventArg);
for (const handler of this.eventHandlers) {
handler(eventArg);
}
}
private commandResult(resultArg: unknown, commandArg: IEsphomeClientCommand): IEsphomeCommandResult {
if (this.isCommandResult(resultArg)) {
return resultArg;
}
return { success: true, data: resultArg ?? { command: commandArg } };
}
private isCommandResult(valueArg: unknown): valueArg is IEsphomeCommandResult {
return typeof valueArg === 'object' && valueArg !== null && 'success' in valueArg;
}
private unsupportedLiveControlMessage(): string {
if (this.hasEncryptionKey()) {
return 'ESPHome live native API writes require protobuf framing plus Noise encryption support, which is not implemented. The mapped command was not sent.';
}
if (this.hasPassword()) {
return 'ESPHome live native API writes require protobuf framing plus legacy password login support, which is not implemented. The mapped command was not sent.';
}
return 'ESPHome live native API writes require protobuf framing, which is not implemented. The mapped command was not sent.';
}
private hasEncryptionKey(): boolean {
return Boolean(this.config.encryptionKey || this.config.noisePsk || this.config.manualEntries?.some((entryArg) => entryArg.encryptionKey || entryArg.noisePsk));
}
private hasPassword(): boolean {
return Boolean(this.config.password || this.config.manualEntries?.some((entryArg) => entryArg.password));
}
private stringValue(valueArg: unknown): string | undefined {
if (typeof valueArg === 'string') {
return valueArg;
}
if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
return String(valueArg);
}
return undefined;
}
}
export class EsphomeNativeApiConnection {
constructor(private readonly config: IEsphomeConfig) {}
public async connect(): Promise<void> {
throw new Error(this.unsupportedMessage());
}
public async sendCommand(commandArg: IEsphomeClientCommand): Promise<void> {
void commandArg;
throw new Error(this.unsupportedMessage());
}
private unsupportedMessage(): string {
if (this.config.encryptionKey || this.config.noisePsk || this.config.manualEntries?.some((entryArg) => entryArg.encryptionKey || entryArg.noisePsk)) {
return 'Encrypted ESPHome native API uses Noise plus protobuf over TCP; live TCP support is intentionally not implemented in this dependency-free port.';
}
return 'ESPHome native API uses protobuf over TCP; live TCP support is intentionally not implemented in this dependency-free port.';
}
}