cdif
Version:
Common device interconnect framework
276 lines (238 loc) • 9.04 kB
JavaScript
var events = require('events');
var util = require('util');
var url = require('url');
var parser = require('json-schema-ref-parser');
var Service = require('./service');
var ConnMan = require('./connect');
var validator = require('./validator');
var logger = require('./logger');
var CdifError = require('./error').CdifError;
var DeviceError = require('./error').DeviceError;
//warn: try not add event listeners in this class
function CdifDevice(spec) {
this.deviceID = '';
this.user = '';
this.secret = '';
this.connectionState = 'disconnected'; // enum of disconnected, connected, & redirecting
this.connMan = new ConnMan(this);
this.schemaDoc = this.getDeviceRootSchema();
this.spec = spec;
this.initServices();
this.getDeviceSpec = this.getDeviceSpec.bind(this);
this.connect = this.connect.bind(this);
this.disconnect = this.disconnect.bind(this);
this.getHWAddress = this.getHWAddress.bind(this);
this.deviceControl = this.deviceControl.bind(this);
this.subscribeDeviceEvent = this.subscribeDeviceEvent.bind(this);
this.unSubscribeDeviceEvent = this.unSubscribeDeviceEvent.bind(this);
}
util.inherits(CdifDevice, events.EventEmitter);
CdifDevice.prototype.setAction = function(serviceID, actionName, action) {
if (action === null || typeof(action) !== 'function') {
return logger.error('set incorrect action type for: ' + serviceID + ' ' + actionName);
}
var service = this.services[serviceID];
if (service) {
service.actions[actionName].invoke = action.bind(this);
} else {
logger.error('cannot set action: ' + serviceID + ' ' + actionName);
}
};
CdifDevice.prototype.initServices = function() {
if (typeof(this.spec) !== 'object' || this.spec.device == null || this.spec.device.serviceList == null) {
return logger.error('no valid device spec: ' + this.constructor.name);
}
var serviceList = this.spec.device.serviceList;
if (!this.services) {
this.services = new Object();
}
for (var i in serviceList) {
var service_spec = serviceList[i];
if (!this.services[i]) {
this.services[i] = new Service(this, i, service_spec);
} else {
this.services[i].updateSpec(service_spec);
}
}
};
CdifDevice.prototype.getDeviceSpec = function(callback) {
if (this.spec === null) {
return callback(new CdifError('cannot get device spec'), null);
}
callback(null, this.spec);
};
CdifDevice.prototype.getServiceStates = function(serviceID, callback) {
if (callback && typeof(callback) !== 'function') {
return logger.error('getServiceStates failed, not valid callback');
}
var service = this.services[serviceID];
if (service == null) {
if (callback && typeof(callback) === 'function') {
return callback(new CdifError('service not found: ' + serviceID), null);
} else {
return logger.error('getServiceStates failed, service not found: ' + serviceID);
}
}
service.getServiceStates(callback);
};
CdifDevice.prototype.setServiceStates = function(serviceID, values, callback) {
if (callback == null || typeof(callback) !== 'function') {
return logger.error('setServiceStates failed, no valid callback');
}
var service = this.services[serviceID];
if (service == null) {
return callback(new CdifError('service not found: ' + serviceID));
}
service.setServiceStates(values, callback);
};
// now support only one user / pass pair
// TODO: check if no other case than oauth redirect flow needs to temporarily unset connected flag
CdifDevice.prototype.connect = function(user, pass, callback) {
if (this.connectionState === 'redirecting') {
return callback(new CdifError('device in action'), null, null);
}
if (this.connectionState === 'connected') {
return this.connMan.verifyConnect(user, pass, callback);
}
return this.connMan.processConnect(user, pass, callback);
};
CdifDevice.prototype.disconnect = function(callback) {
return this.connMan.processDisconnect(callback);
};
CdifDevice.prototype.getHWAddress = function(callback) {
if (this._getHWAddress && typeof(this._getHWAddress) === 'function') {
this._getHWAddress(function(error, data) {
if (error) {
return callback(new DeviceError('get hardware address fail: ' + error.message), null);
}
callback(null, data);
});
} else {
callback(null, null);
}
};
CdifDevice.prototype.deviceControl = function(serviceID, actionName, args, callback) {
var service = this.services[serviceID];
if (service == null) {
return callback(new CdifError('service not found: ' + serviceID), null);
}
service.invokeAction(actionName, args, callback);
};
CdifDevice.prototype.updateDeviceSpec = function(newSpec) {
validator.validateDeviceSpec(newSpec, function(error) {
if (error) {
return logger.error(error.message + ', device spec: ' + JSON.stringify(newSpec));
}
this.spec = newSpec;
this.initServices();
}.bind(this));
};
CdifDevice.prototype.setEventSubscription = function(serviceID, subscribe, unsubscribe) {
if (typeof(subscribe) !== 'function' || typeof(unsubscribe) !== 'function') {
return logger.error('type error for event subscribers');
}
var service = this.services[serviceID];
if (service) {
service.setEventSubscription(subscribe.bind(this), unsubscribe.bind(this));
} else {
logger.error('cannot set subscriber for: ' + serviceID);
}
};
CdifDevice.prototype.subscribeDeviceEvent = function(subscriber, serviceID, callback) {
var service = this.services[serviceID];
if (service == null) {
return callback(new CdifError('cannot subscribe to unknown serviceID: ' + serviceID));
}
service.subscribeEvent(subscriber.onChange, function(err) {
if (!err) {
service.addListener('serviceevent', subscriber.publish);
}
callback(err);
});
};
CdifDevice.prototype.unSubscribeDeviceEvent = function(subscriber, serviceID, callback) {
var service = this.services[serviceID];
if (service == null) {
return callback(new CdifError('cannot unsubscribe from unknown serviceID: ' + serviceID));
}
service.removeListener('serviceevent', subscriber.publish);
if (service.listeners('serviceevent').length === 0) {
service.unsubscribeEvent(callback);
} else {
callback(null);
}
};
// get device root url string
CdifDevice.prototype.getDeviceRootUrl = function(callback) {
if (this.spec.device.devicePresentation !== true || typeof(this._getDeviceRootUrl) !== 'function') {
return callback(new DeviceError('this device do not support presentation'), null);
}
this._getDeviceRootUrl(function(err, data) {
if (err) {
return callback(new DeviceError('get device root url failed: ' + err.message), null);
}
try {
url.parse(data);
} catch(e) {
return callback(new DeviceError('device root url parse failed: ' + e.message), null);
}
callback(null, data);
});
};
// get device root schema document object, must be sync
CdifDevice.prototype.getDeviceRootSchema = function() {
if (typeof(this._getDeviceRootSchema) !== 'function') return null;
try {
return this._getDeviceRootSchema();
} catch (e) {
logger.error(e);
return null;
}
};
// resolve JSON pointer based schema ref and return the schema object associated with it
// For now we only support single doc schema to avoid security risks when resolving external refs
CdifDevice.prototype.resolveSchemaFromPath = function(path, self, callback) {
var schemaDoc = this.schemaDoc;
if (schemaDoc == null || typeof(schemaDoc) !== 'object') {
return callback(new DeviceError('device has no schema doc'), self, null);
}
if (path === '/') {
return callback(null, self, schemaDoc);
}
var doc = null;
try {
doc = JSON.parse(JSON.stringify(schemaDoc));
} catch(e) {
return callback(new DeviceError('invalid schema doc: ' + e.message), self, null);
}
var ref;
// for now we dont support fragment based pointer
// because it won't be able to be resolved
if (/^\/./.test(path) === false) {
return callback(new CdifError('path is not a valid pointer'), self, null);
}
ref = '#' + path;
doc.__ = {
"$ref": ref
};
parser.dereference(doc, {$refs: {external: false}}, function(err, out) {
if (err) {
return callback(new CdifError('pointer dereference fail: ' + err.message), self, null);
}
callback(null, self, out.__);
});
};
CdifDevice.prototype.setOAuthAccessToken = function(params, callback) {
if (typeof(this._setOAuthAccessToken) === 'function') {
this._setOAuthAccessToken(params, function(err) {
if (err) {
return callback(new CdifError(err.message));
}
this.connectionState = 'connected';
callback(null);
}.bind(this));
} else {
callback(new CdifError('cannot set device oauth access token: no available device interface'));
}
};
module.exports = CdifDevice;