homebridge-xiaomi-fan
Version:
Homebridge plugin for Xiaomi Smartmi Fans
1,197 lines (1,030 loc) • 46.2 kB
JavaScript
const fs = require('fs');
const mkdirp = require('mkdirp');
const FanController = require('./lib/FanController.js');
const Events = require('./lib/Events.js');
let Service, Characteristic, Homebridge, Accessory;
const PLUGIN_NAME = 'homebridge-xiaomi-fan';
const PLATFORM_NAME = 'xiaomifan';
const PLUGIN_VERSION = '1.5.9';
// General constants
const BATTERY_LOW_THRESHOLD = 20;
const BUTTON_RESET_TIMEOUT = 20; // in milliseconds
module.exports = function(homebridge) {
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
Homebridge = homebridge;
Accessory = homebridge.platformAccessory;
homebridge.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, xiaomiFanPlatform, true);
};
class xiaomiFanDevice {
constructor(log, config, api) {
this.log = log;
this.api = api;
// check if we have mandatory device info
try {
if (!config.ip) throw new Error(`'ip' is required but not defined for ${config.name}!`);
if (!config.token) throw new Error(`'token' is required but not defined for ${config.name}!`);
} catch (error) {
this.logError(error);
this.logError(`Failed to create platform device, missing mandatory information!`);
this.logError(`Please check your device config!`);
return;
}
// configuration
this.name = config['name'];
this.ip = config['ip'];
this.token = config['token'];
this.deviceId = config['deviceId'];
this.model = config['model'];
this.pollingInterval = config['pollingInterval'] || 5;
this.pollingInterval = this.pollingInterval * 1000;
this.prefsDir = config['prefsDir'] || api.user.storagePath() + '/.xiaomiFan/';
this.deepDebugLog = config.deepDebugLog;
if (this.deepDebugLog === undefined) {
this.deepDebugLog = false;
}
this.buzzerControl = config['buzzerControl'];
if (this.buzzerControl == undefined) {
this.buzzerControl = true;
}
this.ledControl = config['ledControl'];
if (this.ledControl == undefined) {
this.ledControl = true;
}
this.naturalModeControl = config['naturalModeControl'];
if (this.naturalModeControl == undefined) {
this.naturalModeControl = true;
}
this.sleepModeControl = config['sleepModeControl'];
if (this.sleepModeControl == undefined) {
this.sleepModeControl = true;
}
this.moveControl = config['moveControl'];
if (this.moveControl == undefined) {
this.moveControl = false;
}
this.fanLevelControl = config['fanLevelControl'];
if (this.fanLevelControl == undefined) {
this.fanLevelControl = true;
}
this.shutdownTimer = config['shutdownTimer'];
if (this.shutdownTimer == undefined) {
this.shutdownTimer = false;
}
this.ioniserControl = config['ioniserControl'];
if (this.ioniserControl == undefined) {
this.ioniserControl = false;
}
this.angleButtons = config['angleButtons'];
this.verticalAngleButtons = config['verticalAngleButtons'];
this.logInfo(`Init - got fan configuration, initializing device with name: ${this.name}`);
// check if prefs directory ends with a /, if not then add it
if (this.prefsDir.endsWith('/') === false) {
this.prefsDir = this.prefsDir + '/';
}
// check if the fan preferences directory exists, if not then create it
if (fs.existsSync(this.prefsDir) === false) {
mkdirp(this.prefsDir);
}
// create fan model info file name
this.fanInfoFile = this.prefsDir + 'info_' + this.ip.split('.').join('') + '_' + this.token;
// prepare variables
this.fanDevice = undefined;
this.cachedFanInfo = {};
this.rotationSpeedTimeout = null; // for rotation speed set debounce
//try to load cached fan info
this.loadFanInfo();
//start the fan discovery
this.discoverFan();
}
/*----------========== SETUP ==========----------*/
discoverFan() {
// if the user specified a model then use that, else try to get cached model
let fanController = new FanController(this.ip, this.token, this.deviceId, this.model || this.cachedFanInfo.model, this.name, this.pollingInterval, this.log);
fanController.setDeepDebugLogEnabled(this.deepDebugLog);
fanController.on(Events.FAN_DEVICE_READY, (fanDevice) => {
this.fanDevice = fanDevice;
//prepare the fan accessory and services
if (!this.fanAccesory) {
this.initFanAccessory();
}
});
fanController.on(Events.FAN_CONNECTED, (fanDevice) => {
// update fan information
this.updateInformationService();
// save fan information
this.saveFanInfo();
});
fanController.on(Events.FAN_DISCONNECTED, (fanDevice) => {
this.updateFanStatus();
});
fanController.on(Events.FAN_PROPERTIES_UPDATED, (fanDevice) => {
this.updateFanStatus();
});
fanController.connectToFan();
}
/*----------========== SETUP SERVICES ==========----------*/
initFanAccessory() {
// generate uuid
this.UUID = Homebridge.hap.uuid.generate(this.token + this.ip + PLATFORM_NAME);
// prepare the fan accessory
this.fanAccesory = new Accessory(this.name, this.UUID, Homebridge.hap.Accessory.Categories.FAN);
// prepare accessory services
if (this.fanDevice) {
this.setupAccessoryServices();
}
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [this.fanAccesory]);
}
setupAccessoryServices() {
// update the services
this.updateInformationService();
// prepare the fan service
this.prepareFanService();
// additional services
this.prepareMoveControlService();
this.prepareBuzzerControlService();
this.prepareLedControlService();
this.prepareNaturalModeControlService();
this.prepareShutdownTimerService();
this.prepareAngleButtonsService();
this.prepareVerticalAngleButtonsService();
this.prepareFanLevelControlService();
this.prepareSleepModeControlService();
this.prepareIoniserControlService();
this.prepareTemperatureService();
this.prepareRelativeHumidityService();
this.prepareBatteryService();
}
updateInformationService() {
// remove the preconstructed information service, since i will be adding my own
this.fanAccesory.removeService(this.fanAccesory.getService(Service.AccessoryInformation));
let fanModel = this.fanDevice.getFanModel() || 'Unknown';
let fanDeviceId = this.fanDevice.getDeviceId() || 'Unknown';
this.informationService = new Service.AccessoryInformation();
this.informationService
.setCharacteristic(Characteristic.Name, this.name)
.setCharacteristic(Characteristic.Manufacturer, 'Xiaomi')
.setCharacteristic(Characteristic.Model, fanModel)
.setCharacteristic(Characteristic.SerialNumber, fanDeviceId)
.setCharacteristic(Characteristic.FirmwareRevision, PLUGIN_VERSION);
this.fanAccesory.addService(this.informationService);
}
prepareFanService() {
this.fanService = new Service.Fanv2(this.name, 'fanService');
this.fanService
.getCharacteristic(Characteristic.Active)
.on('get', this.getPowerState.bind(this))
.on('set', this.setPowerState.bind(this));
this.fanService
.addCharacteristic(Characteristic.CurrentFanState) // for what is this used?
.on('get', this.getFanState.bind(this));
if (this.fanDevice.supportsFanSpeed()) {
this.fanService
.addCharacteristic(Characteristic.RotationSpeed)
.on('get', this.getRotationSpeed.bind(this))
.on('set', this.setRotationSpeed.bind(this));
}
this.fanService
.addCharacteristic(Characteristic.LockPhysicalControls)
.on('get', this.getLockPhysicalControls.bind(this))
.on('set', this.setLockPhysicalControls.bind(this));
this.fanService
.addCharacteristic(Characteristic.SwingMode)
.on('get', this.getSwingMode.bind(this))
.on('set', this.setSwingMode.bind(this));
this.fanService
.addCharacteristic(Characteristic.RotationDirection) // used to switch between buzzer levels on supported devices
.on('get', this.getRotationDirection.bind(this))
.on('set', this.setRotationDirection.bind(this));
this.fanAccesory.addService(this.fanService);
}
prepareMoveControlService() {
if (this.moveControl && this.fanDevice.supportsLeftRightMove()) {
this.moveLeftService = new Service.Switch('Move left', 'moveLeftService');
this.moveLeftService
.getCharacteristic(Characteristic.On)
.on('get', this.getMoveFanSwitch.bind(this))
.on('set', (state, callback) => {
this.setMoveFanSwitch(state, callback, 'left');
});
this.fanAccesory.addService(this.moveLeftService);
this.moveRightService = new Service.Switch('Move right', 'moveRightService');
this.moveRightService
.getCharacteristic(Characteristic.On)
.on('get', this.getMoveFanSwitch.bind(this))
.on('set', (state, callback) => {
this.setMoveFanSwitch(state, callback, 'right');
});
this.fanAccesory.addService(this.moveRightService);
}
if (this.moveControl && this.fanDevice.supportsUpDownMove()) {
this.moveUpService = new Service.Switch('Move Up', 'moveUpService');
this.moveUpService
.getCharacteristic(Characteristic.On)
.on('get', this.getMoveFanSwitch.bind(this))
.on('set', (state, callback) => {
this.setMoveFanSwitch(state, callback, 'up');
});
this.fanAccesory.addService(this.moveUpService);
this.moveDownService = new Service.Switch('Move down', 'moveDownService');
this.moveDownService
.getCharacteristic(Characteristic.On)
.on('get', this.getMoveFanSwitch.bind(this))
.on('set', (state, callback) => {
this.setMoveFanSwitch(state, callback, 'down');
});
this.fanAccesory.addService(this.moveDownService);
}
}
prepareBuzzerControlService() {
if (this.buzzerControl && this.fanDevice.supportsBuzzerControl()) {
this.buzzerService = new Service.Switch('Buzzer', 'buzzerService');
this.buzzerService
.getCharacteristic(Characteristic.On)
.on('get', this.getBuzzer.bind(this))
.on('set', this.setBuzzer.bind(this));
this.fanAccesory.addService(this.buzzerService);
}
}
prepareLedControlService() {
if (this.ledControl && this.fanDevice.supportsLedControl()) {
if (this.fanDevice.supportsLedBrightness()) {
// if brightness supported then add a lightbulb for controlling
this.ledBrightnessService = new Service.Lightbulb('LED', 'ledBrightnessService');
this.ledBrightnessService
.getCharacteristic(Characteristic.On)
.on('get', this.getLed.bind(this))
.on('set', this.setLed.bind(this));
this.ledBrightnessService
.addCharacteristic(new Characteristic.Brightness())
.on('get', this.getLedBrightness.bind(this))
.on('set', this.setLedBrightness.bind(this));
this.fanAccesory.addService(this.ledBrightnessService);
} else if (this.fanDevice.supportsLedControl()) {
// if not then just a simple switch
this.ledService = new Service.Switch('LED', 'ledService');
this.ledService
.getCharacteristic(Characteristic.On)
.on('get', this.getLed.bind(this))
.on('set', this.setLed.bind(this));
this.fanAccesory.addService(this.ledService);
}
}
}
prepareNaturalModeControlService() {
if (this.naturalModeControl && this.fanDevice.supportsNaturalMode()) {
this.naturalModeControlService = new Service.Switch('Natural mode', 'naturalModeControlService');
this.naturalModeControlService
.getCharacteristic(Characteristic.On)
.on('get', this.getNaturalMode.bind(this))
.on('set', this.setNaturalMode.bind(this));
this.fanAccesory.addService(this.naturalModeControlService);
}
}
prepareSleepModeControlService() {
if (this.sleepModeControl && this.fanDevice.supportsSleepMode()) {
this.sleepModeControlService = new Service.Switch('Sleep mode', 'sleepModeControlService');
this.sleepModeControlService
.getCharacteristic(Characteristic.On)
.on('get', this.getSleepMode.bind(this))
.on('set', this.setSleepMode.bind(this));
this.fanAccesory.addService(this.sleepModeControlService);
}
}
prepareShutdownTimerService() {
if (this.shutdownTimer && this.fanDevice.supportsPowerOffTimer()) {
this.shutdownTimerService = new Service.Lightbulb('Shutdown timer', 'shutdownTimerService');
this.shutdownTimerService
.getCharacteristic(Characteristic.On)
.on('get', this.getShutdownTimerEnabled.bind(this))
.on('set', this.setShutdownTimerEnabled.bind(this));
this.shutdownTimerService
.addCharacteristic(new Characteristic.Brightness())
.on('get', this.getShutdownTimer.bind(this))
.on('set', this.setShutdownTimer.bind(this));
this.fanAccesory.addService(this.shutdownTimerService);
}
}
prepareAngleButtonsService() {
if (this.fanDevice.supportsOscillationAngle() === false && this.fanDevice.supportsOscillationLevels() === false) {
return;
}
if (this.angleButtons === false) {
return;
}
if (this.angleButtons === undefined || this.angleButtons === null) {
if (this.fanDevice.supportsOscillationLevels()) {
// if the fan supports osicllation levels, and user did not specify the property then show all oscillation levels
this.angleButtons = this.fanDevice.oscillationLevels();
} else {
return;
}
}
if (Array.isArray(this.angleButtons) === false) {
this.logWarn('The angle buttons service needs to be defined as an array! Please correct your config.json if you want to use the service.');
return;
}
this.angleButtonsService = new Array();
this.angleButtons.forEach((value, i) => {
let parsedValue = parseInt(value);
if (this.checkAngleButtonValue(parsedValue) === false) {
return;
}
this.angleButtons[i] = parsedValue;
let tmpAngleButton = new Service.Switch('Angle - ' + parsedValue, 'angleButtonService' + i);
tmpAngleButton
.getCharacteristic(Characteristic.On)
.on('get', (callback) => {
this.getAngleButtonState(callback, parsedValue);
})
.on('set', (state, callback) => {
this.setAngleButtonState(state, callback, parsedValue);
});
this.fanAccesory.addService(tmpAngleButton);
this.angleButtonsService.push(tmpAngleButton);
});
}
prepareVerticalAngleButtonsService() {
if (this.fanDevice.supportsVerticalOscillationAngle() === false && this.fanDevice.supportsOscillationVerticalLevels() === false) {
return;
}
if (this.verticalAngleButtons === false) {
return;
}
if (this.verticalAngleButtons === undefined || this.verticalAngleButtons === null) {
if (this.fanDevice.supportsOscillationVerticalLevels()) {
// if the fan supports vertical oscillation levels, and user did not specify the property then show all oscillation levels
this.verticalAngleButtons = this.fanDevice.oscillationVerticalLevels();
} else {
return;
}
}
if (Array.isArray(this.verticalAngleButtons) === false) {
this.logWarn('The vertical angle buttons service needs to be defined as an array! Please correct your config.json if you want to use the service.');
return;
}
this.verticalAngleButtonsService = new Array();
this.verticalAngleButtons.forEach((value, i) => {
let parsedValue = parseInt(value);
if (this.checkVerticalAngleButtonValue(parsedValue) === false) {
return;
}
this.verticalAngleButtons[i] = parsedValue;
let tmpAngleButton = new Service.Switch('Vertical Angle - ' + parsedValue, 'verticalAngleButtonService' + i);
tmpAngleButton
.getCharacteristic(Characteristic.On)
.on('get', (callback) => {
this.getVerticalAngleButtonState(callback, parsedValue);
})
.on('set', (state, callback) => {
this.setVerticalAngleButtonState(state, callback, parsedValue);
});
this.fanAccesory.addService(tmpAngleButton);
this.verticalAngleButtonsService.push(tmpAngleButton);
});
}
prepareFanLevelControlService() {
if (this.fanLevelControl && this.fanDevice.supportsFanLevel()) {
this.fanLevelControlService = new Array();
for (let i = 1; i <= this.fanDevice.numberOfFanLevels(); i++) {
let tmpFanLevelButton = new Service.Switch('Level ' + i, 'levelControlService' + i);
tmpFanLevelButton
.getCharacteristic(Characteristic.On)
.on('get', (callback) => {
this.getFanLevelState(callback, i);
})
.on('set', (state, callback) => {
this.setFanLevelState(state, callback, i);
});
this.fanAccesory.addService(tmpFanLevelButton);
this.fanLevelControlService.push(tmpFanLevelButton);
}
}
}
prepareIoniserControlService() {
if (this.ioniserControl && this.fanDevice.supportsIoniser()) {
this.ioniserControlService = new Service.Switch('Ioniser', 'ioniserControlService');
this.ioniserControlService
.getCharacteristic(Characteristic.On)
.on('get', this.getIoniserState.bind(this))
.on('set', this.setIoniserState.bind(this));
this.fanAccesory.addService(this.ioniserControlService);
}
}
prepareTemperatureService() {
if (this.fanDevice.supportsTemperatureReporting()) {
this.temperatureService = new Service.TemperatureSensor('Temp', 'temperatureService');
this.temperatureService
.setCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.NO_FAULT)
.setCharacteristic(Characteristic.StatusTampered, Characteristic.StatusTampered.NOT_TAMPERED)
.setCharacteristic(Characteristic.StatusLowBattery, Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL);
this.temperatureService
.getCharacteristic(Characteristic.CurrentTemperature)
.on('get', this.getCurrentTemperature.bind(this));
this.fanAccesory.addService(this.temperatureService);
}
}
prepareRelativeHumidityService() {
if (this.fanDevice.supportsRelativeHumidityReporting()) {
this.relativeHumidityService = new Service.HumiditySensor('Humidity', 'relativeHumidityService');
this.relativeHumidityService
.setCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.NO_FAULT)
.setCharacteristic(Characteristic.StatusTampered, Characteristic.StatusTampered.NOT_TAMPERED)
.setCharacteristic(Characteristic.StatusLowBattery, Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL);
this.relativeHumidityService
.getCharacteristic(Characteristic.CurrentRelativeHumidity)
.on('get', this.getCurrentRelativeHumidity.bind(this));
this.fanAccesory.addService(this.relativeHumidityService);
}
}
prepareBatteryService() {
if (this.fanDevice.hasBuiltInBattery() && this.fanDevice.supportsBatteryStateReporting()) {
this.batteryService = new Service.BatteryService('Battery', 'batteryService');
this.batteryService
.setCharacteristic(Characteristic.ChargingState, Characteristic.ChargingState.NOT_CHARGING)
.setCharacteristic(Characteristic.StatusLowBattery, Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL);
this.batteryService
.getCharacteristic(Characteristic.BatteryLevel)
.on('get', this.getBatteryLevel.bind(this));
this.batteryService
.getCharacteristic(Characteristic.StatusLowBattery)
.on('get', this.getBatteryLevelStatus.bind(this));
this.fanAccesory.addService(this.batteryService);
}
}
/*----------========== HOMEBRIDGE STATE SETTERS/GETTERS ==========----------*/
getPowerState(callback) {
let isFanOn = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
isFanOn = this.fanDevice.isPowerOn();
}
callback(null, isFanOn ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE);
}
setPowerState(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
let isPowerOn = state === Characteristic.Active.ACTIVE;
// only fire the setPowerOn method when we want to turn off the fan or the fan is off
// the rotaion speed slider fires this method many times even when the fan is already on so i need to limit that
if (isPowerOn === false || this.fanDevice.isPowerOn() === false) {
this.fanDevice.setPowerOn(isPowerOn);
}
callback();
} else {
callback(this.createError(`cannot set power state`));
}
}
getFanState(callback) {
let fanState = Characteristic.CurrentFanState.INACTIVE;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
fanState = this.fanDevice.isPowerOn() ? Characteristic.CurrentFanState.BLOWING_AIR : Characteristic.CurrentFanState.IDLE
}
callback(null, fanState);
}
getRotationSpeed(callback) {
let fanRotationSpeed = 0;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
fanRotationSpeed = this.fanDevice.getRotationSpeed();
fanRotationSpeed = this.adjustToPercentageRange(fanRotationSpeed);
}
callback(null, fanRotationSpeed);
}
setRotationSpeed(value, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
// use debounce to limit the number of calls when the user slides the rotation slider
if (this.rotationSpeedTimeout) clearTimeout(this.rotationSpeedTimeout);
this.rotationSpeedTimeout = setTimeout(() => this.fanDevice.setRotationSpeed(value), 500);
callback();
} else {
callback(this.createError(`cannot set rotation speed`));
}
}
getLockPhysicalControls(callback) {
let isChildLockActive = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
isChildLockActive = this.fanDevice.isChildLockActive();
}
callback(null, isChildLockActive ? Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED : Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED);
}
setLockPhysicalControls(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
let isChildLockActive = state === Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED;
this.fanDevice.setChildLock(isChildLockActive);
callback();
} else {
callback(this.createError(`cannot set child lock state`));
}
}
getSwingMode(callback) {
let isSwingModeActive = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
isSwingModeActive = this.fanDevice.isSwingModeEnabled();
}
callback(null, isSwingModeActive ? Characteristic.SwingMode.SWING_ENABLED : Characteristic.SwingMode.SWING_DISABLED);
}
setSwingMode(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
let isSwingModeActive = state === Characteristic.SwingMode.SWING_ENABLED;
this.fanDevice.setSwingModeEnabled(isSwingModeActive);
this.updateAngleButtonsAndSwingMode(null, isSwingModeActive); // update the angel buttons if enabled
callback();
} else {
callback(this.createError(`cannot set swing mode state`));
}
}
getRotationDirection(callback) {
let buzzerLevel = 2;
if (this.fanDevice && this.fanDevice.isFanConnected() && this.fanDevice.supportsBuzzerLevelControl()) {
buzzerLevel = this.fanDevice.getBuzzerLevel();
}
callback(null, buzzerLevel === 1 ? Characteristic.RotationDirection.CLOCKWISE : Characteristic.RotationDirection.COUNTER_CLOCKWISE);
}
setRotationDirection(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected() && this.fanDevice.supportsBuzzerLevelControl()) {
if (this.fanDevice.isBuzzerEnabled() === true) {
let buzzerLevel = state === Characteristic.RotationDirection.CLOCKWISE ? 1 : 2;
this.fanDevice.setBuzzerLevel(buzzerLevel);
}
callback();
} else {
// throw no error on devices which does not support the buzzer control, simply do nothing
// callback(this.createError(`cannot set buzzer level`));
}
}
getMoveFanSwitch(callback) {
callback(null, false);
}
setMoveFanSwitch(state, callback, direction) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
if (direction === 'left') {
this.fanDevice.moveLeft();
} else if (direction === 'right') {
this.fanDevice.moveRight();
} else if (direction === 'up') {
this.fanDevice.moveUp();
} else if (direction === 'down') {
this.fanDevice.modeDown();
}
setTimeout(() => {
if (this.moveLeftService) this.moveLeftService.getCharacteristic(Characteristic.On).updateValue(false);
if (this.moveRightService) this.moveRightService.getCharacteristic(Characteristic.On).updateValue(false);
if (this.moveUpService) this.moveUpService.getCharacteristic(Characteristic.On).updateValue(false);
if (this.moveDownService) this.moveDownService.getCharacteristic(Characteristic.On).updateValue(false);
}, BUTTON_RESET_TIMEOUT);
callback();
} else {
callback(this.createError(`cannot move fan`));
}
}
getBuzzer(callback) {
let isBuzzerEnabled = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
isBuzzerEnabled = this.fanDevice.isBuzzerEnabled();
}
callback(null, isBuzzerEnabled);
}
setBuzzer(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
this.fanDevice.setBuzzerEnabled(state);
callback();
} else {
callback(this.createError(`cannot set buzzer state`));
}
}
getLed(callback) {
let isLedEnabled = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
isLedEnabled = this.fanDevice.isLedEnabled();
}
callback(null, isLedEnabled);
}
setLed(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
if (state === false || this.fanDevice.isLedEnabled() === false) {
this.fanDevice.setLedEnabled(state);
}
callback();
} else {
callback(this.createError(`cannot set LED state`));
}
}
getLedBrightness(callback) {
let ledBrightness = 0;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
ledBrightness = this.fanDevice.getLedBrightness();
ledBrightness = this.adjustToPercentageRange(ledBrightness);
}
callback(null, ledBrightness);
}
setLedBrightness(value, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
this.fanDevice.setLedBrightness(value);
callback();
} else {
callback(this.createError(`cannot set LED brightness`));
}
}
getNaturalMode(callback) {
let naturalModeButtonEnabled = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
naturalModeButtonEnabled = this.fanDevice.isNaturalModeEnabled();
}
callback(null, naturalModeButtonEnabled);
}
setNaturalMode(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
this.fanDevice.setNaturalModeEnabled(state);
callback();
} else {
callback(this.createError(`cannot set natural mode state`));
}
}
getSleepMode(callback) {
let sleepModeButtonEnabled = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
sleepModeButtonEnabled = this.fanDevice.isSleepModeEnabled();
}
callback(null, sleepModeButtonEnabled);
}
setSleepMode(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
this.fanDevice.setSleepModeEnabled(state);
callback();
} else {
callback(this.createError(`cannot set sleep mode state`));
}
}
getShutdownTimerEnabled(callback) {
let isShutdownTimerEnabled = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
isShutdownTimerEnabled = this.fanDevice.isShutdownTimerEnabled();
}
callback(null, isShutdownTimerEnabled);
}
setShutdownTimerEnabled(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
if (state === false) { // only if disabling, enabling will automatically set it to 100%
this.fanDevice.setShutdownTimer(0);
}
callback();
} else {
callback(this.createError(`cannot set shutdown timer state`));
}
}
getShutdownTimer(callback) {
let shutdownTimerTime = 0;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
shutdownTimerTime = this.fanDevice.getShutdownTimer();
shutdownTimerTime = this.adjustToPercentageRange(shutdownTimerTime);
}
callback(null, shutdownTimerTime);
}
setShutdownTimer(level, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
this.fanDevice.setShutdownTimer(level);
callback();
} else {
callback(this.createError(`cannot set shutdown timer time`));
}
}
getAngleButtonState(callback, angle) {
let angleButtonEnabled = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
if (this.fanDevice.isPowerOn() && this.fanDevice.isSwingModeEnabled()) {
angleButtonEnabled = this.fanDevice.getAngle() === angle;
}
}
callback(null, angleButtonEnabled);
}
setAngleButtonState(state, callback, angle) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
if (state) {
// if swing mode disabled then turn it on
if (this.fanDevice.isSwingModeEnabled() === false) {
this.fanDevice.setSwingModeEnabled(true);
}
this.fanDevice.setAngle(angle);
} else {
this.fanDevice.setSwingModeEnabled(false);
}
this.updateAngleButtonsAndSwingMode(angle, state);
callback();
} else {
callback(this.createError(`cannot set swing angle`));
}
}
getVerticalAngleButtonState(callback, angle) {
let verticalAngleButtonEnabled = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
if (this.fanDevice.isPowerOn() && this.fanDevice.isVerticalSwingModeEnabled()) {
verticalAngleButtonEnabled = this.fanDevice.getVerticalAngle() === angle;
}
}
callback(null, verticalAngleButtonEnabled);
}
setVerticalAngleButtonState(state, callback, angle) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
if (state) {
// if vertical swing mode disabled then turn it on
if (this.fanDevice.isVerticalSwingModeEnabled() === false) {
this.fanDevice.setVerticalSwingModeEnabled(true);
}
this.fanDevice.setVerticalAngle(angle);
} else {
this.fanDevice.setVerticalSwingModeEnabled(false);
}
this.updateVerticalAngleButtonsAndSwingMode(angle, state);
callback();
} else {
callback(this.createError(`cannot set vertical swing angle`));
}
}
getFanLevelState(callback, level) {
let levelButtonEnabled = false;
if (this.fanDevice && this.fanDevice.isFanConnected() && this.fanDevice.isPowerOn()) {
levelButtonEnabled = this.fanDevice.getFanLevel() === level;
}
callback(null, levelButtonEnabled);
}
setFanLevelState(state, callback, level) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
if (state) {
// if fan turned off then turn it on
if (this.fanDevice.isPowerOn() === false) {
this.fanDevice.setPowerOn(true);
}
this.fanDevice.setFanLevel(level);
}
setTimeout(() => {
this.updateFanLevelButtons();
}, BUTTON_RESET_TIMEOUT);
callback();
} else {
callback(this.createError(`cannot set fan level`));
}
}
getIoniserState(callback) {
let ioniserButtonEnabled = false;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
ioniserButtonEnabled = this.fanDevice.isIoniserEnabled();
}
callback(null, ioniserButtonEnabled);
}
setIoniserState(state, callback) {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
this.fanDevice.setIoniserEnabled(state);
callback();
} else {
callback(this.createError(`cannot set ioniser state`));
}
}
getCurrentTemperature(callback) {
let temp = 0;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
temp = this.fanDevice.getTemperature();
}
callback(null, temp);
}
getCurrentRelativeHumidity(callback) {
let relHumidity = 0;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
relHumidity = this.fanDevice.getRelativeHumidity();
}
callback(null, relHumidity);
}
getBatteryLevel(callback) {
let batteryLevel = 0;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
batteryLevel = this.fanDevice.getBatteryLevel();
}
callback(null, batteryLevel);
}
getBatteryLevelStatus(callback) {
let batteryLevelStatus = Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
if (this.fanDevice && this.fanDevice.isFanConnected()) {
batteryLevelStatus = this.fanDevice.getBatteryLevel() <= BATTERY_LOW_THRESHOLD ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
}
callback(null, batteryLevelStatus);
}
/*----------========== HELPERS ==========----------*/
updateFanStatus() {
if (this.fanDevice && this.fanDevice.isFanConnected()) {
if (this.fanService) this.fanService.getCharacteristic(Characteristic.Active).updateValue(this.fanDevice.isPowerOn() ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE);
if (this.fanService && this.fanDevice.supportsFanSpeed()) this.fanService.getCharacteristic(Characteristic.RotationSpeed).updateValue(this.adjustToPercentageRange(this.fanDevice.getRotationSpeed()));
if (this.fanService) this.fanService.getCharacteristic(Characteristic.LockPhysicalControls).updateValue(this.fanDevice.isChildLockActive() ? Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED : Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED);
if (this.fanService && this.fanDevice.supportsBuzzerLevelControl()) this.fanService.getCharacteristic(Characteristic.RotationDirection).updateValue(this.fanDevice.getBuzzerLevel() === 1 ? Characteristic.RotationDirection.CLOCKWISE : Characteristic.RotationDirection.COUNTER_CLOCKWISE);
if (this.buzzerService) this.buzzerService.getCharacteristic(Characteristic.On).updateValue(this.fanDevice.isBuzzerEnabled());
if (this.ledService) this.ledService.getCharacteristic(Characteristic.On).updateValue(this.fanDevice.isLedEnabled());
if (this.ledBrightnessService) this.ledBrightnessService.getCharacteristic(Characteristic.On).updateValue(this.fanDevice.isLedEnabled());
if (this.ledBrightnessService) this.ledBrightnessService.getCharacteristic(Characteristic.Brightness).updateValue(this.adjustToPercentageRange(this.fanDevice.getLedBrightness()));
if (this.naturalModeControlService) this.naturalModeControlService.getCharacteristic(Characteristic.On).updateValue(this.fanDevice.isNaturalModeEnabled());
if (this.sleepModeControlService) this.sleepModeControlService.getCharacteristic(Characteristic.On).updateValue(this.fanDevice.isSleepModeEnabled());
if (this.shutdownTimerService) this.shutdownTimerService.getCharacteristic(Characteristic.On).updateValue(this.fanDevice.isShutdownTimerEnabled());
if (this.shutdownTimerService) this.shutdownTimerService.getCharacteristic(Characteristic.Brightness).updateValue(this.adjustToPercentageRange(this.fanDevice.getShutdownTimer()));
if (this.ioniserControlService) this.ioniserControlService.getCharacteristic(Characteristic.On).updateValue(this.fanDevice.isIoniserEnabled());
if (this.temperatureService) this.temperatureService.getCharacteristic(Characteristic.CurrentTemperature).updateValue(this.fanDevice.getTemperature());
if (this.relativeHumidityService) this.relativeHumidityService.getCharacteristic(Characteristic.CurrentRelativeHumidity).updateValue(this.fanDevice.getRelativeHumidity());
if (this.batteryService) this.batteryService.getCharacteristic(Characteristic.BatteryLevel).updateValue(this.fanDevice.getBatteryLevel());
if (this.batteryService) this.batteryService.getCharacteristic(Characteristic.StatusLowBattery).updateValue(this.fanDevice.getBatteryLevel() <= BATTERY_LOW_THRESHOLD ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL);
this.updateAngleButtonsAndSwingMode(null, this.fanDevice.isSwingModeEnabled());
this.updateVerticalAngleButtonsAndSwingMode(null, this.fanDevice.isVerticalSwingModeEnabled());
this.updateFanLevelButtons();
}
}
updateAngleButtonsAndSwingMode(activeAngle, enabled) {
if (this.fanService) this.fanService.getCharacteristic(Characteristic.SwingMode).updateValue(enabled ? Characteristic.SwingMode.SWING_ENABLED : Characteristic.SwingMode.SWING_DISABLED);
if (this.angleButtonsService) {
// if swing mode disabled or the fan is not turned on then just disable all the angle switches
if (enabled === false || this.fanDevice.isPowerOn() === false) {
activeAngle = "disabled"; // use fake value for angle
}
// if angle not specified then automatically update the status
if (activeAngle === undefined || activeAngle === null) {
activeAngle = this.fanDevice.getAngle();
}
this.angleButtonsService.forEach((tmpAngleButton, i) => {
if (activeAngle === this.angleButtons[i]) {
tmpAngleButton.getCharacteristic(Characteristic.On).updateValue(true);
} else {
tmpAngleButton.getCharacteristic(Characteristic.On).updateValue(false);
}
});
}
}
updateVerticalAngleButtonsAndSwingMode(activeAngle, enabled) {
if (this.verticalAngleButtonsService) {
// if vertical swing mode disabled or the fan is not turned on then just disable all the angle switches
if (enabled === false || this.fanDevice.isPowerOn() === false) {
activeAngle = "disabled"; // use fake value for angle
}
// if vertical angle not specified then automatically update the status
if (activeAngle === undefined || activeAngle === null) {
activeAngle = this.fanDevice.getVerticalAngle();
}
this.verticalAngleButtonsService.forEach((tmpAngleButton, i) => {
if (activeAngle === this.verticalAngleButtons[i]) {
tmpAngleButton.getCharacteristic(Characteristic.On).updateValue(true);
} else {
tmpAngleButton.getCharacteristic(Characteristic.On).updateValue(false);
}
});
}
}
updateFanLevelButtons() {
if (this.fanLevelControlService) {
let currentLevel = this.fanDevice.getFanLevel();
this.fanLevelControlService.forEach((tmpFanLevelButton, i) => {
if (currentLevel === i + 1 && this.fanDevice.isPowerOn()) { // levels start from 1, index from 0 hence add 1
tmpFanLevelButton.getCharacteristic(Characteristic.On).updateValue(true);
} else {
tmpFanLevelButton.getCharacteristic(Characteristic.On).updateValue(false);
}
});
}
}
createError(msg) {
return new Error(`[${this.name}] Fan is not connected, ` + msg);
}
saveFanInfo() {
// save model name and deviceId
if (this.fanDevice) {
this.cachedFanInfo.model = this.fanDevice.getFanModel();
this.cachedFanInfo.deviceId = this.fanDevice.getDeviceId();
fs.writeFile(this.fanInfoFile, JSON.stringify(this.cachedFanInfo), (err) => {
if (err) {
this.logDebug('Error occured could not write fan model info %s', err);
} else {
this.logDebug('Successfully saved fan info!');
}
});
}
}
loadFanInfo() {
try {
this.cachedFanInfo = JSON.parse(fs.readFileSync(this.fanInfoFile));
} catch (err) {
this.logDebug('Fan info file does not exist yet, device unknown!');
}
}
checkAngleButtonValue(angleValue) {
if (this.fanDevice.supportsOscillationAngle()) {
// if specified angle not within range then show a a warning and stop processing this value
if (this.fanDevice.checkOscillationAngleWithinRange(angleValue) === false) {
this.logWarn(`Specified angle ${angleValue} is not within the supported range ${JSON.stringify(this.fanDevice.oscillationAngleRange())}. Not adding angle button!`);
return false;
}
} else if (this.fanDevice.supportsOscillationLevels()) {
// if the fan uses predefined osiscllation levels then check if the specified angle is on the list
if (this.fanDevice.checkOscillationLevelSupported(angleValue) === false) {
this.logWarn(`Specified angle ${angleValue} is not within the supported angle levels of your fan. Allowed values: ${JSON.stringify(this.fanDevice.oscillationLevels())}. Not adding angle button!`);
return false;
}
}
return true;
}
checkVerticalAngleButtonValue(angleValue) {
if (this.fanDevice.supportsVerticalOscillationAngle()) {
// if specified angle not within range then show a a warning and stop processing this value
if (this.fanDevice.checkVerticalOscillationAngleWithinRange(angleValue) === false) {
this.logWarn(`Specified vertical angle ${angleValue} is not within the supported vertical range ${JSON.stringify(this.fanDevice.oscillationVerticalAngleRange())}. Not adding vertical angle button!`);
return false;
}
} else if (this.fanDevice.supportsOscillationVerticalLevels()) {
// if the fan uses predefined vertical oscillation levels then check if the specified angle is on the list
if (this.fanDevice.checkVerticalOscillationLevelSupported(angleValue) === false) {
this.logWarn(`Specified vertical angle ${angleValue} is not within the supported vertical angle levels of your fan. Allowed values: ${JSON.stringify(this.fanDevice.verticalOscillationLevels())}. Not adding vertical angle button!`);
return false;
}
}
return true;
}
adjustToPercentageRange(value) {
// make sure a percentage value is a number and withinh 0-100
let newValue = value;
if (!Number.isFinite(newValue)) newValue = 0;
if (newValue > 100) newValue = 100;
if (newValue < 0) newValue = 0;
return newValue;
}
/*----------========== LOG ==========----------*/
logInfo(message, ...args) {
this.log.info((this.name ? `[${this.name}] ` : "") + message, ...args);
}
logWarn(message, ...args) {
this.log.warn((this.name ? `[${this.name}] ` : "") + message, ...args);
}
logDebug(message, ...args) {
this.log.debug((this.name ? `[${this.name}] ` : "") + message, ...args);
}
logError(message, ...args) {
this.log.error((this.name ? `[${this.name}] ` : "") + message, ...args);
}
}
/*----------========== PLATFORM STUFF ==========----------*/
class xiaomiFanPlatform {
constructor(log, config, api) {
this.fans = [];
this.log = log;
this.api = api;
this.config = config;
if (this.api) {
/*
* When this event is fired, homebridge restored all cached accessories from disk and did call their respective
* `configureAccessory` method for all of them. Dynamic Platform plugins should only register new accessories
* after this event was fired, in order to ensure they weren't added to homebridge already.
* This event can also be used to start discovery of new accessories.
*/
this.api.on("didFinishLaunching", () => {
this.removeAccessories(); // remove all cached devices, we do not want to use cache for now, maybe in future?
this.initDevices();
});
}
}
/*
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory) {
this.log.debug("Found cached accessory %s", accessory.displayName);
this.fans.push(accessory);
}
// ------------ CUSTOM METHODS ------------
initDevices() {
this.log.info('Init - initializing devices');
// read from config.devices
if (this.config.devices && Array.isArray(this.config.devices)) {
for (let device of this.config.devices) {
if (device) {
new xiaomiFanDevice(this.log, device, this.api);
}
}
} else if (this.config.devices) {
this.log.info('The devices property is not of type array. Cannot initialize. Type: %s', typeof this.config.devices);
}
// also read from config.fans
if (this.config.fans && Array.isArray(this.config.fans)) {
for (let fan of this.config.fans) {
if (fan) {
new xiaomiFanDevice(this.log, fan, this.api);
}
}
} else if (this.config.fans) {
this.log.info('The fans property is not of type array. Cannot initialize. Type: %s', typeof this.config.fans);
}
if (!this.config.devices && !this.config.fans) {
this.log.info('-------------------------------------------');
this.log.info('Init - no fan configuration found');
this.log.info('Missing devices or fans in your platform config');
this.log.info('-------------------------------------------');
}
}
removeAccessories() {
// we don't have any special identifiers, we just remove all our accessories
this.log.debug("Removing all cached accessories");
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, this.fans);
this.fans = []; // clear out the array
}
removeAccessory(accessory) {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.fans = this.fans.filter(item => item !== accessory);
}
}