@we5/homebridge-philipsair-platform
Version:
Homebridge plugin for philips air purifier and humidifier.
570 lines (459 loc) • 19 kB
JavaScript
'use strict';
const path = require('path');
const { exec, spawn } = require('child_process');
const logger = require('../utils/logger');
class Handler {
constructor(api, accessory) {
this.api = api;
this.accessory = accessory;
this.shutdown = false;
this.airControl = null;
this.obj = {};
this.keyMaps = {};
this.valueMaps = {};
this.speeds = [{ om: '1' }, { om: '2' }, { om: 't' }];
// FIXME: Sleep speed is likely to be removed with new config approach
if (this.accessory.context.config.sleepSpeed) {
this.speeds = [{ om: 's' }, { om: '1' }, { om: '2' }, { om: 't' }];
}
// FIXME: Here we test for some models (set with configuration, not yet pulled from received type or modelid)
// This should be extracted into a separate configuration function which handles different models.
if (this.accessory.context.config.model == 'AC3036' || this.accessory.context.config.model == 'AC1715') {
this.speeds = [{ mode: 'S' }, { mode: 'AG' }, { mode: 'M', om: 1 }, { mode: 'M', om: 2 }, { mode: 'T' }];
}
if (this.accessory.context.config.model == 'AC1715') {
this.keyMaps = {
pwr: 'D03-02',
om: 'D03-13',
speed: 'D03-13',
mode: 'D03-11',
cl: 'D03-03',
aqil: 'D03-04',
uil: 'D03-05',
iaql: 'D03-32',
pm25: 'D03-33',
fltt1: 'D05-02',
fltt2: 'D05-03',
flttotal0: 'D05-07',
flttotal1: 'D05-08',
flttotal2: 'D05-09',
fltsts0: 'D05-13',
fltsts1: 'D05-14',
fltsts2: 'D05-15',
};
this.valueMaps = {
pwr: {
OFF: 0,
ON: 1,
0: 'OFF',
1: 'ON',
},
};
}
this.args = [
'python3',
`${path.resolve(__dirname, '../../')}/lib/pyaircontrol.py`,
'-H',
this.accessory.context.config.host,
'-P',
this.accessory.context.config.port,
this.accessory.context.config.debug ? '-D' : '',
].filter((cmd) => cmd);
}
sendCMD(args) {
logger.debug(`CMD: ${args.join(' ')}`, this.accessory.displayName);
return new Promise((resolve, reject) => {
exec(args.join(' '), (err, stdout, stderr) => {
if (err) {
return reject(err);
}
logger.debug(stderr, this.accessory.displayName);
resolve();
});
});
}
handleResponse(json) {
this.obj = json;
Object.entries(this.keyMaps).forEach(([key, mappedKey]) => {
this.obj[key] = this.valueMaps[key] ? this.valueMaps[key][this.obj[mappedKey]] : this.obj[mappedKey];
delete this.obj[mappedKey];
});
logger.debug(this.obj, this.accessory.displayName);
}
handleCommand(key, value) {
key = this.keyMaps[key] || key;
value = this.valueMaps[key] ? this.valueMaps[key][value] : value;
logger.debug(`${key}=${value}`, this.accessory.displayName);
return `${key}=${value}`;
}
speedsMinStep() {
return 100 / this.speeds.length;
}
rotationSpeed() {
let speedConfigIndex = this.speeds.findIndex((speedConfig) => {
return Object.entries(speedConfig).every(([cmd, value]) => {
return this.obj[cmd].toString() == value.toString();
});
});
let speedIndex = speedConfigIndex + 1;
logger.debug(`#rotationSpeed: ${speedIndex * this.speedsMinStep()}`, this.accessory.displayName);
return speedIndex * this.speedsMinStep();
}
//Air Purifier
async setPurifierActive(state) {
try {
const stateNumber = state ? 1 : 0;
const args = [...this.args];
args.push('set', `${this.handleCommand('pwr', stateNumber)}`);
this.purifierService.updateCharacteristic(this.api.hap.Characteristic.CurrentAirPurifierState, stateNumber * 2);
logger.info(`Purifier Active: ${state}`, this.accessory.displayName);
await this.sendCMD(args);
} catch (err) {
logger.warn('An error occured during changing purifier state!', this.accessory.displayName);
logger.error(err, this.accessory.displayName);
}
}
async setPurifierTargetState(state) {
try {
const values = {
mode: state ? 'P' : this.accessory.context.config.allergicFunc ? 'A' : 'M',
};
if (state != 0) {
this.purifierService
.updateCharacteristic(this.api.hap.Characteristic.RotationSpeed, 0)
.updateCharacteristic(this.api.hap.Characteristic.TargetAirPurifierState, state);
}
const args = [...this.args];
args.push('set', `${this.handleCommand('mode', values.mode)}`);
logger.info(`Purifier Mode: ${state}`, this.accessory.displayName);
await this.sendCMD(args);
} catch (err) {
logger.warn('An error occured during changing target purifier state!', this.accessory.displayName);
logger.error(err, this.accessory.displayName);
}
}
async setPurifierLockPhysicalControls(state) {
try {
const values = {
cl: state == 1,
};
const args = [...this.args];
args.push('set', `${this.handleCommand('cl', values.cl)}`);
logger.info(`Lock: ${state}`, this.accessory.displayName);
await this.sendCMD(args);
} catch (err) {
logger.warn('An error occured during changing lock state!', this.accessory.displayName);
logger.error(err, this.accessory.displayName);
}
}
async setPurifierRotationSpeed(value) {
try {
const speed = Math.ceil(value / this.speedsMinStep());
if (speed > 0) {
this.purifierService.updateCharacteristic(this.api.hap.Characteristic.TargetAirPurifierState, 0);
logger.info(`Purifier Rotation Speed: value: ${value}`, this.accessory.displayName);
let args = [...this.args];
let cmds = [];
Object.entries(this.speeds[speed - 1]).forEach(([cmd, value]) => {
cmds.push(`${this.handleCommand(cmd, value)}`);
});
args.push('set', cmds.join(' '));
logger.info(`Purifier Rotation Speed: cmds: ${cmds.join(' ')}`, this.accessory.displayName);
await this.sendCMD(args);
}
} catch (err) {
logger.warn('An error occured during changing purifier rotation speed!', this.accessory.displayName);
logger.error(err, this.accessory.displayName);
}
}
//Humidifier
async setHumidifierActive(state) {
try {
const values = {
func: state ? 'PH' : 'P',
};
let water_level = 100;
if (this.obj.func == 'PH' && this.obj.wl == 0) {
water_level = 0;
}
let speed_humidity = 0;
let state_ph = 0;
if (this.obj.func == 'PH' && water_level == 100) {
state_ph = 1;
if (this.obj.rhset == 40) {
speed_humidity = 25;
} else if (this.obj.rhset == 50) {
speed_humidity = 50;
} else if (this.obj.rhset == 60) {
speed_humidity = 75;
} else if (this.obj.rhset == 70) {
speed_humidity = 100;
}
}
this.humidifierService.updateCharacteristic(this.api.hap.Characteristic.TargetHumidifierDehumidifierState, 1);
if (state) {
this.humidifierService
.updateCharacteristic(this.api.hap.Characteristic.Active, 1)
.updateCharacteristic(this.api.hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2)
.updateCharacteristic(this.api.hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity);
} else {
this.humidifierService
.updateCharacteristic(this.api.hap.Characteristic.Active, 0)
.updateCharacteristic(this.api.hap.Characteristic.CurrentHumidifierDehumidifierState, 0)
.updateCharacteristic(this.api.hap.Characteristic.RelativeHumidityHumidifierThreshold, 0);
}
const args = [...this.args];
args.push('set', `${this.handleCommand('func', values.func)}`);
logger.info(`Humidifier Active: ${state}`, this.accessory.displayName);
await this.sendCMD(args);
} catch (err) {
logger.warn('An error occured during changing humidifier state!', this.accessory.displayName);
logger.error(err, this.accessory.displayName);
}
}
/*setHumidifierCurrentState(state) {
return new Promise((resolve, reject) => {});
}*/
async setHumidifierTargetState(state) {
try {
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;
}
let water_level = 100;
if (this.obj.func == 'PH' && this.obj.wl == 0) {
water_level = 0;
}
this.humidifierService.updateCharacteristic(this.api.hap.Characteristic.TargetHumidifierDehumidifierState, 1);
if (speed_humidity > 0) {
this.humidifierService
.updateCharacteristic(this.api.hap.Characteristic.Active, 1)
.updateCharacteristic(this.api.hap.Characteristic.CurrentHumidifierDehumidifierState, 2)
.updateCharacteristic(this.api.hap.Characteristic.WaterLevel, water_level)
.updateCharacteristic(this.api.hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity);
} else {
this.humidifierService.updateCharacteristic(this.api.hap.Characteristic.Active, 0);
}
const args1 = [...this.args];
const args2 = [...this.args];
args1.push('set', `${this.handleCommand('func', values.func)}`);
args2.push('set', `${this.handleCommand('rhset', values.rhset)}`, '-I');
logger.info(`Humidifier State: ${state}`, this.accessory.displayName);
await this.sendCMD(args1);
await this.sendCMD(args2);
} catch (err) {
logger.warn('An error occured during changing target humidifer state!', this.accessory.displayName);
logger.error(err, this.accessory.displayName);
}
}
/*setHumidifierThreshold(value) {
return new Promise((resolve, reject) => {});
}*/
//Light
async setLightOn(state) {
if (this.settingBrightess) {
return;
}
this.settingLightState = true;
try {
const values = {
aqil: state ? 100 : 0,
uil: state ? '1' : '0',
};
//Light
const args1 = [...this.args];
const args2 = [...this.args];
args1.push('set', `${this.handleCommand('aqil', values.aqil)}`, '-I');
args2.push('set', `${this.handleCommand('uil', values.uil)}`);
logger.info(`Light state: ${state}`, this.accessory.displayName);
await this.sendCMD(args1);
await this.sendCMD(args2);
} catch (err) {
logger.warn('An error occured during changing light state!', this.accessory.displayName);
logger.error(err, this.accessory.displayName);
}
this.settingLightState = false;
}
async setLightBrightness(value) {
if (this.settingLightState) {
return;
}
this.settingBrightess = true;
try {
const values = {
aqil: value,
uil: value ? '1' : '0',
};
//Light
const args1 = [...this.args];
const args2 = [...this.args];
args1.push('set', `${this.handleCommand('aqil', values.aqil)}`, '-I');
args2.push('set', `${this.handleCommand('uil', values.uil)}`);
logger.info(`Brightness: ${value}`, this.accessory.displayName);
await this.sendCMD(args1);
await this.sendCMD(args2);
} catch (err) {
logger.warn('An error occured during changing light brightness!', this.accessory.displayName);
logger.error(err, this.accessory.displayName);
}
this.settingBrightess = false;
}
//Longpoll Process
longPoll() {
this.purifierService = this.accessory.getService(this.api.hap.Service.AirPurifier);
this.humidifierService = this.accessory.getService('Humidifier');
this.temperatureService = this.accessory.getService('Temperature Sensor');
this.humidityService = this.accessory.getService('Humidity Sensor');
this.lightService = this.accessory.getService('Light');
this.airQualityService = this.accessory.getService('Air Quality');
this.preFilterService = this.accessory.getService('Pre Filter');
this.carbonFilterService = this.accessory.getService('Active carbon filter');
this.hepaFilterService = this.accessory.getService('HEPA filter');
this.wickFilterService = this.accessory.getService('Wick filter');
const args = [...this.args];
args.push('status-observe', '-J');
this.airControl = spawn(args.shift(), args);
this.airControl.stdout.on('data', async (data) => {
this.handleResponse(JSON.parse(data.toString()));
//Air Purifier
this.purifierService
.updateCharacteristic(this.api.hap.Characteristic.Active, parseInt(this.obj.pwr) ? 1 : 0)
.updateCharacteristic(this.api.hap.Characteristic.CurrentAirPurifierState, parseInt(this.obj.pwr) * 2)
.updateCharacteristic(this.api.hap.Characteristic.TargetAirPurifierState, this.obj.mode === 'M' ? 0 : 1)
.updateCharacteristic(this.api.hap.Characteristic.LockPhysicalControls, this.obj.cl ? 1 : 0)
.updateCharacteristic(this.api.hap.Characteristic.RotationSpeed, this.rotationSpeed());
if (this.airQualityService) {
this.airQualityService
.updateCharacteristic(this.api.hap.Characteristic.AirQuality, Math.ceil(this.obj.iaql / 3))
.updateCharacteristic(this.api.hap.Characteristic.PM2_5Density, this.obj.pm25);
}
if (this.temperatureService) {
this.temperatureService.updateCharacteristic(this.api.hap.Characteristic.CurrentTemperature, this.obj.temp);
}
if (this.humidityService) {
this.humidityService.updateCharacteristic(this.api.hap.Characteristic.CurrentRelativeHumidity, this.obj.rh);
}
if (this.lightService) {
if (this.obj.pwr == '1') {
this.lightService
.updateCharacteristic(this.api.hap.Characteristic.On, this.obj.aqil > 0)
.updateCharacteristic(this.api.hap.Characteristic.Brightness, this.obj.aqil);
} else {
this.lightService.updateCharacteristic(this.api.hap.Characteristic.On, false);
}
}
if (this.humidifierService) {
let water_level = 100;
let speed_humidity = 0;
if (this.obj.func == 'PH' && this.obj.wl == 0) {
water_level = 0;
}
if (this.obj.pwr == '1') {
if (this.obj.func == 'PH' && water_level == 100) {
if (this.obj.rhset == 40) {
speed_humidity = 25;
} else if (this.obj.rhset == 50) {
speed_humidity = 50;
} else if (this.obj.rhset == 60) {
speed_humidity = 75;
} else if (this.obj.rhset == 70) {
speed_humidity = 100;
}
}
}
this.humidifierService
.updateCharacteristic(
this.api.hap.Characteristic.Active,
parseInt(this.obj.pwr) ? (this.obj.func === 'PH' ? 1 : 0) : 0
)
.updateCharacteristic(this.api.hap.Characteristic.CurrentRelativeHumidity, this.obj.rh)
.updateCharacteristic(this.api.hap.Characteristic.WaterLevel, water_level)
.updateCharacteristic(this.api.hap.Characteristic.TargetHumidifierDehumidifierState, 1)
.updateCharacteristic(this.api.hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity);
if (water_level == 0) {
if (this.obj.func != 'P') {
await this.setPurifierTargetState(true);
}
this.humidifierService
.updateCharacteristic(this.api.hap.Characteristic.Active, 0)
.updateCharacteristic(this.api.hap.Characteristic.CurrentHumidifierDehumidifierState, 0)
.updateCharacteristic(this.api.hap.Characteristic.RelativeHumidityHumidifierThreshold, 0);
}
if (this.wickFilterService) {
const fltwickchange = this.obj.wicksts == 0;
const fltwicklife = Math.round((this.obj.wicksts / 4800) * 100);
this.wickFilterService
.updateCharacteristic(this.api.hap.Characteristic.FilterChangeIndication, fltwickchange)
.updateCharacteristic(this.api.hap.Characteristic.FilterLifeLevel, fltwicklife);
}
}
if (this.preFilterService) {
const fltsts0change = this.obj.fltsts0 == 0;
const fltsts0maxlife = this.obj.flttotal0 ? this.obj.flttotal0 : 360;
const fltsts0life = (this.obj.fltsts0 / fltsts0maxlife) * 100;
this.preFilterService
.updateCharacteristic(this.api.hap.Characteristic.FilterChangeIndication, fltsts0change)
.updateCharacteristic(this.api.hap.Characteristic.FilterLifeLevel, fltsts0life);
}
if (this.carbonFilterService) {
const fltsts2change = this.obj.fltsts2 == 0;
const fltsts2maxlife = this.obj.flttotal2 ? this.obj.flttotal2 : 4800;
const fltsts2life = (this.obj.fltsts2 / fltsts2maxlife) * 100;
this.carbonFilterService
.updateCharacteristic(this.api.hap.Characteristic.FilterChangeIndication, fltsts2change)
.updateCharacteristic(this.api.hap.Characteristic.FilterLifeLevel, fltsts2life);
}
if (this.hepaFilterService) {
const fltsts1change = this.obj.fltsts1 == 0;
const fltsts1maxlife = this.obj.flttotal1 ? this.obj.flttotal1 : 4800;
const fltsts1life = (this.obj.fltsts1 / fltsts1maxlife) * 100;
this.hepaFilterService
.updateCharacteristic(this.api.hap.Characteristic.FilterChangeIndication, fltsts1change)
.updateCharacteristic(this.api.hap.Characteristic.FilterLifeLevel, fltsts1life);
}
});
this.airControl.stderr.on('data', (data) => {
logger.debug(data.toString(), this.accessory.displayName);
});
this.airControl.stderr.on('exit', () => {
logger.debug(
`airControl process killed (${this.shutdown ? 'expected' : 'not expected'})`,
this.accessory.displayName
);
clearTimeout(this.processTimeout);
if (!this.shutdown) {
logger.debug('Restarting polling process', this.accessory.displayName);
}
});
this.processTimeout = setTimeout(() => {
if (this.airControl) {
this.airControl.kill();
this.airControl = null;
}
this.longPoll();
}, 1 * 60 * 1000);
}
kill(shutdown) {
this.shutdown = shutdown || false;
if (this.airControl) {
logger.debug('Killing airControl process', this.accessory.displayName);
this.airControl.kill();
}
}
}
module.exports = Handler;