homebridge-miot
Version:
Homebridge plugin for devices supporting the miot protocol
441 lines (363 loc) • 15.1 kB
JavaScript
let Service, Characteristic, Accessory, HapStatusError, HAPStatus;
const AbstractPropertyWrapper = require('./AbstractPropertyWrapper.js');
const Constants = require('../constants/Constants.js');
const PropFormat = require('../constants/PropFormat.js');
const PropUnit = require('../constants/PropUnit.js');
const PropAccess = require('../constants/PropAccess.js');
class PropertyWrapper extends AbstractPropertyWrapper {
constructor(wrapperName, prop, device, accessory, api, logger) {
Service = api.hap.Service;
Characteristic = api.hap.Characteristic;
Accessory = api.platformAccessory;
HapStatusError = api.hap.HapStatusError;
HAPStatus = api.hap.HAPStatus;
super(wrapperName, prop, device, accessory, api, logger);
}
/*----------========== PROPERTY WRAPPER INFO ==========----------*/
getWrapperType() {
return 'Property';
}
/*----------========== SETUP WRAPPER ==========----------*/
prepareWrapper() {
if (!this.isWritable()) {
this.getLogger().warn('<-W-> This property is not writable! Cannot create property wrapper!');
return false;
}
if (this.hasFixedValue()) {
this.handleFixedValue();
this.getLogger().deepDebug('<-W-> Property type: Fixed Value! Creating value switch!');
return true;
}
if (this.isBoolean()) {
this.handleBoolean();
this.getLogger().deepDebug('<-W-> Property type: Boolean! Creating on/off switch!');
return true;
}
if (this.hasValueRange()) {
this.lastValueRangeValue = null;
this.handleValueRange();
this.getLogger().deepDebug(`<-W-> Property type: Value Range! Creating 0%-100% lightbulb! Is value range percentage: ${this._isValueRangePercentage() ? 'YES' : 'NO - emulating!'}`);
return true;
}
if (this.hasValueList()) {
this.handleValueList();
this.getLogger().deepDebug('<-W-> Property type: Value List! Creating list item switches!');
return true;
}
if (this.isWriteOnly()) {
this.getLogger().warn('<-W-> This is a write only property and write only properties require a value!');
return false;
}
return false;
}
//fixedValue
handleFixedValue() {
let propName = this.getProp().getName();
let serviceName = this.getWrapperName() || propName;
let serviceId = this.generateServiceId();
if (this.isWriteOnly()) {
this.fixedValueStatelessService = this.createStatlessSwitch(serviceName, serviceId, this.setStatelessFixedValueSwitchOn);
this.addAccessoryService(this.fixedValueStatelessService);
} else {
this.fixedValueStatefulService = this.createStatefulSwitch(serviceName, serviceId, this.isStatefulFixedValueSwitchOn, this.setStatefulFixedValueSwitchOn);
this.addAccessoryService(this.fixedValueStatefulService);
}
}
// boolean
handleBoolean() {
// simple on/off switch
let propName = this.getProp().getName();
let serviceName = this.getWrapperName() || propName;
let serviceId = this.generateServiceId();
this.propBooleanService = new Service.Switch(this.sanitizeName(serviceName), serviceId);
this.setServiceConfiguredName(this.propBooleanService, serviceName);
this.propBooleanService
.getCharacteristic(Characteristic.On)
.onGet(this.isBooleanSwitchOn.bind(this))
.onSet(this.setBooleanSwitchOn.bind(this));
this.addAccessoryService(this.propBooleanService);
}
// value range
handleValueRange() {
// create a lightbulb where the value is a percentage representation
let propName = this.getProp().getName();
let serviceName = this.getWrapperName() || propName;
let serviceId = this.generateServiceId();
let isFan = this.getConfiguration() && this.getConfiguration().type === 'fan';
if (isFan) {
// fan
this.propFanService = new Service.Fanv2(this.sanitizeName(serviceName), serviceId);
this.setServiceConfiguredName(this.propFanService, serviceName);
this.propFanService
.getCharacteristic(Characteristic.Active)
.onGet(this.isPropFanSwitchOn.bind(this))
.onSet(this.setPropFanSwitchOn.bind(this));
this.propFanService.addCharacteristic(Characteristic.RotationSpeed)
.onGet(this.getPropRotationSpeed.bind(this))
.onSet(this.setPropRotationSpeed.bind(this));
this.addAccessoryService(this.propFanService);
} else {
// light bulb
this.propBrightnessService = new Service.Lightbulb(this.sanitizeName(serviceName), serviceId);
this.setServiceConfiguredName(this.propBrightnessService, serviceName);
this.propBrightnessService
.getCharacteristic(Characteristic.On)
.onGet(this.isPropBrightnessSwitchOn.bind(this))
.onSet(this.setPropBrightnessSwitchOn.bind(this));
this.propBrightnessService
.addCharacteristic(new Characteristic.Brightness())
.onGet(this.getPropBrightness.bind(this))
.onSet(this.setPropBrightness.bind(this));
this.addAccessoryService(this.propBrightnessService);
}
}
// value list
handleValueList() {
// create switches for every item where only one item can be active at a time
this.propValueListServices = new Array();
this.valueList().forEach((item, i) => {
let itemVal = item.value;
let itemDesc = item.description;
let propName = this.getProp().getName();
let name = this.getWrapperName() || propName;
let switchName = name + ' - ' + itemDesc;
let switchId = this.generateServiceId(itemVal);
let tmpSwitch = null;
if (this.isWriteOnly()) {
tmpSwitch = this.createStatlessSwitch(switchName, switchId, (value) => {
this.setStatlessValueListSwitchOn(value, itemVal);
});
} else {
tmpSwitch = this.createStatefulSwitch(switchName, switchId,
() => {
return this.isValueListSwitchOn(itemVal);
},
(value) => {
this.setValueListSwitchOn(value, itemVal);
});
}
this.addAccessoryService(tmpSwitch);
this.propValueListServices.push(tmpSwitch);
});
}
/*----------========== STATE SETTERS/GETTERS ==========----------*/
//fixed value - stateful
isStatefulFixedValueSwitchOn() {
if (this.isMiotDeviceConnected()) {
return this.getProp().getValue() === this.getFixedValue();
}
return false;
}
setStatefulFixedValueSwitchOn(state) {
if (this.isMiotDeviceConnected()) {
if (state) {
this.enableLinkedPropIfNecessary();
this.setPropValue(this.getFixedValue());
this.fixedValueStatefulService.getCharacteristic(Characteristic.On).updateValue(true);
} else {
// if user tries to turn off an fixed value switch, then re enable it
setTimeout(() => {
this.fixedValueStatefulService.getCharacteristic(Characteristic.On).updateValue(true);
}, Constants.BUTTON_RESET_TIMEOUT);
}
} else {
throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
//fixed value - stateless
setStatelessFixedValueSwitchOn(state) {
if (this.isMiotDeviceConnected()) {
this.enableLinkedPropIfNecessary();
this.setPropValue(this.getFixedValue());
} else {
throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
// boolean
isBooleanSwitchOn() {
if (this.isMiotDeviceConnected() && this.checkLinkedPropStatus()) {
return this.getProp().getValue();
}
return false;
}
setBooleanSwitchOn(value) {
if (this.isMiotDeviceConnected()) {
this.setPropValue(value);
} else {
throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
// value range
// -- lightbulb
isPropBrightnessSwitchOn() {
if (this.isMiotDeviceConnected() && this.checkLinkedPropStatus()) {
return this.isPropValueRangeEnabled();
}
return false;
}
setPropBrightnessSwitchOn(value) {
if (this.isMiotDeviceConnected()) {
if (!value || this.isPropValueRangeEnabled() === false) {
//TODO: it gets called twice once the previous value and once 100% find a fix for that?
// check screen brightness at zhimi.airpurifier.mb4 device which has the issue
this.setPropValueRangeEnabled(value);
}
} else {
throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
getPropBrightness() {
if (this.isMiotDeviceConnected()) {
return this.getPropValueRangePercentage();
}
return 0;
}
setPropBrightness(value) {
if (this.isMiotDeviceConnected()) {
// use debounce to limit the number of calls when the user slides the slider
if (this.ledBrightnessTimeout) clearTimeout(this.ledBrightnessTimeout);
this.ledBrightnessTimeout = setTimeout(() => this.setPropValueRangePercentage(value), Constants.SLIDER_DEBOUNCE);
} else {
throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
// -- fan
isPropFanSwitchOn() {
if (this.isMiotDeviceConnected() && this.checkLinkedPropStatus()) {
return Characteristic.Active.ACTIVE;
}
return Characteristic.Active.INACTIVE;
}
setPropFanSwitchOn(state) {
if (this.isMiotDeviceConnected()) {
let value = state === Characteristic.Active.ACTIVE;
this.setPropValueRangeEnabled(value);
} else {
throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
getPropRotationSpeed() {
if (this.isMiotDeviceConnected()) {
return this.getPropValueRangePercentage();
}
return 0;
}
setPropRotationSpeed(value) {
if (this.isMiotDeviceConnected()) {
// use debounce to limit the number of calls when the user slides the slider
if (this.fanRotationSpeedTimeout) clearTimeout(this.fanRotationSpeedTimeout);
this.fanRotationSpeedTimeout = setTimeout(() => this.setPropValueRangePercentage(value), Constants.SLIDER_DEBOUNCE);
} else {
throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
// value list - stateful
isValueListSwitchOn(itemVal) {
if (this.isMiotDeviceConnected() && this.checkLinkedPropStatus()) {
return this.getProp().getValue() === itemVal;
}
return false;
}
setValueListSwitchOn(state, itemVal) {
if (this.isMiotDeviceConnected()) {
if (state) {
this.enableLinkedPropIfNecessary();
this.setPropValue(itemVal);
this.updateStatefulValueListSwitches(itemVal);
} else {
// if user tries to turn off active switch, then reset the state of all switches
setTimeout(() => {
this.updateStatefulValueListSwitches();
}, Constants.BUTTON_RESET_TIMEOUT);
}
} else {
throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
// value list - stateless
setStatlessValueListSwitchOn(state, itemVal) {
if (this.isMiotDeviceConnected()) {
this.enableLinkedPropIfNecessary();
this.setPropValue(itemVal);
} else {
throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
/*----------========== SERVICE PROTOCOL ==========----------*/
updateWrapperStatus() {
super.updateWrapperStatus(); // call super implementation
if (this.fixedValueStatefulService) this.fixedValueStatefulService.getCharacteristic(Characteristic.On).updateValue(this.isStatefulFixedValueSwitchOn());
if (this.propBooleanService) this.propBooleanService.getCharacteristic(Characteristic.On).updateValue(this.isBooleanSwitchOn());
if (this.propBrightnessService) {
this.propBrightnessService.getCharacteristic(Characteristic.On).updateValue(this.isPropBrightnessSwitchOn());
this.propBrightnessService.getCharacteristic(Characteristic.Brightness).updateValue(this.getPropBrightness());
}
if (this.propFanService) {
this.propFanService.getCharacteristic(Characteristic.Active).updateValue(this.isPropFanSwitchOn());
this.propFanService.getCharacteristic(Characteristic.RotationSpeed).updateValue(this.getPropRotationSpeed());
}
this.updateStatefulValueListSwitches();
}
/*----------========== STATE HELPERS ==========----------*/
updateStatefulValueListSwitches(activeVal) {
if (!this.isWriteOnly() && this.propValueListServices) {
activeVal = activeVal !== undefined ? activeVal : this.getProp().getValue(); // if activeVal specified from outside then use that, else get current prop value
this.propValueListServices.forEach((tmpValSwitch, i) => {
let item = this.valueList()[i];
let itemVal = item.value;
let isSwitchOn = (activeVal === itemVal) && this.checkLinkedPropStatus();
tmpValSwitch.getCharacteristic(Characteristic.On).updateValue(isSwitchOn);
});
}
}
/*----------========== GETTERS ==========----------*/
/*----------========== CONVENIENCE ==========----------*/
isPropValueRangeEnabled() {
return this.getProp().getValue() > 0;
}
setPropValueRangeEnabled(enabled) {
let valueToSet = enabled;
// save the previous value
if (!enabled && !this.lastValueRangeValue) {
this.lastValueRangeValue = this.getProp().getValue();
}
// set the new value
if (enabled && this.lastValueRangeValue !== null && this.lastValueRangeValue !== undefined) {
valueToSet = this.lastValueRangeValue;
this.lastValueRangeValue == null;
} else {
let minLevel = this.valueRange()[0];
let maxLevel = this.valueRange()[1];
valueToSet = enabled ? maxLevel : minLevel;
}
this.setPropValue(valueToSet);
}
getPropValueRangePercentage() {
if (this._isValueRangePercentage()) {
return this.getProp().getValue();
} else {
const calculatedPercentageValue = this.getDevice().convertPropValueToPercentage(this.getProp());
return Math.max(calculatedPercentageValue, 1); // minimum 1, if we return 0 for some reason the lightbulb jumps to 100%, same with fan?
}
}
setPropValueRangePercentage(percentage) {
if (this._isValueRangePercentage()) {
this.setPropValue(percentage);
} else {
let valPercentage = this.getDevice().convertPercentageToPropValue(percentage, this.getProp());
this.setPropValue(valPercentage);
}
}
/*----------========== HELPERS ==========----------*/
_isValueRangePercentage() {
if (this.getUnit() === PropUnit.PERCENTAGE) {
return this._checkMaxRangeAndStepForPercentage(); // sometimes the prop unit is wrong so make sure that this is actaully a percentage by checking the max range and step.
}
return this._checkMaxRangeAndStepForPercentage(); // unit is empty or not percentage so check just based on the value range.
}
_checkMaxRangeAndStepForPercentage() {
return (this.valueRange()[1] === 100 && this.valueRange()[2] === 1); // make sure value range has max range of 100 and 1 step, this should be a good indication for percentage value range.
}
/*----------========== LINKED PROP HELPERS ==========----------*/
}
module.exports = PropertyWrapper;