UNPKG

cdif

Version:

Common device interconnect framework

310 lines (278 loc) 9.46 kB
var events = require('events'); var util = require('util'); var validator = require('./validator'); var logger = require('./logger'); var CdifError = require('./error').CdifError; var DeviceError = require('./error').DeviceError; var Domain = require('domain'); function Service(device, serviceID, spec) { this.device = device; this.serviceID = serviceID; this.serviceType = spec.serviceType; this.actions = {}; this.states = {}; this.updateSpec(spec); this.updateStateFromAction = this.updateStateFromAction.bind(this); } util.inherits(Service, events.EventEmitter); Service.prototype.addAction = function(actionName, action) { this.actions[actionName].invoke = action; }; Service.prototype.updateSpec = function(spec) { var actionList = spec.actionList; for (var i in actionList) { if (!this.actions[i]) { var action = actionList[i]; this.actions[i] = {}; this.actions[i].args = action.argumentList; // save for validation this.actions[i].invoke = null; // to be filled by device modules } } // TODO: to save memory usage we can reclaim spec object and dynamically reconstruct it on get-spec call var stateVariables = spec.serviceStateTable; for (var i in stateVariables) { if (!this.states[i]) { this.states[i] = {}; if (stateVariables[i].dataType === 'object') { this.states[i].variable = JSON.parse(JSON.stringify(stateVariables[i])); // save for schema deref var schemaRef = stateVariables[i].schema; if (schemaRef != null) { var self = this.states[i].variable; this.device.resolveSchemaFromPath(schemaRef, self, function(err, s, data) { if (!err) { s.schema = JSON.parse(JSON.stringify(data)); // reclaim doc object } // or else this is still a pointer }); } } else { this.states[i].variable = stateVariables[i]; } //TODO: need to deep clone this if we reclaim spec obj if (stateVariables[i].hasOwnProperty('defaultValue')) { this.states[i].value = stateVariables[i].defaultValue; } else { this.states[i].value = ''; } } } }; Service.prototype.getServiceStates = function(callback) { var output = {}; for (var i in this.states) { output[i] = this.states[i].value; } callback(null, output); }; Service.prototype.setServiceStates = function(values, callback) { var _this = this; var errorMessage = null; var updated = false; var sendEvent = false; var data = {}; if (typeof(values) !== 'object') { errorMessage = 'event data must be object'; } else { for (var i in values) { if (this.states[i] === undefined) { errorMessage = 'set invalid state for variable name: ' + i; break; } } } if (errorMessage === null) { for (var i in values) { validator.validate(i, this.states[i].variable, values[i], function(err) { if (!err) { if (typeof(values[i]) === 'object') { if (JSON.stringify(values[i]) !== JSON.stringify(_this.states[i].value)) { updated = true; _this.states[i].value = JSON.parse(JSON.stringify(values[i])); } } else { if (_this.states[i].value !== values[i]) { _this.states[i].value = values[i]; updated = true; } } } else { errorMessage = err.message; } }); if (errorMessage) break; // report only eventable data if (this.states[i].variable.sendEvents === true) { if (typeof(values[i]) === 'object') { data[i] = JSON.parse(JSON.stringify(values[i])); } else { data[i] = values[i]; } } } } if (errorMessage) { callback(new CdifError('setServiceStates error: ' + errorMessage)); } else { this.emit('serviceevent', updated, this.device.deviceID, this.serviceID, data); callback(null); } }; Service.prototype.updateStateFromAction = function(action, input, output, callback) { var updated = false; var data = {}; for (var i in input) { var argument = action.args[i]; if (argument == null) break; var stateVarName = argument.relatedStateVariable; if (stateVarName == null) break; if (argument.direction === 'in') { if (this.states[stateVarName].variable.sendEvents === true) { if (this.states[stateVarName].value !== input[i]) { data[stateVarName] = input[i]; updated = true; } } this.states[stateVarName].value = input[i]; } } for (var i in output) { var argument = action.args[i]; if (argument == null) break; var stateVarName = argument.relatedStateVariable; if (stateVarName == null) break; if (argument.direction === 'out') { if (this.states[stateVarName].variable.sendEvents === true) { if (this.states[stateVarName].value !== output[i]) { data[stateVarName] = output[i]; updated = true; } } this.states[stateVarName].value = output[i]; } } callback(updated, data); }; Service.prototype.validateActionCall = function(action, arguments, isInput, callback) { var argList = action.args; var failed = false; var error = null; if (arguments == null) { return callback(new CdifError('no valid arguments')); } // argument keys must match spec if (isInput) { for (var i in argList) { if (argList[i].direction === 'in') { if (arguments[i] === undefined) { failed = true; error = new CdifError('missing argument: ' + i); break; } } } } else { for (var i in argList) { if (argList[i].direction === 'out') { if (arguments[i] === undefined) { failed = true; error = new CdifError('missing output argument: ' + i); break; } } } } if (failed) { return callback(error); } // validate data for (var i in arguments) { var name = argList[i].relatedStateVariable; var stateVar = this.states[name].variable; if (isInput && argList[i].direction === 'out') { // only check out args on call return continue; } else { validator.validate(name, stateVar, arguments[i], function(err) { if (err) { error = new CdifError(err.message); failed = true; } }); } if (failed) break; } callback(error); }; Service.prototype.invokeAction = function(actionName, input, callback) { var _this = this; var action = this.actions[actionName]; if (action === undefined) { return callback(new CdifError('action not found: ' + actionName), null); } if (input === undefined) { return callback(new CdifError('cannot identify input arguments'), null); } if (action.invoke === null) { return callback(new DeviceError('action: ' + actionName + ' not implemented'), null); } this.validateActionCall(action, input, true, function(err) { if (err) { return callback(err, null); } var unsafeDomain = Domain.create(); unsafeDomain.on('error', function(err) { logger.error(err); return callback(err, null); }); unsafeDomain.run(function() { action.invoke(input, function(err, output) { if (err) { //TODO: validate the content of fault object according to its optional fault definition in device spec // API's formal fault definition, which can be in either simple or complex type, would make it more conformant to WSDL if (output && output.fault) { return callback(new DeviceError(err.message), output.fault); } return callback(new DeviceError(err.message), null); } _this.validateActionCall(action, output, false, function(error) { if (error) { return callback(error, null); } _this.updateStateFromAction(action, input, output, function(updated, data) { _this.emit('serviceevent', updated, _this.device.deviceID, _this.serviceID, data); }); callback(null, output); }); }); }); }); }; Service.prototype.setEventSubscription = function(subscribe, unsubscribe) { this.subscribe = subscribe; this.unsubscribe = unsubscribe; }; Service.prototype.subscribeEvent = function(onChange, callback) { if (this.subscribe) { this.subscribe(onChange, function(err) { if (err) { return callback(new DeviceError('event subscription failed: ' + err.message)); } callback(null); }); } else { // we can still send state change events upon action call callback(null); } }; Service.prototype.unsubscribeEvent = function(callback) { if (this.unsubscribe) { this.unsubscribe(function(err) { if (err) { return callback(new DeviceError('event unsubscription failed: ' + err.message)); } callback(null); }); } else { callback(null); } }; module.exports = Service;