UNPKG

homebridge-homeconnect

Version:

A Homebridge plugin that connects Home Connect appliances to Apple HomeKit

134 lines 7.23 kB
// Homebridge plugin for Home Connect home appliances // Copyright © 2026 Alexander Thoukydides import { assertIsDefined, assertIsNumber, plural } from './utils.js'; import { Ventilation } from './api-value-types.js'; // Add a ventilation fan to an accessory export function HasFanHob(Base) { return class HasFanHob extends Base { // Accessory services fanService; // Manual control fan speeds fanLevels = []; fanPercentStep = 0; // Mixin constructor constructor(...args) { super(...args); // Continue initialisation asynchronously this.asyncInitialise('Fan', this.initHasVentilation()); } // Asynchronous initialisation async initHasVentilation() { // Check whether the appliance supports ventilation fans const allSettings = await this.getCached('settings', () => this.device.getSettings()); if (!allSettings.some(s => s.key === 'Cooking.Hob.Setting.Ventilation')) { this.log.info('Does not support integrated ventilation'); return; } // Check whether a ventilation fan should be supported if (!this.hasOptionalFeature('Fan', 'Ventilation')) return; // Check the supported fan speeds const setting = await this.getCached('ventilation', () => this.device.getSetting('Cooking.Hob.Setting.Ventilation')); const allLevels = setting?.constraints?.allowedvalues ?? []; const fanLevels = allLevels.filter(level => level !== Ventilation.Off); if (!fanLevels.length) throw new Error('No ventilation levels'); this.log.info(`Fan supports ${plural(fanLevels.length, 'venting level')}`); // Select an appropriate rotation speed step size (suitable for Siri) // (allow low=25%, medium=50%, and high=100% for Siri) this.fanPercentStep = fanLevels.length <= 2 ? 100 / fanLevels.length : (fanLevels.length <= 4 ? 25 : 5); // Convert each rotation speed to a percentage this.fanLevels = fanLevels.map((level, index) => { const percent = (index + 1) * 100 / fanLevels.length; const rounded = Math.floor(percent / this.fanPercentStep) * this.fanPercentStep; return { level, percent: rounded }; }); // Tweak the percentage values to match Siri const siriPercent = { low: 25, medium: 50, high: 100 }; for (const [siri, percent] of Object.entries(siriPercent)) { const option = this.fromFanSpeedPercent(percent); const level = this.fanLevels.find(o => o.level === option.level); assertIsDefined(level); level.siri = siri; level.percent = percent; } // Verify that the fan speed mapping is stable for (const level of this.fanLevels) { const option = this.fromFanSpeedPercent(level.percent); if (level.level !== option.level) { this.log.error(`Unstable fan speed mapping: ${level.level} → ${level.percent}% → ${option.level}`); } this.log.info(` ${level.percent}% (${level.level})` + (level.siri ? ` = Siri '${level.siri}'` : '')); } // Add the fan service this.addFan(); } // Add a fan addFan() { const { INACTIVE: OFF, ACTIVE } = this.Characteristic.Active; const { INACTIVE, BLOWING_AIR } = this.Characteristic.CurrentFanState; // Add a fan (v2) service for the hob ventilation this.fanService = this.makeService(this.Service.Fanv2, 'Fan'); // Add the fan state characteristics const activeCharacteristic = this.fanService.getCharacteristic(this.Characteristic.Active); const rotationSpeedCharacteristic = this.fanService.getCharacteristic(this.Characteristic.RotationSpeed); const currentFanStateCharacteristic = this.fanService.getCharacteristic(this.Characteristic.CurrentFanState); rotationSpeedCharacteristic.setProps({ minValue: 0, maxValue: 100, minStep: this.fanPercentStep }); // Update from HomeKit to Home Connect const updateHC = this.makeSerialisedObject(value => this.updateFanHC(value)); this.onSetNumber(activeCharacteristic, value => updateHC({ active: value })); this.onSetNumber(rotationSpeedCharacteristic, value => updateHC({ percent: value })); // Update from Home Connect to HomeKit this.device.on('Cooking.Hob.Setting.Ventilation', level => { const percent = this.toFanSpeedPercent(level); const active = 0 < percent; this.log.info(active ? `Fan ${percent}%` : 'Fan off'); activeCharacteristic.updateValue(active ? ACTIVE : OFF); currentFanStateCharacteristic.updateValue(active ? BLOWING_AIR : INACTIVE); rotationSpeedCharacteristic.updateValue(percent); }); } // Deferred update of Home Connect state from HomeKit characteristics async updateFanHC({ active, percent }) { // Read missing Fan service values const read = (characteristic) => { assertIsDefined(this.fanService); const value = this.fanService.getCharacteristic(characteristic).value; assertIsNumber(value); return value; }; active ??= read(this.Characteristic.Active); percent ??= read(this.Characteristic.RotationSpeed); // Configure the fan const { INACTIVE: OFF } = this.Characteristic.Active; if (active === OFF || percent === 0) { this.log.info('SET fan off'); await this.device.setSetting('Cooking.Hob.Setting.Ventilation', Ventilation.Off); } else { const { level } = this.fromFanSpeedPercent(percent); const snapPercent = this.toFanSpeedPercent(level); this.log.info(`SET fan ${snapPercent}%`); await this.device.setSetting('Cooking.Hob.Setting.Ventilation', level); } } // Convert from a rotation speed percentage to a program option fromFanSpeedPercent(percent) { if (!percent) throw new Error('Attempted to convert 0% to ventilation level'); const index = Math.ceil(percent * this.fanLevels.length / 100) - 1; const percentLevel = this.fanLevels[index]; assertIsDefined(percentLevel); return percentLevel; } // Convert from a program option to a rotation speed percentage toFanSpeedPercent(level) { const percentLevel = this.fanLevels.find(o => o.level === level); if (!percentLevel) return 0; // (presumably Off) return percentLevel.percent; } }; } //# sourceMappingURL=has-fan-hob.js.map