UNPKG

homebridge-homeconnect

Version:

A Homebridge plugin that connects Home Connect appliances to Apple HomeKit

224 lines 10.2 kB
// Homebridge plugin for Home Connect home appliances // Copyright © 2023-2025 Alexander Thoukydides import assert from 'node:assert'; import { MS, assertIsDefined, getValidationTree, keyofChecker } from './utils.js'; import { APIKeyValuesLog } from './api-value.log.js'; import { typeSuite, checkers } from './ti/api-value-types.js'; // Minimum interval between reporting unrecognised keys const REPORT_INTERVAL = 24 * 60 * 60 * MS; // (24 hours in milliseconds) // Home Connect key-value type checkers export class APICheckValues { // Create a key-value type checker constructor(log, config, persist) { this.log = log; this.config = config; this.persist = persist; // Earliest to report an unrecognised key again this.nextKeyReport = {}; this.logValues = new APIKeyValuesLog(log, config.clientid, persist); } // Validation errors are logged, but values still returned with type assertion regardless // Check a list of appliances appliances(appliances) { this.logValues.setAppliances(appliances); return appliances; } // Check a single appliance appliance(haid, appliance) { assert.strictEqual(appliance.haId, haid); this.logValues.setAppliances([appliance]); return appliance; } // Check a list of programs programs(haid, programs) { const context = { haid, group: 'Program', json: {} }; programs.programs.forEach((program, index) => this.isLiteral(checkers.ProgramKey, { ...context, type: `Programs.programs[${index}]`, json: program }, program.key)); if (programs.selected?.key) this.program(haid, programs.selected, 'Programs.selected'); if (programs.active?.key) this.program(haid, programs.active, 'Programs.active'); return programs; } // Check a single program definition programDefinition(haid, program) { const context = { haid, group: 'Program', type: 'ProgramDefinition', json: program }; this.isLiteral(checkers.ProgramKey, context, program.key); if (program.options) this.optionDefinitions(haid, program.options); return program; } // Check a single program (selected/active) program(haid, program, type = 'Program') { const context = { haid, group: 'Program', type, json: program }; this.isLiteral(checkers.ProgramKey, context, program.key); if (program.options) this.options(haid, program.options); return program; } // Check a list of program options options(haid, options) { return options.map(option => this.option(haid, option)); } // Check a single program option option(haid, option) { const context = { haid, group: 'Option', json: option }; this.isKey(typeSuite, typeSuite.OptionValues, context, option.key); this.isValue(checkers.OptionValues, context, option.key, option.value); return option; } // Check a list of program option definitions optionDefinitions(haid, options) { return options.map(option => this.optionDefinition(haid, option)); } // Check a single program option definition optionDefinition(haid, option) { const context = { haid, group: 'Option', type: 'OptionDefinition', json: option }; this.logValues.addDetail(option); this.isKey(typeSuite, typeSuite.OptionValues, context, option.key); this.isConstraints(checkers.OptionValues, context, option.key, option.constraints); return option; } // Check a list of statuses statuses(haid, status) { return status.map(status => this.status(haid, status)); } // Check a single status status(haid, status) { const context = { haid, group: 'Status', json: status }; this.logValues.addDetail(status); this.isKey(typeSuite, typeSuite.StatusValues, context, status.key); this.isValue(checkers.StatusValues, context, status.key, status.value); this.isConstraints(checkers.StatusValues, context, status.key, status.constraints); return status; } // Check a list of settings settings(haid, settings) { return settings.map(setting => this.setting(haid, setting)); } // Check a single setting setting(haid, setting) { const context = { haid, group: 'Setting', json: setting }; this.logValues.addDetail(setting); this.isKey(typeSuite, typeSuite.SettingValues, context, setting.key); this.isValue(checkers.SettingValues, context, setting.key, setting.value); this.isConstraints(checkers.SettingValues, context, setting.key, setting.constraints); return setting; } // Check a list of commands commands(haid, commands) { commands.forEach(command => { const context = { haid, group: 'Command', json: command }; this.isKey(typeSuite, typeSuite.CommandValues, context, command.key); }); return commands; } // Check an event event(haid, event) { if ('data' in event && event.data) { const type = `${event.event} Event.data`; const props = typeSuite.EventMapValues.props; const typeName = props?.find(prop => prop.name === event.event)?.ttype.name; if (!typeName) { this.logValidation('Unrecognised event', type, event, [event.event]); } else { const check = (type, data) => { const context = { haid, group: 'Event', subGroup: event.event, type, json: data }; assertIsDefined(typeSuite[typeName]); assertIsDefined(checkers[typeName]); this.isKey(typeSuite, typeSuite[typeName], context, data.key); this.isValue(checkers[typeName], context, data.key, data.value); }; if ('items' in event.data) { event.data.items.forEach((data, index) => { check(`${type}.items[${index}]`, data); }); } else { check(type, event.data); } } } return event; } // Test whether a literal is recognised for the specified union type isLiteral(checker, context, literal) { // Test whether the key exist in the type const isCorrect = checker.test(literal); this.logValues.addValue(context.haid, 'ProgramKey', literal, !isCorrect); if (isCorrect) return true; // Log the unrecognised literal, avoiding frequent reports of the same value const now = Date.now(); const type = context.type ?? context.group; const nextKeyReport = this.nextKeyReport[literal]; if (nextKeyReport === undefined || nextKeyReport < now) { this.logValidation(`Unrecognised ${type} literal`, type, context.json, [literal]); } this.nextKeyReport[literal] = now + REPORT_INTERVAL; return false; } // Test whether a key is recognised for the specified type isKey(typeSuite, checkerType, context, key) { // Test whether the key exist in the type const isCorrect = keyofChecker(typeSuite, checkerType).includes(key); context.keyFailed = !isCorrect; this.logValues.addKey(context.haid, context.group, context.subGroup, key, !isCorrect); if (isCorrect) return true; // Log the unrecognised key, avoiding frequent reports of the same key const now = Date.now(); const type = context.type ?? context.group; const nextKeyReport = this.nextKeyReport[key]; if (nextKeyReport === undefined || nextKeyReport < now) { this.logValidation(`Unrecognised ${type}`, type, context.json, [key]); } this.nextKeyReport[key] = now + REPORT_INTERVAL; return false; } // Test whether constraints are of the correct type isConstraints(checker, context, key, constraints) { // Constraints are optional if (constraints === undefined) return true; const isValueResults = []; const isValue = (type, value) => { isValueResults.push(this.isValue(checker, { ...context, type }, key, value)); }; const type = (context.type ?? context.group).concat('.constraints'); // Check default value, if specified if (constraints.default !== undefined) { isValue(`${type}.default`, constraints.default); } // Check allowed values, if specified if ('allowedvalues' in constraints) { constraints.allowedvalues?.forEach((value, index) => { isValue(`${type}.allowedvalues[${index}]`, value); }); } // Return whether all tests passed return !isValueResults.includes(false); } // Test whether a value is of the correct type isValue(checker, context, key, value) { // Test whether the value has the expected type const kv = { [key]: value }; const validation = checker.validate(kv); const isCorrect = validation === null; // Key exists and value correct type this.logValues.addValue(context.haid, key, value, context.keyFailed === true || !isCorrect); if (context.keyFailed || isCorrect) return true; // Log details of the mismatched value type const type = context.type ?? context.group; this.logValidation(`Mismatched type for ${type} value`, type, context.json, [`${value} (type ${typeof value})`, ...getValidationTree(validation)]); return false; } // Log key-value validation errors logValidation(message, name, json, details) { this.log.info(`${message} in Home Connect API:`); for (const line of details) this.log.info(` ${line}`); this.log.debug(`Received ${name} (reformatted)`); const jsonLines = JSON.stringify(json, null, 4).split('\n'); for (const line of jsonLines) this.log.debug(` ${line}`); } } //# sourceMappingURL=api-value.js.map