homebridge-homeconnect
Version:
A Homebridge plugin that connects Home Connect appliances to Apple HomeKit
155 lines • 6.91 kB
JavaScript
// Homebridge plugin for Home Connect home appliances
// Copyright © 2023-2025 Alexander Thoukydides
import { keyofChecker } from '../../utils.js';
import { cloneTemplate, getElementById } from './utils-dom.js';
import { typeSuite } from '../../ti/config-types.js';
// The current plugin configuration
export class Config {
// Create a new configuration object
constructor(log, ipc) {
this.log = log;
this.ipc = ipc;
// The configuration being edited
this.global = {};
this.appliances = {};
this.readyPromise = this.getConfig();
this.activePromise = this.getActiveConfig();
}
// Retrieve and parse the configuration to be edited
async getConfig() {
// Retrieve the current plugin configuration (if any)
const configArray = await window.homebridge.getPluginConfig();
if (configArray[0] === undefined) {
this.log.warn('No plugin configuration found; creating a new one');
this.savedConfig = { platform: window.homebridge.plugin.displayName };
}
else {
if (1 < configArray.length) {
this.log.error(`Using the first of ${configArray.length} plugin configurations`, configArray);
window.homebridge.toast.error('Only a single platform instance is supported; using the first', 'Multiple Configuration Blocks');
}
this.savedConfig = configArray[0];
this.checkIfModified(this.savedConfig);
}
// Treat all unexpected properties as appliance configurations
const keyofConfigPlugin = keyofChecker(typeSuite, typeSuite.ConfigPlugin);
const select = (predicate) => Object.fromEntries(Object.entries(this.savedConfig ?? {}).filter(predicate));
this.global = select(([key]) => keyofConfigPlugin.includes(key));
this.appliances = select(([key]) => !keyofConfigPlugin.includes(key));
this.log.debug('getConfig() global %O appliances %O', this.global, this.appliances);
this.onGlobal?.(this.global);
}
// Retrieve the most recently used configuration
async getActiveConfig() {
try {
return await this.ipc.request('/config', null);
}
catch {
return {};
}
}
// Update the homebridge-ui-x copy of the configuration
async putConfig(save = false) {
// Construct the full configuration
const config = { ...this.global, ...this.appliances };
this.log.debug(`putConfig(${save})'}`, config);
// Inform homebridge-ui-x of the new configuration
await window.homebridge.updatePluginConfig([config]);
if (save) {
await window.homebridge.savePluginConfig();
this.savedConfig = config;
this.log.info('Plugin configuration saved');
}
// Check whether the configuration matches the active plugin
this.checkIfModified(config);
}
// Compare two configuration objects
diffObject(to, from, keyPrefix = '') {
const keys = [...Object.keys(to), ...Object.keys(from)]
.filter((key, index, self) => index === self.indexOf(key) && key !== '_bridge');
const diff = [];
for (const key of keys) {
const valueTo = to[key];
const valueFrom = from[key];
const keyName = `${keyPrefix}.${key}`;
const isObject = (value) => value !== undefined && typeof value === 'object';
if (Array.isArray(valueTo) && Array.isArray(valueFrom))
diff.push(...this.diffArray(valueTo, valueFrom, keyName));
else if (isObject(valueTo) && isObject(valueFrom))
diff.push(...this.diffObject(valueTo, valueFrom, keyName));
else if (valueTo !== valueFrom)
diff.push({ key: keyName, from: valueFrom, to: valueTo });
}
return diff;
}
// Compare two configuration arrays
diffArray(to, from, keyPrefix = '') {
const isSimple = (array) => array.length !== 0 && typeof array[0] !== 'object';
if (isSimple(to) || isSimple(from)) {
if (to.length === from.length && to.every((value, index) => value === from[index]))
return [];
else
return [{ key: keyPrefix, from, to }];
}
else {
return this.diffObject(to, from, keyPrefix);
}
}
// Check whether the configuration has been modified
async checkIfModified(config) {
// Ignore completely new/empty configuration
if (!Object.keys(config).length)
return;
// Compare the configuration to the active plugin and saved versions
const activeConfig = await this.activePromise;
const activeDiff = this.diffObject(config, activeConfig);
const savedDiff = this.diffObject(config, this.savedConfig ?? {});
if (activeDiff.length || savedDiff.length) {
this.log.debug('checkIfModified(%O) active %O saved %O', config, activeDiff, savedDiff);
}
// Hide or show the restart required message
this.showRestartRequired(activeDiff, 0 < savedDiff.length);
}
// Show or hide the restart required message
showRestartRequired(changes, isUnsaved) {
// Show the message if there are any differences
getElementById('hc-modified').hidden = changes.length === 0 && !isUnsaved;
getElementById('hc-modified-saved').hidden = isUnsaved;
getElementById('hc-modified-unsaved').hidden = !isUnsaved;
// Display the diff
const formatValue = (value) => JSON.stringify(value, null, 4);
getElementById('hc-modified-diff').replaceChildren(...changes.map(change => cloneTemplate('hc-modified-diff-delta', {
key: change.key,
from: formatValue(change.from),
to: formatValue(change.to)
})));
}
// Get the current global configuration
async getGlobal() {
await this.readyPromise;
return this.global;
}
// Set updated global configuration
async setGlobal(config) {
if (this.diffObject(config, await this.getGlobal()).length) {
this.log.debug('setGlobal(%O)', config);
this.global = config;
this.onGlobal?.(config);
await this.putConfig();
}
}
// Get the current configuration for a specific appliance
async getAppliance(haid) {
await this.readyPromise;
return this.appliances[haid] ?? {};
}
// Set updated global configuration
async setAppliance(haid, config) {
if (this.diffObject(config, await this.getAppliance(haid)).length) {
this.log.debug(`setAppliance("${haid}", %O)`, config);
this.appliances[haid] = config;
await this.putConfig();
}
}
}
//# sourceMappingURL=config.js.map