import { API, APIEvent, CharacteristicSetCallback, CharacteristicValue, DynamicPlatformPlugin, HAP, Logging, PlatformAccessory, PlatformAccessoryEvent, PlatformConfig } from 'homebridge'; import {AirClient, HttpClient, CoapClient, PlainCoapClient, HttpClientLegacy} from 'philips-air'; import {promisify} from 'util'; import {exec} from 'child_process'; import * as fs from 'fs'; import timestamp from 'time-stamp'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import localStorage from 'node-sessionstorage'; import {PhilipsAirPlatformConfig, DeviceConfig} from './configTypes'; import {PurifierStatus, PurifierFilters, PurifierFirmware} from './deviceTypes'; let hap: HAP; let Accessory: typeof PlatformAccessory; const PLUGIN_NAME = 'homebridge-philips-air'; const PLATFORM_NAME = 'philipsAir'; const pathToModule = require.resolve(PLUGIN_NAME); const pathTopyaircontrol = pathToModule.replace('dist/index.js', 'node_modules/philips-air/pyaircontrol.py'); const pathToSensorFiles = pathToModule.replace('dist/index.js', 'sensor/'); enum CommandType { Polling = 0, GetFirmware, GetFilters, GetStatus, SetData } type Command = { purifier: Purifier, type: CommandType, callback?: (error?: Error | null | undefined) => void, data?: any // eslint-disable-line @typescript-eslint/no-explicit-any }; type Purifier = { accessory: PlatformAccessory, client: AirClient, config: DeviceConfig, timeout?: NodeJS.Timeout, lastfirmware?: number, lastfilters?: number, laststatus?: number, aqil?: number, uil?: string, rh?: number, rhset?: number, func?: string }; class PhilipsAirPlatform implements DynamicPlatformPlugin { private readonly log: Logging; private readonly api: API; private readonly config: PhilipsAirPlatformConfig; private readonly timeout: number; private readonly cachedAccessories: Array<PlatformAccessory> = []; private readonly purifiers: Map<string, Purifier> = new Map(); private readonly commandQueue: Array<Command> = []; private queueRunning = false; enqueuePromise = promisify(this.enqueueCommand); constructor(log: Logging, config: PlatformConfig, api: API) { this.log = log; this.config = config as unknown as PhilipsAirPlatformConfig; this.api = api; this.timeout = (this.config.timeout_seconds || 5) * 1000; api.on(APIEvent.DID_FINISH_LAUNCHING, this.didFinishLaunching.bind(this)); } configureAccessory(accessory: PlatformAccessory): void { this.cachedAccessories.push(accessory); } didFinishLaunching(): void { const ips: Array<string> = []; this.config.devices.forEach((device: DeviceConfig) => { this.addAccessory(device); const uuid = hap.uuid.generate(device.ip); ips.push(uuid); }); const badAccessories: Array<PlatformAccessory> = []; this.cachedAccessories.forEach(cachedAcc => { if (!ips.includes(cachedAcc.UUID)) { badAccessories.push(cachedAcc); } }); this.removeAccessories(badAccessories); this.purifiers.forEach((purifier) => { this.enqueueCommand(CommandType.Polling, purifier); this.enqueueCommand(CommandType.GetFirmware, purifier); this.enqueueCommand(CommandType.GetStatus, purifier); this.enqueueCommand(CommandType.GetFilters, purifier); }); } async storeKey(purifier: Purifier): Promise<void> { if (purifier.client && purifier.client instanceof HttpClient) { purifier.accessory.context.key = (purifier.client as HttpClient).key; } } async setData(purifier: Purifier, values: any, // eslint-disable-line @typescript-eslint/no-explicit-any callback?: (error?: Error | null | undefined) => void): Promise<void> { try { await purifier.client?.setValues(values); await this.storeKey(purifier); if (callback) { callback(); } } catch (err) { if (callback) { callback(err); } } } async updatePolling(purifier: Purifier): Promise<void> { try { // Polling interval let polling = purifier.config.polling || 60; if (polling < 60) { polling = 60; } setInterval(function() { exec('python3 ' + pathTopyaircontrol + ' --ipaddr ' + purifier.config.ip + ' --protocol ' + purifier.config.protocol + ' --status', (error, stdout, stderr) => { if (error || stderr) { console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[36m[Philips Air] \x1b[31m[' + purifier.config.name + '] Unable to get data for polling: Error: spawnSync python3 ETIMEDOUT.\x1b[0m'); console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[33mIf your have "Error: spawnSync python3 ETIMEDOUT" your need unplug the accessory from outlet for 10 seconds and plug again.\x1b[0m'); } if (error || stderr || error && stderr) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore stdout = {om: localStorage.getItem('om'), pwr: localStorage.getItem('pwr'), cl: false, aqil: localStorage.getItem('aqil'), uil: localStorage.getItem('uil'), dt: 0, dtrs: 0, mode: localStorage.getItem('mode'), func: localStorage.getItem('func'), rhset: localStorage.getItem('rhset'), 'rh': localStorage.getItem('rh'), 'temp': localStorage.getItem('temp'), pm25: localStorage.getItem('pm25'), iaql: localStorage.getItem('iaql'), aqit: 4, ddp: '1', rddp: localStorage.getItem('rddp'), err: 0, wl: localStorage.getItem('wl'), fltt1: localStorage.getItem('fltt1'), fltt2: localStorage.getItem('fltt2'), fltsts0: localStorage.getItem('fltsts0'), fltsts1: localStorage.getItem('fltsts1'), fltsts2: localStorage.getItem('fltsts2'), wicksts: localStorage.getItem('wicksts')}; stdout = JSON.stringify(stdout); } const obj = JSON.parse(stdout); if (!error || !stderr || !error && !stderr) { localStorage.setItem('pwr', obj.pwr); localStorage.setItem('om', obj.om); localStorage.setItem('aqil', obj.aqil); localStorage.setItem('uil', obj.uil); localStorage.setItem('mode', obj.mode); localStorage.setItem('func', obj.func); localStorage.setItem('rhset', obj.rhset); localStorage.setItem('iaql', obj.iaql); localStorage.setItem('pm25', obj.pm25); localStorage.setItem('rh', obj.rh); localStorage.setItem('temp', obj.temp); localStorage.setItem('rddp', obj.rddp); localStorage.setItem('wl', obj.wl); localStorage.setItem('fltt1', obj.fltt1); localStorage.setItem('fltt2', obj.fltt2); localStorage.setItem('fltsts0', obj.fltsts0); localStorage.setItem('fltsts1', obj.fltsts1); localStorage.setItem('fltsts2', obj.fltsts2); localStorage.setItem('wicksts', obj.wicksts); } else { localStorage.setItem('pwr', 1); localStorage.setItem('om', '0'); localStorage.setItem('aqil', '0'); localStorage.setItem('uil', 0); localStorage.setItem('mode', 'A'); localStorage.setItem('func', 'PH'); localStorage.setItem('rhset', 50); localStorage.setItem('iaql', 1); localStorage.setItem('pm25', 1); localStorage.setItem('rh', 45); localStorage.setItem('temp', 25); localStorage.setItem('rddp', 1); localStorage.setItem('wl', 100); localStorage.setItem('fltt1', 'A3'); localStorage.setItem('fltt2', 'C7'); localStorage.setItem('fltsts0', 287); localStorage.setItem('fltsts1', 2553); localStorage.setItem('fltsts2', 2553); localStorage.setItem('wicksts', 4005); } const purifierService = purifier.accessory.getService(hap.Service.AirPurifier); if (purifierService) { const state = parseInt(obj.pwr) * 2; purifierService .updateCharacteristic(hap.Characteristic.Active, obj.pwr) .updateCharacteristic(hap.Characteristic.CurrentAirPurifierState, state) .updateCharacteristic(hap.Characteristic.LockPhysicalControls, obj.cl); } const qualityService = purifier.accessory.getService(hap.Service.AirQualitySensor); if (qualityService) { const iaql = Math.ceil(obj.iaql / 3); qualityService .updateCharacteristic(hap.Characteristic.AirQuality, iaql) .updateCharacteristic(hap.Characteristic.PM2_5Density, obj.pm25); } if (purifier.config.temperature_sensor) { const temperature_sensor = purifier.accessory.getService('Temperature'); if (temperature_sensor) { temperature_sensor.updateCharacteristic(hap.Characteristic.CurrentTemperature, obj.temp); } } if (purifier.config.humidity_sensor) { const humidity_sensor = purifier.accessory.getService('Humidity'); if (humidity_sensor) { humidity_sensor.updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, obj.rh); } } if (purifier.config.light_control) { const lightsService = purifier.accessory.getService('Lights'); if (obj.pwr == '1') { if (lightsService) { lightsService .updateCharacteristic(hap.Characteristic.On, obj.aqil > 0) .updateCharacteristic(hap.Characteristic.Brightness, obj.aqil); } } } if (purifier.config.humidifier) { let water_level = 100; let speed_humidity = 0; if (obj.func == 'PH' && obj.wl == 0) { water_level = 0; } if (obj.pwr == '1') { if (obj.func == 'PH' && water_level == 100) { if (obj.rhset == 40) { speed_humidity = 25; } else if (obj.rhset == 50) { speed_humidity = 50; } else if (obj.rhset == 60) { speed_humidity = 75; } else if (obj.rhset == 70) { speed_humidity = 100; } } } const Humidifier = purifier.accessory.getService('Humidifier'); if (Humidifier) { Humidifier .updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, obj.rh) .updateCharacteristic(hap.Characteristic.WaterLevel, water_level) .updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1) .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity); if (water_level == 0) { if (obj.func != 'P') { exec('airctrl --ipaddr ' + purifier.config.ip + ' --protocol ' + purifier.config.protocol + ' --func P', (error, stdout, stderr) => { if (error || stderr) { console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[36m[Philips Air] \x1b[31m[' + purifier.config.name + '] Unable to get data for polling: Error: spawnSync python3 ETIMEDOUT.\x1b[0m'); console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[33mIf your have "Error: spawnSync python3 ETIMEDOUT" your need unplug the accessory from outlet for 10 seconds and plug again.\x1b[0m'); } }); } Humidifier .updateCharacteristic(hap.Characteristic.Active, 0) .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 0) .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, 0); } } } if (purifier.config.logger) { if (purifier.config.temperature_sensor) { if (!error || !stderr || !error && !stderr) { const logger_temp = fs.createWriteStream(pathToSensorFiles + 'temp.txt', { flags: 'w' }); logger_temp.write(obj.temp.toString()); logger_temp.end(); } } if (purifier.config.humidity_sensor) { if (!error || !stderr || !error && !stderr) { const logger_hum = fs.createWriteStream(pathToSensorFiles + 'hum.txt', { flags: 'w' }); logger_hum.write(obj.rh.toString()); logger_hum.end(); } } } }); }, polling * 1000); } catch (err) { this.log.error('[' + purifier.config.name + '] Unable to load polling info'); } } async updateFirmware(purifier: Purifier): Promise<void> { try { purifier.lastfirmware = Date.now(); const firmware: PurifierFirmware = await purifier.client?.getFirmware(); await this.storeKey(purifier); const accInfo = purifier.accessory.getService(hap.Service.AccessoryInformation); if (accInfo) { const name = firmware.modelid; accInfo .updateCharacteristic(hap.Characteristic.Manufacturer, 'Philips') .updateCharacteristic(hap.Characteristic.SerialNumber, purifier.config.ip) .updateCharacteristic(hap.Characteristic.Model, name) .updateCharacteristic(hap.Characteristic.FirmwareRevision, firmware.version); } } catch (err) { this.log.error('[' + purifier.config.name + '] Unable to load firmware info: ' + err); } } async updateFilters(purifier: Purifier): Promise<void> { try { const filters: PurifierFilters = await purifier.client?.getFilters(); purifier.lastfilters = Date.now(); await this.storeKey(purifier); const preFilter = purifier.accessory.getService('Pre-filter'); if (preFilter) { const fltsts0change = filters.fltsts0 == 0; const fltsts0life = filters.fltsts0 / 360 * 100; preFilter .updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltsts0change) .updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltsts0life); } const carbonFilter = purifier.accessory.getService('Active carbon filter'); if (carbonFilter) { const fltsts2change = filters.fltsts2 == 0; const fltsts2life = filters.fltsts2 / 4800 * 100; carbonFilter .updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltsts2change) .updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltsts2life); } const hepaFilter = purifier.accessory.getService('HEPA filter'); if (hepaFilter) { const fltsts1change = filters.fltsts1 == 0; const fltsts1life = filters.fltsts1 / 4800 * 100; hepaFilter .updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltsts1change) .updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltsts1life); } if (purifier.config.humidifier) { const wickFilter = purifier.accessory.getService('Wick filter'); if (wickFilter) { const fltwickchange = filters.wicksts == 0; const fltwicklife = Math.round(filters.wicksts / 4800 * 100); wickFilter .updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltwickchange) .updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltwicklife); } } } catch (err) { this.log.error('[' + purifier.config.name + '] Unable to load filter info: ' + err); } } async updateStatus(purifier: Purifier): Promise<void> { try { const status: PurifierStatus = await purifier.client?.getStatus(); purifier.laststatus = Date.now(); await this.storeKey(purifier); const purifierService = purifier.accessory.getService(hap.Service.AirPurifier); if (purifierService) { const state = parseInt(status.pwr) * 2; purifierService .updateCharacteristic(hap.Characteristic.Active, status.pwr) .updateCharacteristic(hap.Characteristic.CurrentAirPurifierState, state) .updateCharacteristic(hap.Characteristic.LockPhysicalControls, status.cl); } const qualityService = purifier.accessory.getService(hap.Service.AirQualitySensor); if (qualityService) { const iaql = Math.ceil(status.iaql / 3); qualityService .updateCharacteristic(hap.Characteristic.AirQuality, iaql) .updateCharacteristic(hap.Characteristic.PM2_5Density, status.pm25); } if (purifier.config.temperature_sensor) { const temperature_sensor = purifier.accessory.getService('Temperature'); if (temperature_sensor) { temperature_sensor.updateCharacteristic(hap.Characteristic.CurrentTemperature, status.temp); } } if (purifier.config.humidity_sensor) { const humidity_sensor = purifier.accessory.getService('Humidity'); if (humidity_sensor) { humidity_sensor.updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, status.rh); } } if (purifier.config.light_control) { const lightsService = purifier.accessory.getService('Lights'); if (status.pwr == '1') { if (lightsService) { lightsService .updateCharacteristic(hap.Characteristic.On, status.aqil > 0) .updateCharacteristic(hap.Characteristic.Brightness, status.aqil); } } } if (purifier.config.humidifier) { const Humidifier = purifier.accessory.getService('Humidifier'); if (Humidifier) { let speed_humidity = 0; let state_ph = 0; let water_level = 100; if (status.func == 'PH' && status.wl == 0) { water_level = 0; } if (status.pwr == '1') { if (status.func == 'PH' && water_level == 100) { state_ph = 1; if (status.rhset == 40) { speed_humidity = 25; } else if (status.rhset == 50) { speed_humidity = 50; } else if (status.rhset == 60) { speed_humidity = 75; } else if (status.rhset == 70) { speed_humidity = 100; } } } Humidifier .updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, status.rh) .updateCharacteristic(hap.Characteristic.WaterLevel, water_level) .updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1); if (state_ph && status.rhset >= 40) { Humidifier .updateCharacteristic(hap.Characteristic.Active, state_ph) .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2) .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity); } if (water_level == 0) { if (status.func != 'P') { exec('airctrl --ipaddr ' + purifier.config.ip + ' --protocol ' + purifier.config.protocol + ' --func P', (err, stdout, stderr) => { if (err) { return; } if (stderr) { console.error('Unable to switch off purifier ' + stderr + '. If your have "sync timeout" error your need unplug the accessory from the outlet for 10 seconds.'); } }); } } } } } catch (err) { this.log.error('[' + purifier.config.name + '] Unable to load status info: ' + err); } } async setPower(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> { const purifier = this.purifiers.get(accessory.displayName); if (purifier) { const values = { pwr: (state as boolean).toString() }; try { const status: PurifierStatus = await purifier.client?.getStatus(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore purifier.laststatus = Date.now(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.storeKey(purifier); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore let water_level = 100; if (status.func == 'PH' && status.wl == 0) { water_level = 0; } const purifierService = accessory.getService(hap.Service.AirPurifier); if (purifierService) { purifierService.updateCharacteristic(hap.Characteristic.CurrentAirPurifierState, state as number * 2); } if (purifier.config.humidifier) { if (water_level == 0) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore values['func'] = 'P'; } } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.enqueuePromise(CommandType.SetData, purifier, values); if (purifier.config.light_control) { const lightsService = accessory.getService('Lights'); if (lightsService) { if (state) { lightsService .updateCharacteristic(hap.Characteristic.On, status.aqil > 0) .updateCharacteristic(hap.Characteristic.Brightness, status.aqil); } else { lightsService .updateCharacteristic(hap.Characteristic.On, 0) .updateCharacteristic(hap.Characteristic.Brightness, 0); } } } if (purifier.config.humidifier) { const Humidifier = accessory.getService('Humidifier'); let state_ph = 0; if (status.func == 'PH' && water_level == 100) { state_ph = 1; } if (Humidifier) { Humidifier.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1); if (state) { Humidifier .updateCharacteristic(hap.Characteristic.Active, state_ph) .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2); } else { Humidifier .updateCharacteristic(hap.Characteristic.Active, 0) .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 0) .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, 0); } } } } catch (err) { this.log.error('[' + purifier.config.name + '] Error setting power: ' + err); } } } async setBrightness(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> { const purifier = this.purifiers.get(accessory.displayName); if (purifier) { const values = { aqil: state, uil: state ? '1' : '0' }; try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.enqueuePromise(CommandType.SetData, purifier, values); } catch (err) { this.log.error('[' + purifier.config.name + '] Error setting brightness: ' + err); } } } async setMode(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> { const purifier = this.purifiers.get(accessory.displayName); if (purifier) { const values = { mode: state ? 'P' : 'M' }; if (purifier.config.allergic_func) { values.mode = state ? 'P' : 'A'; } else { values.mode = state ? 'P' : 'M'; } try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.enqueuePromise(CommandType.SetData, purifier, values); if (state != 0) { const purifierService = accessory.getService(hap.Service.AirPurifier); if (purifierService) { purifierService .updateCharacteristic(hap.Characteristic.RotationSpeed, 0) .updateCharacteristic(hap.Characteristic.TargetAirPurifierState, state); } } } catch (err) { this.log.error('[' + purifier.config.name + '] Error setting mode: ' + err); } } } async setLock(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> { const purifier = this.purifiers.get(accessory.displayName); if (purifier) { const values = { cl: state == 1 }; try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.enqueuePromise(CommandType.SetData, purifier, values); } catch (err) { this.log.error('[' + purifier.config.name + '] Error setting lock: ' + err); } } } async setHumidity(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> { const purifier = this.purifiers.get(accessory.displayName); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const status: PurifierStatus = await purifier.client?.getStatus(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore purifier.laststatus = Date.now(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.storeKey(purifier); const Humidifier = accessory.getService('Humidifier'); if (purifier) { const values = { func: state ? 'PH' : 'P' }; let water_level = 100; if (status.func == 'PH' && status.wl == 0) { water_level = 0; } let speed_humidity = 0; let state_ph = 0; if (status.func == 'PH' && water_level == 100) { state_ph = 1; if (status.rhset == 40) { speed_humidity = 25; } else if (status.rhset == 50) { speed_humidity = 50; } else if (status.rhset == 60) { speed_humidity = 75; } else if (status.rhset == 70) { speed_humidity = 100; } } try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.enqueuePromise(CommandType.SetData, purifier, values); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Humidifier.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1); if (state) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Humidifier .updateCharacteristic(hap.Characteristic.Active, 1) .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2) .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity); } else { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Humidifier .updateCharacteristic(hap.Characteristic.Active, 0) .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 0) .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, 0); } } catch (err) { this.log.error('[' + purifier.config.name + '] Error setting func: ' + err); } } } async setHumidityTarget(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> { const purifier = this.purifiers.get(accessory.displayName); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const status: PurifierStatus = await purifier.client?.getStatus(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore purifier.laststatus = Date.now(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.storeKey(purifier); const Humidifier = accessory.getService('Humidifier'); if (purifier) { const speed = state; const values = { func: state ? 'PH' : 'P', rhset: 40 }; let speed_humidity = 0; if (speed > 0 && speed <= 25) { values.rhset = 40; speed_humidity = 25; } else if (speed > 25 && speed <= 50) { values.rhset = 50; speed_humidity = 50; } else if (speed > 50 && speed <= 75) { values.rhset = 60; speed_humidity = 75; } else if (speed > 75 && speed <= 100) { values.rhset = 70; speed_humidity = 100; } try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.enqueuePromise(CommandType.SetData, purifier, values); if (Humidifier) { let water_level = 100; if (status.func == 'PH' && status.wl == 0) { water_level = 0; } Humidifier.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1); if (speed_humidity > 0) { Humidifier .updateCharacteristic(hap.Characteristic.Active, 1) .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 2) .updateCharacteristic(hap.Characteristic.WaterLevel, water_level) .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity); } else { Humidifier .updateCharacteristic(hap.Characteristic.Active, 0); } } } catch (err) { this.log.error('[' + purifier.config.name + '] Error setting humidifier: ' + err); } } } async setFan(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> { const purifier = this.purifiers.get(accessory.displayName); if (purifier) { let divisor = 25; let offset = 0; if (purifier.config.sleep_speed) { divisor = 20; offset = 1; } const speed = Math.ceil(state as number / divisor); if (speed > 0) { const values = { mode: 'M', om: '' }; if (offset == 1 && speed == 1) { values.om = 's'; } else if (speed < 4 + offset) { values.om = (speed - offset).toString(); } else { values.om = 't'; } try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.enqueuePromise(CommandType.SetData, purifier, values); const service = accessory.getService(hap.Service.AirPurifier); if (service) { service.updateCharacteristic(hap.Characteristic.TargetAirPurifierState, 0); } if (purifier.timeout) { clearTimeout(purifier.timeout); } purifier.timeout = setTimeout(() => { if (service) { service.updateCharacteristic(hap.Characteristic.RotationSpeed, speed * divisor); } purifier.timeout = undefined; }, 1000); } catch (err) { this.log.error('[' + purifier.config.name + '] Error setting fan: ' + err); } } } } addAccessory(config: DeviceConfig): void { this.log('[' + config.name + '] Initializing accessory...'); const uuid = hap.uuid.generate(config.ip); let accessory = this.cachedAccessories.find(cachedAcc => { return cachedAcc.UUID == uuid; }); if (!accessory) { accessory = new Accessory(config.name, uuid); accessory.addService(hap.Service.AirPurifier, config.name); accessory.addService(hap.Service.AirQualitySensor, 'Air quality', 'Air quality'); if (config.light_control) { accessory.addService(hap.Service.Lightbulb, 'Lights', 'Lights') .addCharacteristic(hap.Characteristic.Brightness); } accessory.addService(hap.Service.FilterMaintenance, 'Pre-filter', 'Pre-filter'); accessory.addService(hap.Service.FilterMaintenance, 'Active carbon filter', 'Active carbon filter'); accessory.addService(hap.Service.FilterMaintenance, 'HEPA filter', 'HEPA filter'); if (config.temperature_sensor) { accessory.addService(hap.Service.TemperatureSensor, 'Temperature', 'Temperature'); } if (config.humidity_sensor) { accessory.addService(hap.Service.HumiditySensor, 'Humidity', 'Humidity'); } if (config.humidifier) { accessory.addService(hap.Service.HumidifierDehumidifier, 'Humidifier', 'Humidifier'); accessory.addService(hap.Service.FilterMaintenance, 'Wick filter', 'Wick filter'); } this.api.registerPlatformAccessories('homebridge-philips-air', 'philipsAir', [accessory]); } else { let lightsService = accessory.getService('Lights'); if (config.light_control) { if (lightsService == undefined) { lightsService = accessory.addService(hap.Service.Lightbulb, 'Lights', 'Lights'); lightsService.addCharacteristic(hap.Characteristic.Brightness); } } else if (lightsService != undefined) { accessory.removeService(lightsService); } const temperature_sensor = accessory.getService('Temperature'); if (config.temperature_sensor) { if (temperature_sensor == undefined) { accessory.addService(hap.Service.TemperatureSensor, 'Temperature', 'Temperature'); } } else if (temperature_sensor != undefined) { accessory.removeService(temperature_sensor); } const humidity_sensor = accessory.getService('Humidity'); if (config.humidity_sensor) { if (humidity_sensor == undefined) { accessory.addService(hap.Service.HumiditySensor, 'Humidity', 'Humidity'); } } else if (humidity_sensor != undefined) { accessory.removeService(humidity_sensor); } const Humidifier = accessory.getService('Humidifier'); if (config.humidifier) { if (Humidifier == undefined) { accessory.addService(hap.Service.HumidifierDehumidifier, 'Humidifier', 'Humidifier'); } } else if (Humidifier != undefined) { accessory.removeService(Humidifier); } } this.setService(accessory, config); let client: AirClient; switch (config.protocol) { case 'coap': client = new CoapClient(config.ip, this.timeout); break; case 'plain_coap': client = new PlainCoapClient(config.ip, this.timeout); break; case 'http_legacy': client = new HttpClientLegacy(config.ip, this.timeout); break; case 'http': default: if (accessory.context.key) { client = new HttpClient(config.ip, this.timeout, accessory.context.key); } else { client = new HttpClient(config.ip, this.timeout); } } this.purifiers.set(accessory.displayName, { accessory: accessory, client: client, config: config }); } removeAccessories(accessories: Array<PlatformAccessory>): void { accessories.forEach(accessory => { this.log('[' + accessory.displayName + '] Removed from Homebridge.'); this.api.unregisterPlatformAccessories('homebridge-philips-air', 'philipsAir', [accessory]); }); } setService(accessory: PlatformAccessory, config: DeviceConfig): void { accessory.on(PlatformAccessoryEvent.IDENTIFY, () => { this.log('[' + accessory.displayName + '] Identify requested.'); }); const purifierService = accessory.getService(hap.Service.AirPurifier); let min_step_purifier_speed = 25; if (config.sleep_speed) { min_step_purifier_speed = 20; } if (purifierService) { purifierService .getCharacteristic(hap.Characteristic.Active) .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { try { await this.setPower(accessory, state); callback(); } catch (err) { callback(err); } }); purifierService .getCharacteristic(hap.Characteristic.TargetAirPurifierState) .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { try { await this.setMode(accessory, state); callback(); } catch (err) { callback(err); } }); purifierService .getCharacteristic(hap.Characteristic.LockPhysicalControls) .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { try { await this.setLock(accessory, state); callback(); } catch (err) { callback(err); } }); purifierService .getCharacteristic(hap.Characteristic.RotationSpeed) .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { try { await this.setFan(accessory, state); callback(); } catch (err) { callback(err); } }).setProps({ minValue: 0, maxValue: 100, minStep: min_step_purifier_speed }); } if (config.light_control) { const lightService = accessory.getService('Lights'); if (lightService) { lightService .getCharacteristic(hap.Characteristic.Brightness) .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { try { await this.setBrightness(accessory, state); callback(); } catch (err) { callback(err); } }).setProps({ minValue: 0, maxValue: 100, minStep: 25 }); } } if (config.humidifier) { const Humidifier = accessory.getService('Humidifier'); if (Humidifier) { Humidifier .getCharacteristic(hap.Characteristic.Active) .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { try { await this.setHumidity(accessory, state); callback(); } catch (err) { callback(err); } }); Humidifier .getCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState) .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { try { await this.setHumidityTarget(accessory, state); await this.setHumidity(accessory, state); callback(); } catch (err) { callback(err); } }).setProps({ validValues: [ hap.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE, hap.Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING ] }); Humidifier .getCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState) .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { try { await this.setHumidityTarget(accessory, state); await this.setHumidity(accessory, state); callback(); } catch (err) { callback(err); } }).setProps({ validValues: [ hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER ] }); Humidifier .getCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold) .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { try { await this.setHumidityTarget(accessory, state); callback(); } catch (err) { callback(err); } }).setProps({ minValue: 0, maxValue: 100, minStep: 25 }); } } } enqueueAccessory(commandType: CommandType, accessory: PlatformAccessory): void { const purifier = this.purifiers.get(accessory.displayName); if (purifier) { this.enqueueCommand(commandType, purifier); } } enqueueCommand(commandType: CommandType, purifier: Purifier, data?: any, // eslint-disable-line @typescript-eslint/no-explicit-any callback?: (error?: Error | null | undefined) => void): void { if (commandType != CommandType.SetData) { const exists = this.commandQueue.find((command) => { return command.purifier.config.ip == purifier.config.ip && command.type == commandType; }); if (exists) { return; // Don't enqueue commands we already have in the queue } } this.commandQueue.push({ purifier: purifier, type: commandType, callback: callback, data: data }); if (!this.queueRunning) { this.queueRunning = true; this.nextCommand(); } } nextCommand(): void { const todoItem = this.commandQueue.shift(); if (!todoItem) { return; } let command; switch (todoItem.type) { case CommandType.Polling: command = this.updatePolling(todoItem.purifier); break; case CommandType.GetFirmware: command = this.updateFirmware(todoItem.purifier); break; case CommandType.GetFilters: command = this.updateFilters(todoItem.purifier); break; case CommandType.GetStatus: command = this.updateStatus(todoItem.purifier); break; case CommandType.SetData: command = this.setData(todoItem.purifier, todoItem.data, todoItem.callback); } command.then(() => { if (this.commandQueue.length > 0) { this.nextCommand(); } else { this.queueRunning = false; } }); } } export = (api: API): void => { hap = api.hap; Accessory = api.platformAccessory; api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, PhilipsAirPlatform); };