homebridge-homeconnect
Version:
A Homebridge plugin that connects Home Connect appliances to Apple HomeKit
139 lines • 7.75 kB
JavaScript
// Homebridge plugin for Home Connect home appliances
// Copyright © 2025 Alexander Thoukydides
import { assertIsDefined, assertIsNumber, formatList, plural } from './utils.js';
// Add an air conditioner fan to an accessory
export function HasFanAC(Base) {
return class HasFan extends Base {
// Accessory services
activeService;
// Supported air conditioner programs
acPrograms = new Map();
// Mixin constructor
constructor(...args) {
super(...args);
// Add a fan (v2) service (instead of the usual HasActive service)
this.activeService = this.makeService(this.Service.Fanv2, 'Fan');
// Continue initialisation asynchronously
this.asyncInitialise('Fan', this.initHasFan());
}
// Asynchronous initialisation
async initHasFan() {
// Enable polling of selected/active programs when connected
this.device.pollPrograms();
// Read the air conditioner program details
const programs = await this.getCached('fan', () => this.device.getAvailablePrograms());
if (!programs.length)
throw new Error('No air conditioner programs are supported');
const programNames = programs.map(program => program.key.replace(/^.*\./, ''));
this.log.info(`Air conditioner supports ${plural(programs.length, 'program')}: ${formatList(programNames)}`);
// Read the options supported by each program
for (const program of programs) {
const details = await this.getCached(`fan ${program.key}`, () => this.device.getAvailableProgram(program.key));
const { options } = details;
const supports = (option) => Boolean(options?.some(o => o.key === option));
this.acPrograms.set(program.key, {
auto: supports('HeatingVentilationAirConditioning.AirConditioner.Option.FanSpeedMode'),
manual: supports('HeatingVentilationAirConditioning.AirConditioner.Option.FanSpeedPercentage')
});
}
// Control the fan
const updateHC = this.makeSerialisedObject(value => this.updateFanHC(value));
// Add the fan service
this.addFan(updateHC);
}
// Add a fan
addFan(updateHC) {
const service = this.activeService;
const { INACTIVE: OFF, ACTIVE } = this.Characteristic.Active;
const { INACTIVE, BLOWING_AIR } = this.Characteristic.CurrentFanState;
const { MANUAL, AUTO } = this.Characteristic.TargetFanState;
// Add the fan state characteristics
service.getCharacteristic(this.Characteristic.Active)
.onSet(this.onSetNumber(value => updateHC({ active: value })));
service.getCharacteristic(this.Characteristic.CurrentFanState);
service.getCharacteristic(this.Characteristic.TargetFanState)
.onSet(this.onSetNumber(value => updateHC(value === AUTO ? { auto: value, active: ACTIVE } : { auto: value })));
service.getCharacteristic(this.Characteristic.RotationSpeed)
.onSet(this.onSetNumber(value => updateHC({ percent: value })));
// Update the status
this.device.on('HeatingVentilationAirConditioning.AirConditioner.Option.FanSpeedMode', mode => {
const manual = mode === 'HeatingVentilationAirConditioning.AirConditioner.EnumType.FanSpeedMode.Manual';
this.log.info(`Fan ${manual ? 'manual' : 'automatic'} control`);
service.updateCharacteristic(this.Characteristic.TargetFanState, manual ? MANUAL : AUTO);
});
this.device.on('HeatingVentilationAirConditioning.AirConditioner.Option.FanSpeedPercentage', percent => {
this.log.info(`Fan ${percent}%`);
service.updateCharacteristic(this.Characteristic.RotationSpeed, percent);
});
this.device.on('BSH.Common.Status.OperationState', () => {
const active = this.device.isOperationState('Run');
this.log.info(`Fan ${active ? 'running' : 'off'}`);
service.updateCharacteristic(this.Characteristic.Active, active ? ACTIVE : OFF);
service.updateCharacteristic(this.Characteristic.CurrentFanState, active ? BLOWING_AIR : INACTIVE);
});
}
// Deferred update of Home Connect state from HomeKit characteristics
updateFanHC({ active, auto, percent }) {
// Read missing Fan service values
const read = (characteristic) => {
const value = this.activeService.getCharacteristic(characteristic).value;
assertIsNumber(value);
return value;
};
active ??= read(this.Characteristic.Active);
auto ??= read(this.Characteristic.TargetFanState);
percent ??= read(this.Characteristic.RotationSpeed);
// Configure the fan
const { INACTIVE: OFF, ACTIVE } = this.Characteristic.Active;
const { AUTO } = this.Characteristic.TargetFanState;
if (auto !== AUTO && percent === 0)
active = OFF;
return this.setFan(active === ACTIVE, auto === AUTO, percent);
}
// Configure the fan for a particular mode and speed
async setFan(active, auto, percent) {
if (!active) {
// Turn the fan off
this.log.info('SET fan off');
await this.device.stopProgram();
}
else {
// Check the active or selected program capabilities
const isActive = this.device.isOperationState('Run');
let program = this.device.getItem(isActive ? 'BSH.Common.Root.ActiveProgram' : 'BSH.Common.Root.SelectedProgram');
program ??= this.acPrograms.keys().next().value;
assertIsDefined(program);
const details = this.acPrograms.get(program);
assertIsDefined(details);
// Select the appropriate program options
if (!details.manual)
auto = true;
this.log.info(`SET fan ${auto ? 'automatic' : `manual ${percent}%`}`);
const optionsList = [];
if (details.auto)
optionsList.push({
key: 'HeatingVentilationAirConditioning.AirConditioner.Option.FanSpeedMode',
value: auto ? 'HeatingVentilationAirConditioning.AirConditioner.EnumType.FanSpeedMode.Automatic'
: 'HeatingVentilationAirConditioning.AirConditioner.EnumType.FanSpeedMode.Manual'
});
if (details.manual && !auto)
optionsList.push({
key: 'HeatingVentilationAirConditioning.AirConditioner.Option.FanSpeedPercentage',
value: percent
});
if (isActive) {
// Try changing the options for the current program
for (const { key, value } of optionsList) {
await this.device.setActiveProgramOption(key, value);
}
}
else {
// Start the currently selected program
const options = Object.fromEntries(optionsList.map(({ key, value }) => [key, value]));
await this.device.startProgram(program, options);
}
}
}
};
}
//# sourceMappingURL=has-fan-ac.js.map