homebridge-xiaomi-fan
Version:
Homebridge plugin for Xiaomi Smartmi Fans
599 lines (448 loc) • 15.3 kB
JavaScript
const miio = require('miio');
var EventEmitter = require('events');
const FanCapabilities = require('./FanCapabilities.js');
// general constants
const COMMAND_NOT_SUPPORTED_MSG = 'Not supported: The requested command is not supported by this device!';
// DEVICES: http://miot-spec.org/miot-spec-v2/instances?status=all
class BaseFan extends EventEmitter {
constructor(miioDevice, model, deviceId, name, log) {
super();
// config
this.deviceId = deviceId;
this.model = model;
this.name = name;
this.log = log || console;
this.deepDebugLog = false;
//fan info
this.miioFanDevice = undefined;
this.fanInfo = {};
// prepare the variables
this.capabilities = {};
// init fan capabilities
this.initFanCapabilities()
// if we construct with a miiodevice then we can start with the setup
if (miioDevice) {
this.miioFanDevice = miioDevice;
this.setupFan();
}
}
/*----------========== INIT ==========----------*/
initFanCapabilities() {
this.addCapability(FanCapabilities.POWER_CONTROL, true);
}
/*----------========== SETUP ==========----------*/
setupFan() {
this.logDebug(`Setting up fan!`);
// get the fan info
this.logDebug(`Getting device info.`);
this.miioFanDevice.management.info().then((info) => {
this.fanInfo = info;
}).catch(err => {
this.logDebug(`Could not retrieve device info: ${err}`);
});
// get the fan deviceId if not specified
if (!this.deviceId) {
this.deviceId = this.getDeviceId();
this.logDebug(`Got fan did: ${this.deviceId}.`);
}
// do a model specific fan setup
this.logDebug(`Doing model specific setup.`);
this.modelSpecificSetup();
// add properties to the fan
this.logDebug(`Adding properties to fan.`);
this.addFanProperties();
// initial properties fetch
this.logDebug(`Doing initial properties fetch.`);
this.doInitialPropertiesFetch();
this.logDebug(`Setup finished! Fan can now be controlled!`);
}
/*----------========== DEVICE CONTROL ==========----------*/
disconnectAndDestroyMiioDevice() {
if (this.miioFanDevice) {
this.miioFanDevice.destroy();
}
this.miioFanDevice = undefined;
}
updateMiioDevice(newMiioDevice) {
this.miioFanDevice = newMiioDevice;
this.setupFan();
}
/*----------========== DEVICE LIFECYCLE ==========----------*/
modelSpecificSetup() {
this.logDebug(`Needs to be implemented by devices!`);
}
addFanProperties() {
this.logDebug(`Needs to be implemented by devices!`);
}
doInitialPropertiesFetch() {
this.logDebug(`Needs to be implemented by devices!`);
}
async pollProperties() {
this.logDebug(`Needs to be implemented by devices!`);
}
getFanProperties() {
this.logDebug(`Needs to be implemented by devices!`);
}
/*----------========== INFO ==========----------*/
isFanConnected() {
return this.miioFanDevice !== undefined;
}
getFanModel() {
if (this.isFanConnected()) {
return this.miioFanDevice.miioModel;
}
return this.model;
}
isDmakerFan() {
return this.getFanModel().includes('dmaker');
}
isSmartmiFan() {
return this.getFanModel().includes('zhimi');
}
getFanInfo() {
return this.fanInfo;
}
getProtocolType() {
this.logDebug(`Needs to be implemented by devices!`);
}
isMiioDevice() {
return this.getProtocolType() === 'miio';
}
isMiotDevice() {
return this.getProtocolType() === 'miot';
}
getDeviceId() {
if (this.isFanConnected()) {
return this.miioFanDevice.id.replace(/^miio:/, '');
}
return this.deviceId;
}
/*----------========== CAPABILITIES ==========----------*/
supportsPowerControl() {
return this.capabilities[FanCapabilities.POWER_CONTROL] || true; // every fan supports that? on/off
}
supportsFanSpeed() {
return this.capabilities[FanCapabilities.FAN_SPEED_CONTROL] || false; // free speed selection from 0% to 100%
}
supportsFanSpeedRpmReporting() {
return this.capabilities[FanCapabilities.FAN_SPEED_RPM_REPORTING] || false; // whether the fan reports the rpm speed value
}
supportsFanLevel() {
return this.capabilities[FanCapabilities.FAN_LEVEL_CONTROL] || false; // preconfigured fan levels which can be set
}
numberOfFanLevels() {
return this.capabilities[FanCapabilities.NUMBER_OF_FAN_LEVELS] || 0; // how many fan levels
}
supportsOscillation() {
return this.capabilities[FanCapabilities.OSCILLATION_CONTROL] || false; // fan moves left to right on/off
}
supportsOscillationAngle() {
return this.capabilities[FanCapabilities.OSCILLATION_ANGLE_CONTROL] || false; // if a custom angle can be set for oscillation usually 0 to 120 degree
}
oscillationAngleRange() {
return this.capabilities[FanCapabilities.OSCILLATION_ANGLE_RANGE] || []; // range for oscillation angle
}
supportsOscillationLevels() {
return this.capabilities[FanCapabilities.OSCILLATION_LEVEL_CONTROL] || false; // preconfigured levels for oscillation
}
oscillationLevels() {
return this.capabilities[FanCapabilities.OSCILLATION_LEVELS] || []; // array of levels (in degree) for oscillation
}
supportsVerticalOscillation() {
return this.capabilities[FanCapabilities.OSCILLATION_VERTICAL_CONTROL] || false; // fan moves up to down on/off
}
supportsVerticalOscillationAngle() {
return this.capabilities[FanCapabilities.OSCILLATION_VERTICAL_ANGLE_CONTROL] || false; // if a custom angle can be set for vertical oscillation usually 0 to 90 degree
}
oscillationVerticalAngleRange() {
return this.capabilities[FanCapabilities.OSCILLATION_VERTICAL_ANGLE_RANGE] || []; // range for vertical oscillation angle
}
supportsOscillationVerticalLevels() {
return this.capabilities[FanCapabilities.OSCILLATION_VERTICAL_LEVEL_CONTROL] || false; // preconfigured levels for vertical oscillation
}
oscillationVerticalLevels() {
return this.capabilities[FanCapabilities.OSCILLATION_VERTICAL_LEVELS] || []; // array of levels (in degree) for vertical oscillation
}
supportsLeftRightMove() {
return this.capabilities[FanCapabilities.LEFT_RIGHT_MOVE] || false; // whether the fan can be rotated left or right by 5 degree
}
supportsUpDownMove() {
return this.capabilities[FanCapabilities.UP_DOWN_MOVE] || false; // whether the fan can be rotated up or down by 5 degree
}
supportsNaturalMode() {
return this.capabilities[FanCapabilities.NATURAL_MODE] || false; // whether the natural mode is supported on/off
}
supportsSleepMode() {
return this.capabilities[FanCapabilities.SLEEP_MODE] || false; // whether the sleep mode is supported on/off
}
supportsChildLock() {
return this.capabilities[FanCapabilities.CHILD_LOCK] || false; // should be clear on/off
}
supportsPowerOffTimer() {
return this.capabilities[FanCapabilities.POWER_OFF_TIMER] || false; // if a power off timer can be configured
}
powerOffTimerUnit() {
return this.capabilities[FanCapabilities.POWER_OFF_TIMER_UNIT] || ''; // the unit of the power off timer
}
supportsBuzzerControl() {
return this.capabilities[FanCapabilities.BUZZER_CONTROL] || false; // if buzzer can be configured on/off
}
supportsBuzzerLevelControl() {
return this.capabilities[FanCapabilities.BUZZER_LEVEL_CONTROL] || false; // if buzzer can be set to different levels eg 0,1,2
}
buzzerLevels() {
return this.capabilities[FanCapabilities.BUZZER_LEVELS] || []; // array of levels (in degree) for buzzer
}
supportsLedControl() {
return this.capabilities[FanCapabilities.LED_CONTROL] || false; // if indicator light can be configured on/off
}
supportsLedLevelControl() {
return this.capabilities[FanCapabilities.LED_LEVEL_CONTROL] || false; // if indicator light can be set to different levels eg 0,1,2
}
ledLevels() {
return this.capabilities[FanCapabilities.LED_LEVELS] || []; // array of levels (in degree) for indicator light
}
supportsLedBrightness() {
return this.capabilities[FanCapabilities.LED_CONTROL_BRIGHTNESS] || false; // if indicator light can be controlled like a light bulb with 0 to 100% percent values
}
supportsUseTimeReporting() {
return this.capabilities[FanCapabilities.USE_TIME_REPORTING] || false; // whether the fan returns use time
}
supportsIoniser() {
return this.capabilities[FanCapabilities.IONISER_CONTROL] || false; // whether the fan has a built in ioniser which can be controled on/off
}
supportsTemperatureReporting() {
return this.capabilities[FanCapabilities.TEMPERATURE_REPORTING] || false; // whether the fan has a built in temperature sensor which can be read
}
supportsRelativeHumidityReporting() {
return this.capabilities[FanCapabilities.HUMIDITY_REPORTING] || false; // whether the fan has a built in humidity sensor which can be read
}
hasBuiltInBattery() {
return this.capabilities[FanCapabilities.BUILT_IN_BATTERY] || false; // whether the fan has a built in battery
}
supportsBatteryStateReporting() {
return this.capabilities[FanCapabilities.BATTERY_STATE_REPORTING] || false; // whether the fan reports the state of the built in battery
}
/*----------========== CAPABILITY HELPERS ==========----------*/
/*--== oscillation angle ==--*/
adjustOscillationAngleToRange(angle) {
if (this.supportsOscillationAngle() && this.oscillationAngleRange().length == 2) {
let low = this.oscillationAngleRange()[0];
let high = this.oscillationAngleRange()[1];
if (angle > high) angle = high;
if (angle < low) angle = low;
return angle;
}
return angle;
}
checkOscillationAngleWithinRange(angle) {
if (this.supportsOscillationAngle() && this.oscillationAngleRange().length == 2) {
let low = this.oscillationAngleRange()[0];
let high = this.oscillationAngleRange()[1];
if (angle >= low && angle <= high) {
return true;
}
}
return false;
}
/*--== oscillation levels ==--*/
checkOscillationLevelSupported(angle) {
if (this.supportsOscillationLevels()) {
if (this.oscillationLevels().includes(angle)) {
return true;
}
}
return false;
}
/*--== vertical oscillation angle ==--*/
adjustVerticalOscillationAngleToRange(angle) {
if (this.supportsVerticalOscillationAngle() && this.oscillationVerticalAngleRange().length == 2) {
let low = this.oscillationVerticalAngleRange()[0];
let high = this.oscillationVerticalAngleRange()[1];
if (angle > high) angle = high;
if (angle < low) angle = low;
return angle;
}
return angle;
}
checkVerticalOscillationAngleWithinRange(angle) {
if (this.supportsVerticalOscillationAngle() && this.oscillationVerticalAngleRange().length == 2) {
let low = this.oscillationVerticalAngleRange()[0];
let high = this.oscillationVerticalAngleRange()[1];
if (angle >= low && angle <= high) {
return true;
}
}
return false;
}
/*--== vertical oscillation levels ==--*/
checkVerticalOscillationLevelSupported(angle) {
if (this.supportsOscillationVerticalLevels()) {
if (this.oscillationVerticalLevels().includes(angle)) {
return true;
}
}
return false;
}
/*----------========== STATUS ==========----------*/
isPowerOn() {
return false;
}
getRotationSpeed() {
return 0;
}
getSpeed() {
return 0;
}
getFanLevel() {
return 0;
}
isChildLockActive() {
return false;
}
isSwingModeEnabled() {
return false;
}
isVerticalSwingModeEnabled() {
return false;
}
isNaturalModeEnabled() {
return false;
}
isSleepModeEnabled() {
return false;
}
isBuzzerEnabled() {
return false;
}
getBuzzerLevel() {
return this.isBuzzerEnabled() === true ? 1 : 0;
}
isLedEnabled() {
return false;
}
getLedLevel() {
return 0;
}
getLedBrightness() {
return 0;
}
getShutdownTimer() {
return 0;
}
isShutdownTimerEnabled() {
return this.getShutdownTimer() > 0;
}
getUseTime() {
return 0;
}
isIoniserEnabled() {
return false;
}
getTemperature() {
return 0;
}
getRelativeHumidity() {
return 0;
}
getBatteryLevel() {
return 0;
}
/*----------========== COMMANDS ==========----------*/
async setPowerOn(power) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setRotationSpeed(speed) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setFanLevel(level) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setChildLock(active) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setSwingModeEnabled(enabled) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setAngle(angle) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setVerticalSwingModeEnabled(enabled) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setVerticalAngle(angle) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setNaturalModeEnabled(enabled) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setSleepModeEnabled(enabled) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async moveLeft() {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async moveRight() {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async moveUp() {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async moveDown() {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setBuzzerEnabled(enabled) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setBuzzerLevel(level) {
// generic implementation for fans that does not support buzzer level
let enabled = this.getBuzzerLevel() === 0 ? false : true;
this.setBuzzerEnabled(enabled);
}
async setLedEnabled(enabled) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setLedLevel(level) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setLedBrightness(brightness) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setShutdownTimer(minutes) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
async setIoniserEnabled(enabled) {
this.logWarn(COMMAND_NOT_SUPPORTED_MSG);
}
/*----------========== HELPERS ==========----------*/
createErrorPromise(msg) {
return new Promise((resolve, reject) => {
reject(new Error(msg));
}).catch(err => {
this.logDebug(err);
});
}
addCapability(name, value) {
this.capabilities[name] = value;
}
getSafePropertyValue(value, safe) {
if (value === undefined) {
return safe;
}
return value;
}
/*----------========== 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);
}
}
module.exports = BaseFan;