UNPKG

homebridge-homeconnect

Version:

A Homebridge plugin that connects Home Connect appliances to Apple HomeKit

89 lines 4.12 kB
// Homebridge plugin for Home Connect home appliances // Copyright © 2023-2025 Alexander Thoukydides import { EventEmitter } from 'events'; import { createCheckers } from 'ts-interface-checker'; import { formatMilliseconds } from './utils.js'; import { logError } from './log-error.js'; import { APIEventStreamError, APIValidationError } from './api-errors.js'; import apiTI from './ti/api-types-ti.js'; // Checkers for API responses const checkers = createCheckers(apiTI); // Home Connect API event stream export class APIEventStream extends EventEmitter { // Create a new event stream object constructor(log, ua) { super({ captureRejections: true }); this.log = log; this.ua = ua; super.on('error', (err) => logError(this.log, 'API event', err)); // Start an event stream when the first listener registers this.once('newListener', () => void this.startEventStream()); } // Emit events for a single appliance or all appliances async startEventStream(haid, eventName = 'event') { const description = `events stream for ${haid ?? 'all appliances'}`; const path = haid ? `/api/homeappliances/${haid}/events` : '/api/homeappliances/events'; for (;;) { const startTime = Date.now(); const elapsed = () => formatMilliseconds(Date.now() - startTime); try { // Start the event stream this.log.info(`Starting ${description}`); const { request, response, stream } = await this.ua.getStream(path); this.log.debug(`Started ${description} after ${elapsed()}`); this.emit(eventName, { event: 'START' }); // Dispatch events as they are received for await (const sse of stream) { const event = this.parseSSEToHomeConnect(request, response, sse); this.emit(eventName, event); } // Normal end of event stream this.emit(eventName, { event: 'STOP' }); } catch (err) { // Stream terminated due to an error logError(this.log, 'API event stream', err); this.emit(eventName, { event: 'STOP', err }); } finally { // Log completion of the stream this.log.debug(`Terminated ${description} after ${elapsed()}`); } } } // Parse an SSE event into a Home Connect event structure parseSSEToHomeConnect(request, response, sse) { const event = { ...sse }; // Attempt to parse any 'data' field as JSON if ('data' in sse && sse.data.length) { try { event.data = JSON.parse(sse.data); } catch (cause) { throw new APIEventStreamError(request, response, `Failed to parse JSON event data (${String(cause)})`, sse, { cause }); } // Workaround for Home Connect API bug if (event.data && typeof event.data === 'object' && 'haId' in event.data && !event.id) { this.log.debug('Applying workaround for issue #88'); event.id = event.data.haId; } } // Check that the response has the expected fields const checker = checkers.Event; checker.setReportedPath('event'); if (!checker.test(event)) { const validation = checker.validate(event) ?? []; this.ua.logCheckerValidation("error" /* LogLevel.ERROR */, 'Unexpected structure of Home Connect API event', request, validation, event); throw new APIValidationError(request, response, validation); } const strictValidation = checker.strictValidate(event); if (strictValidation) { this.ua.logCheckerValidation("warn" /* LogLevel.WARN */, 'Unexpected fields in Home Connect API event', request, strictValidation, event); } // Return the event return event; } } //# sourceMappingURL=api-events.js.map