homebridge-miot
Version:
Homebridge plugin for devices supporting the miot protocol
708 lines (530 loc) • 19.3 kB
JavaScript
const EventEmitter = require('events');
const MiotDevice = require('../protocol/MiotDevice.js');
const MiotService = require('../protocol/MiotService.js');
const MiotProperty = require('../protocol/MiotProperty.js');
const MiotAction = require('../protocol/MiotAction.js');
const MiotEvent = require('../protocol/MiotEvent.js');
const Constants = require('../constants/Constants.js');
const Events = require('../constants/Events.js');
const DevTypes = require('../constants/DevTypes.js');
const PropFormat = require('../constants/PropFormat.js');
const PropUnit = require('../constants/PropUnit.js');
const PropAccess = require('../constants/PropAccess.js');
const AccessoryFactory = require('../factories/AccessoryFactory.js');
// DEVICES: http://miot-spec.org/miot-spec-v2/instances?status=released
class AbstractDevice extends EventEmitter {
constructor(miotDevice, name, logger) {
super();
if (new.target === AbstractDevice) {
throw new Error("Cannot instantiate AbstractDevice directly!")
}
this.miotDevice = miotDevice;
this.name = name;
this.logger = logger;
if (!this.miotDevice) {
this.logger.error(`Missing miot device!`);
}
this.accessoryWrapper = null;
}
/*----------========== INIT ==========----------*/
async initDevice(propertyChunkSize) {
// init device services
this.logger.info(`Initializing device services`);
this.initDeviceServices();
this.logger.info(`Device services: ${JSON.stringify(this.getAllServiceTypes(), null, 2)}`);
// init device properties
this.logger.info(`Initializing device properties`);
this.initDeviceProperties();
this.logger.info(`Device properties: ${JSON.stringify(this.getAllPropertyNames(), null, 2)}`);
// init device actions
this.logger.info(`Initializing device actions`);
this.initDeviceActions();
if (this.hasActions()) {
this.logger.info(`Device actions: ${JSON.stringify(this.getAllActionNames(), null, 2)}`);
}
// init device events
this.logger.debug(`Initializing device events`);
this.initDeviceEvents();
if (this.hasEvents()) {
this.logger.debug(`Device events: ${JSON.stringify(this.getAllEventNames(), null, 2)}`);
}
this.getMiotDevice().on(Events.MIOT_DEVICE_CONNECTED, (miotDevice) => {
this.deviceConnected();
this.updateAccessoryStatus();
});
this.getMiotDevice().on(Events.MIOT_DEVICE_DISCONNECTED, (miotDevice) => {
this.deviceDisconnected();
this.updateAccessoryStatus();
});
this.getMiotDevice().on(Events.MIOT_DEVICE_INITIAL_PROPERTY_FETCH_DONE, (miotDevice) => {
this._initialPropertiesFetched();
this.updateAccessoryStatus();
});
this.getMiotDevice().on(Events.MIOT_DEVICE_ALL_PROPERTIES_UPDATED, (miotDevice) => {
this.allPropertiesUpdated();
this.updateAccessoryStatus();
});
this.getMiotDevice().on(Events.MIOT_DEVICE_PROPERTY_VALUE_SET, (property) => {
this.propertyValueSet(property);
this.updateAccessoryStatus();
});
this.getMiotDevice().on(Events.MIOT_DEVICE_ACTION_EXECUTED, (action) => {
this.actionExecuted(action);
// this.updateAccessoryStatus(); // no need to update accessory status since an action does not change properties directly
});
// set which properties to monitor initially
const propsToMonitor = this._getInitialPropsToMonitor();
this.getMiotDevice().setPropertiesToMonitor(propsToMonitor);
this._logPropsToMonitor('Initial');
// set whether the device requires a mi cloud connection
this.getMiotDevice().setRequiresMiCloud(this.requiresMiCloud());
// set whether the action params should be parsed for this device
this.getMiotDevice().setParseActionParams(this.parseActionParams());
// set the all property request chunk size, for devices with less processing power this needs to be low
const chunkSize = parseInt(propertyChunkSize) || this.allPropRequestChunkSize();
if (chunkSize) {
this.getMiotDevice().setAllPropRequestChunkSize(chunkSize);
}
}
initDeviceAccessory(uuid, config, api, cachedDeviceInfo) {
this.logger.info('Initializing accessory!');
this.accessoryWrapper = AccessoryFactory.createAccessory(this.getName(), this, uuid, config, api, this.logger);
if (this.accessoryWrapper) {
this.updateAccessoryInformationService(cachedDeviceInfo);
this.logger.info('Accessory successfully initialized!');
this._logPropsToMonitor('Final');
} else {
this.logger.error('Something went wrong. Could not initialize accessory!');
}
}
/*----------========== LIFECYCLE ==========----------*/
_initialPropertiesFetched() {
this.initialPropertyFetchDone();
}
/*----------========== DEVICE OVERRIDES ==========----------*/
initDeviceServices() {
// implemented by devices
}
initDeviceProperties() {
// implemented by devices
}
initDeviceActions() {
// implemented by devices
}
initDeviceEvents() {
// implemented by devices
}
deviceSpecificSetup() {
// implemented by devices
}
deviceConnected() {
// implemented by devices
}
deviceDisconnected() {
// implemented by devices
}
initialPropertyFetchDone() {
// implemented by devices
}
allPropertiesUpdated() {
// implemented by devices
}
propertyValueSet(prop) {
// implemented by devices
}
actionExecuted(action) {
// implemented by devices
}
/*----------========== DEVICE INFO ==========----------*/
getType() {
return DevTypes.UNKNOWN;
}
getModel() {
return this.getMiotDevice().getModel();
}
getDeviceName() {
return this.getSpecDescription() || 'Unknown device';
}
getMiotSpecUrl() {
return null;
}
getMainService() {
return this._getMainDeviceService();
}
/*----------========== GETTERS ==========----------*/
isConnected() {
return this.getMiotDevice().isConnected();
}
isLocallyConnected() {
return this.getMiotDevice().isLocallyConnected();
}
getName() {
return this.name;
}
getMiotDevice() {
return this.miotDevice;
}
getSpecDescription() {
return this.getMiotDevice().getDescription();
}
getMiotSpec() {
return this.getMiotDevice().getMiotSpec();
}
/*----------========== ACCESSORY STUFF ==========----------*/
getAccessoryWrapper() {
return this.accessoryWrapper;
}
getAccessories() {
return this.accessoryWrapper.getAccessories();
}
updateAccessoryStatus() {
if (this.accessoryWrapper) this.accessoryWrapper.updateAccessoryStatus();
}
updateAccessoryInformationService(cachedDeviceInfo) {
if (this.accessoryWrapper) {
let manufacturer = 'Xiaomi';
let model = this.getMiotDevice().getModel() || cachedDeviceInfo.model || 'Unknown';
let deviceId = this.getMiotDevice().getDeviceId() || cachedDeviceInfo.deviceId || 'Unknown';
let firmwareRev = this.getMiotDevice().getFirmwareRevision() || cachedDeviceInfo.firmwareRev || 'Unknown';
if (model.includes('yeelink.')) {
manufacturer = 'Yeelight Technology';
} else if (model.includes('.')) {
const tmpMan = model.split('.')[0];
if (tmpMan && tmpMan.length) {
manufacturer = tmpMan.charAt(0).toUpperCase() + tmpMan.substring(1);
}
}
this.accessoryWrapper.updateInformationService(this.getName(), manufacturer, model, deviceId, firmwareRev);
}
}
/*----------========== CONFIG ==========----------*/
requiresMiCloud() {
return false;
}
parseActionParams() {
return true;
}
propertiesToMonitor() {
return [];
}
allPropRequestChunkSize() {
return null; // when null then use default from miot device
}
devicePropertiesToMonitor() {
return null; //needs to be null to know if a device did override properties to monitor
}
commonProperties() {
return [];
}
/*----------========== VALUES ==========----------*/
/*----------========== PROPERTIES ==========----------*/
/*----------========== ACTIONS ==========----------*/
/*----------========== FEATURES ==========----------*/
/*----------========== GETTERS ==========----------*/
/*----------========== SETTERS ==========----------*/
/*----------========== CONVENIENCE ==========----------*/
/*----------========== VALUE CONVENIENCE ==========----------*/
/*----------========== HELPERS ==========----------*/
_getInitialPropsToMonitor() {
let propsToMonitor = [];
const deviceSpecificPropsToMonitor = this.devicePropertiesToMonitor();
// if device class specified an empty array then monitor all properties
if (deviceSpecificPropsToMonitor !== null && deviceSpecificPropsToMonitor.length === 0) {
return propsToMonitor;
}
//if the device specified an array of properties then get it
if (deviceSpecificPropsToMonitor !== null && deviceSpecificPropsToMonitor.length > 0) {
propsToMonitor = [...new Set([...propsToMonitor, ...deviceSpecificPropsToMonitor])];
}
// merge the device properties to monitor with the ones specified by the module
if (propsToMonitor !== null) {
const modulePropsToMonitor = this.propertiesToMonitor();
propsToMonitor = [...new Set([...propsToMonitor, ...modulePropsToMonitor])];
}
// combine the result from above with common properties
if (propsToMonitor !== null && propsToMonitor.length > 0) { //empty array = all properties, if not all specified then append the common properties
const commonPropsToMonitor = this.commonProperties();
propsToMonitor = [...new Set([...propsToMonitor, ...commonPropsToMonitor])];
}
//if something failed for any reason then monitor all
if (!propsToMonitor) {
propsToMonitor = [];
}
return propsToMonitor;
}
_registerForPropUpdate(miotProp) {
if (miotProp) {
miotProp.on(Events.PROP_VALUE_CHANGED, (prop) => {
this.logger.debug(`Property ${prop.getName()} value changed to ---> ${prop.getValue()}`);
});
}
}
_registerForActionExecuted(miotAction) {
if (miotAction) {
miotAction.on(Events.ACTION_EXECUTED, (action) => {
this.logger.debug(`Action ${action.getName()} executed! Updating properties!`);
try {
this.getMiotDevice().pollProperties(); // trigger a manual property update since some properties might have changed
} catch (err) {
this.logger.debug(`Property poll after action executed failed: Error: ${err}`);
}
});
}
}
/*----------========== PROPERTY HELPERS ==========----------*/
getPropertyValue(propObj) {
let prop = this.getProperty(propObj);
if (prop) {
return prop.getValue();
}
return undefined;
}
getSafePropertyValue(propObj) {
let prop = this.getProperty(propObj);
if (prop) {
return prop.getSafeValue();
}
return 0;
}
getPropertyValueRange(propObj) {
let prop = this.getProperty(propObj);
if (prop && prop.hasValueRange()) {
return prop.getValueRange();
}
return [];
}
getPropertyValueList(propObj) {
let prop = this.getProperty(propObj);
if (prop && prop.hasValueList()) {
return prop.getValueList();
}
return [];
}
async setPropertyValue(propObj, value) {
let prop = this.getProperty(propObj);
if (prop) {
let adjustedValue = prop.adjustValueToPropRange(value);
if (adjustedValue !== value) {
let propRange = prop.getValueRange();
this.logger.debug(`Trying to set ${prop.getName()} property with an out of range value: ${value} Range: ${JSON.stringify(propRange)}. Adjusting value to: ${adjustedValue}`);
}
if (prop.getValue() !== adjustedValue) {
return this.getMiotDevice().setProperty(prop, adjustedValue);
} else {
this.logger.debug(`Property ${prop.getName()} seems to have already the value: ${adjustedValue}. Set not needed! Skipping...`);
}
}
}
getPropertyUnit(propObj) {
let prop = this.getProperty(propObj);
if (prop) {
return prop.getUnit();
}
return PropUnit.NONE;
}
/*----------========== ACTION HELPERS ==========----------*/
async fireAction(actionObj, paramValues = []) {
let action = this.getAction(actionObj);
if (action) {
return this.getMiotDevice().executeAction(action, paramValues);
}
}
/*----------========== METHOD HELPERS ==========----------*/
async executeMethod(methodName, params = []) {
if (methodName && methodName.length > 0) {
return this.getMiotDevice().executeMethod(methodName, params);
}
}
/*----------========== EVENT HELPERS ==========----------*/
/*----------========== INTERNAL HELPERS ==========----------*/
_logPropsToMonitor(status) {
const propsToMonitor = this.getMiotDevice().getPropertiesToMonitor();
if (propsToMonitor.length > 0) {
this.logger.debug(`${status} properties to monitor: ${JSON.stringify(propsToMonitor, null, 2)}`);
} else {
this.logger.debug(`!-!-! ${status} - Monitoring all properties !-!-!`);
}
}
_getMainDeviceService() {
let mainService = null;
// first check if a service with the specified name exists
if (this.getSpecDescription() && this.getSpecDescription().toLowerCase) {
const specDescLower = this.getSpecDescription().toLowerCase();
mainService = this.getServiceByType(specDescLower);
}
//if none found then just get the first service which is usually the main one
if (!mainService && this.getAllServices() && this.getAllServices().length > 0) {
mainService = this.getAllServices()[0];
}
return mainService;
}
/*----------========== PROXY CALLS ==========----------*/
// services
createService(siid, type, description) {
return this.getMiotDevice().createService(siid, type, description);
}
createServiceBySpec(serviceSpec) {
return this.getMiotDevice().createServiceBySpec(serviceSpec);
}
createServiceByString(specString) {
return this.getMiotDevice().createServiceByString(specString);
}
getServiceById(siid) {
return this.getMiotDevice().getServiceById(siid);
}
getServiceByType(type) {
return this.getMiotDevice().getServiceByType(type);
}
getAllServicesByType(type) {
return this.getMiotDevice().getAllServicesByType(type);
}
hasServiceWithId(siid) {
return this.getMiotDevice().hasServiceWithId(siid);
}
hasServiceWithType(type) {
return this.getMiotDevice().hasServiceWithType(type);
}
getAllServices() {
return this.getMiotDevice().getAllServices();
}
getAllServiceTypes() {
return this.getMiotDevice().getAllServiceTypes();
}
// properties
addProperty(name, siid, piid, type, description, format, access, unit, valueRange, valueList) {
const newProp = this.getMiotDevice().addProperty(name, siid, piid, type, description, format, access, unit, valueRange, valueList);
this._registerForPropUpdate(newProp);
return newProp;
}
addPropertyBySpec(name, propSpec) {
const newProp = this.getMiotDevice().addPropertyBySpec(name, propSpec);
this._registerForPropUpdate(newProp);
return newProp;
}
addPropertyByString(name, specString) {
const newProp = this.getMiotDevice().addPropertyByString(name, specString);
this._registerForPropUpdate(newProp);
return newProp;
}
hasProperty(miotProp) {
return this.getMiotDevice().hasProperty(propName);
}
hasPropertyByName(propName) {
return this.getMiotDevice().hasPropertyByName(propName);
}
hasPropertyById(propId) {
return this.getMiotDevice().hasPropertyById(propId);
}
getProperty(miotProp) {
return this.getMiotDevice().getProperty(miotProp);
}
getPropertyByName(propName) {
return this.getMiotDevice().getPropertyByName(propName);
}
getPropertyById(propId) {
return this.getMiotDevice().getPropertyById(propId);
}
findPropertyByType(propType) {
return this.getMiotDevice().findPropertyByType(propType);
}
getAllProperties() {
return this.getMiotDevice().getAllProperties();
}
getAllPropertyNames() {
return this.getMiotDevice().getAllPropertyNames();
}
getBeautifiedAllPropNameValues() {
return this.getMiotDevice().getBeautifiedAllPropNameValues();
}
// actions
addAction(name, siid, aiid, type, description, inDef) {
const newAction = this.getMiotDevice().addAction(name, siid, aiid, type, description, inDef);
this._registerForActionExecuted(newAction);
return newAction;
}
addActionBySpec(name, actionSpec) {
const newAction = this.getMiotDevice().addActionBySpec(name, actionSpec);
this._registerForActionExecuted(newAction);
return newAction;
}
addActionByString(name, specString) {
const newAction = this.getMiotDevice().addActionByString(name, specString);
this._registerForActionExecuted(newAction);
return newAction;
}
hasAction(miotAction) {
return this.getMiotDevice().hasAction(miotAction);
}
hasActionByName(actionName) {
return this.getMiotDevice().hasActionByName(actionName);
}
hasActionById(actionId) {
return this.getMiotDevice().hasActionById(actionId);
}
hasActions() {
return this.getMiotDevice().hasActions();
}
getAction(miotAction) {
return this.getMiotDevice().getAction(miotAction);
}
getActionByName(actionName) {
return this.getMiotDevice().getActionByName(actionName);
}
getActionyById(actionId) {
return this.getMiotDevice().getActionyById(actionId);
}
findActionByType(actionType) {
return this.getMiotDevice().findActionByType(actionType);
}
getAllActions() {
return this.getMiotDevice().getAllActions();
}
getAllActionNames() {
return this.getMiotDevice().getAllActionNames();
}
// events
addEvent(name, siid, eiid, type, description, argumentsDef) {
return this.getMiotDevice().addEvent(name, siid, eiid, type, description, argumentsDef);
}
addEventBySpec(name, eventSpec) {
return this.getMiotDevice().addEventBySpec(name, eventSpec);
}
addEventByString(name, specString) {
return this.getMiotDevice().addEventByString(name, specString);
}
hasEvent(miotEvent) {
return this.getMiotDevice().hasEvent(miotEvent);
}
hasEventByName(eventName) {
return this.getMiotDevice().hasEventByName(eventName);
}
hasEventById(eventId) {
return this.getMiotDevice().hasEventById(eventId);
}
hasEvents() {
return this.getMiotDevice().hasEvents();
}
getEvent(miotEvent) {
return this.getMiotDevice().getEvent(miotEvent);
}
getEventByName(eventName) {
return this.getMiotDevice().getEventByName(eventName);
}
getEventById(eventId) {
return this.getMiotDevice().getEventById(eventId);
}
findEventByType(evenType) {
return this.getMiotDevice().findEventByType(evenType);
}
getAllEvents() {
return this.getMiotDevice().getAllEvents();
}
getAllEventNames() {
return this.getMiotDevice().getAllEventNames();
}
//config
addPropertyToMonitor(miotProp) {
this.getMiotDevice().addPropertyToMonitor(miotProp);
}
}
module.exports = AbstractDevice;