117 lines
4.2 KiB
TypeScript
117 lines
4.2 KiB
TypeScript
|
|
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.';
|
||
|
|
}
|
||
|
|
}
|