homebridge-homeconnect
Version:
A Homebridge plugin that connects Home Connect appliances to Apple HomeKit
224 lines • 10.2 kB
JavaScript
// 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