UNPKG

homebridge-homeconnect

Version:

A Homebridge plugin that connects Home Connect appliances to Apple HomeKit

303 lines 11.2 kB
// Homebridge plugin for Home Connect home appliances // Copyright © 2023 Alexander Thoukydides import { setImmediate as setImmediateP } from 'timers/promises'; import { OperationState } from '../api-value-types.js'; import { APIStatusCodeError } from '../api-errors.js'; // Base class for a mock appliance export class MockAppliance { // Create a new API object constructor(log) { this.log = log; // Optional appliance details this.brand = 'Mock'; this.status = []; this.settings = []; this.commands = []; this.programs = []; // Other appliance state this.connected = true; // Event stream this.dataEventQueue = { STATUS: [], NOTIFY: [], EVENT: [] }; this.emitEventPromise = new Promise(resolve => { this.emitEventResolve = resolve; }); } // Derived identifiers for this appliance get vib() { return this.enumber.split('/')[0] ?? ''; } get haid() { return `${this.brand.toUpperCase()}-${this.vib}-0123456789AB`; } // Get details of the mock appliance getAppliance() { return { brand: this.brand, connected: this.connected, enumber: this.enumber, haId: this.haid, name: this.name ?? `Mock ${this.type}`, type: this.type, vib: this.vib }; } // Get all programs getPrograms() { const getProgramPartial = (op) => { try { return op(); } catch { return {}; } }; return { programs: this.programs.map(p => ({ key: p.key, name: p.name, constraints: { available: true, execution: 'selectandstart' } })), selected: getProgramPartial(() => this.getSelectedProgram()), active: getProgramPartial(() => this.getActiveProgram()) }; } // Get a list of the available programs getAvailablePrograms() { return this.getPrograms(); } // Get the details of a specific available programs getAvailableProgram(key) { const program = this.programs.find(p => p.key === key); if (!program) throw MockAppliance.statusCodeError(409, 'SDK.Error.UnsupportedProgram', key); return program; } // Get the program which is currently being executed getActiveProgram() { if (this.isOperationState('Inactive', 'Ready') || !this.program) throw MockAppliance.statusCodeError(404, 'SDK.Error.NoProgramActive'); return this.program; } // Start a specified program setActiveProgram(key, options = []) { this.setProgramWithDefaultOptions(key, options); this.emitNotifyEvent('BSH.Common.Root.ActiveProgram', key); this.setStatus('BSH.Common.Status.OperationState', OperationState.Run); } // Stop the active program stopActiveProgram() { this.setStatus('BSH.Common.Status.OperationState', OperationState.Ready); this.emitNotifyEvent('BSH.Common.Root.ActiveProgram', null); } // Get all options of the active program getActiveProgramOptions() { return this.getActiveProgram().options ?? []; } // Set all options of the active program setActiveProgramOptions(options) { for (const option of options) { const activeOption = this.getActiveProgramOption(option.key); activeOption.value = option.value; this.emitNotifyEvent(option.key, option.value); } } // Get a specific option of the active program getActiveProgramOption(key) { const option = this.getActiveProgramOptions().find(o => o.key === key); if (!option) throw MockAppliance.statusCodeError(409, 'SDK.Error.UnsupportedOption', key); return option; } // Set a specific option of the active program setActiveProgramOption(key, value) { const selectedOption = this.getActiveProgramOption(key); selectedOption.value = value; this.emitNotifyEvent(key, value); } // Get the program which is currently selected getSelectedProgram() { if (!this.program) throw MockAppliance.statusCodeError(404, 'SDK.Error.NoProgramSelected'); return this.program; } // Select a program setSelectedProgram(key, options) { this.setProgramWithDefaultOptions(key, options); this.emitNotifyEvent('BSH.Common.Root.SelectedProgram', key); } // Get all options of the selected program getSelectedProgramOptions() { return this.getSelectedProgram().options ?? []; } // Set all options of the selected program setSelectedProgramOptions(options) { for (const option of options) { const selectedOption = this.getSelectedProgramOption(option.key); selectedOption.value = option.value; this.emitNotifyEvent(option.key, option.value); } } // Get a specific option of the selected program getSelectedProgramOption(key) { const option = this.getSelectedProgramOptions().find(o => o.key === key); if (!option) throw MockAppliance.statusCodeError(409, 'SDK.Error.UnsupportedOption', key); return option; } // Set a specific option of the selected program setSelectedProgramOption(key, value) { const selectedOption = this.getSelectedProgramOption(key); selectedOption.value = value; this.emitNotifyEvent(key, value); } // Select a program and apply its initial options setProgramWithDefaultOptions(key, options = []) { // First check whether the supplied options are valid const programOptions = this.getAvailableProgram(key).options ?? []; for (const option of options) { const programOption = programOptions.some(o => o.key === option.key); if (!programOption) throw MockAppliance.statusCodeError(409, 'SDK.Error.UnsupportedOption', option.key); } // Select the program this.program = { key, options: [] }; this.emitNotifyEvent('BSH.Common.Root.SelectedProgram', key); // Set its options this.program.options = programOptions.map((option) => { const requested = options.find(o => o.key === option.key); let value = requested?.value ?? option.constraints?.default; switch (option.type) { case 'Double': case 'Int': value ?? (value = option.constraints?.min ?? 0); break; case 'Boolean': value ?? (value = false); break; default: value ?? (value = option.constraints?.allowedvalues?.[0] ?? ''); } if (value !== undefined) this.emitNotifyEvent(option.key, value); return { key: option.key, name: option.name, value: value, unit: option.unit }; }); } // Get the current status getStatus() { return this.status; } // Get a specific status getStatusSpecific(key) { const status = this.status.find(s => s.key === key); if (!status) throw MockAppliance.statusCodeError(409, 'SDK.Error.UnsupportedStatus', key); return status; } // Set a specific status setStatus(key, value) { this.log.debug(`Mock status ${key} <= ${String(value)}`); const status = this.getStatusSpecific(key); status.value = value; this.emitStatusEvent(key, value); } // Get all settings getSettings() { return this.settings; } // Get a specific setting getSetting(key) { const setting = this.settings.find(s => s.key === key); if (!setting) throw MockAppliance.statusCodeError(409, 'SDK.Error.UnsupportedSetting', key); return setting; } // Set a specific setting setSetting(key, value) { this.log.debug(`Mock setting ${key} <= ${String(value)}`); const setting = this.getSetting(key); setting.value = value; this.emitNotifyEvent(key, value); } // Get a list of supported commands getCommands() { return this.commands; } // Issue a command setCommand(key) { this.log.debug(`Mock command ${key}`); } // Emit a CONNECTED/DISCONNECTED/PAIRED/DEPAIRED event emitConnectedEvent(event) { this.log.debug(`Mock event ${event}`); this.emitEventResolve({ event, id: this.haid }); } // Emit a NOTIFY event emitNotifyEvent(key, value) { this.emitDataEvent('NOTIFY', key, value); } // Emit a STATUS event emitStatusEvent(key, value) { this.emitDataEvent('STATUS', key, value); } // Emit an EVENT event emitEventEvent(key, value) { this.emitDataEvent('EVENT', key, value); } // Emit a NOTIFY/STATUS/EVENT event emitDataEvent(event, key, value) { // Emit the event after collecting all pending data const emitEvent = async () => { await setImmediateP(); const eventWithData = { event, id: this.haid, data: { items: this.dataEventQueue[event] } }; this.emitEventResolve(eventWithData); this.dataEventQueue[event] = []; }; if (!this.dataEventQueue[event].length) emitEvent(); // Queue the data this.dataEventQueue[event].push({ key, value, timestamp: Math.floor(Date.now() / 1000), level: 'info', handling: 'none' }); this.log.debug(`Mock event ${event}(${this.dataEventQueue[event].length}) ${key}=${value}`); } // Get events for the mock appliance async *getEvents() { for (;;) { const event = await this.emitEventPromise; this.emitEventPromise = new Promise(resolve => this.emitEventResolve = resolve); yield event; } } // Test whether the current OperationState is one of the specified values isOperationState(...states) { try { const operationState = this.getStatusSpecific('BSH.Common.Status.OperationState').value; return states.map(state => OperationState[state]).includes(operationState); } catch { return false; } } // Create an APIStatusCodeError with a specified key static statusCodeError(statusCode, errorKey, itemKey = 'n/a') { const request = { method: 'MOCK', path: itemKey }; const response = { statusCode }; const body = { error: { key: errorKey } }; return new APIStatusCodeError(request, response, JSON.stringify(body)); } } //# sourceMappingURL=mock-appliance.js.map