'use strict';
const _isNil = require('lodash/isNil');
const _lowerFirst = require('lodash/lowerFirst');
const _ = require('lodash');
const events = require('events');
const path = require('path');
/**
* @class
* @description The base class for all devices
*/
class Device {
/**
* @constructor
* @param {Adapter} adapter The adapter object associated with this sequencer
*/
constructor(adapter) {
this._adapter = adapter;
this._emitter = new events.EventEmitter();
this._isTemporaryDevice = false;
this._modelVarsCache = new Map();
}
/**
* @description Performs any wrapping up of device class remnants. Might
* no longer have a corresponding sender node on the K4Model.
*/
cleanup() {
this._emitter.removeAllListeners();
}
/**
* @description The K4Model node of the device
* @type {Object}
*/
set model(node) {
this.devicePath = node.path();
}
get model() {
return this._adapter.k4.model.child(this.devicePath);
}
/**
* @description Usually this will be the path to the device on the K4Model.
* If the device is temporary, this path will not reflect a K4Model node
* path and cannot be used to retrieve a K4Model node.
* @type {String}
*/
set devicePath(devicePathToSet) {
this._path = devicePathToSet;
}
get devicePath() {
return this._path;
}
/**
* @description Set to True if this device instance is intended to be
* temporary. Defaults to False, because standard practice is to only have
* device instances on the bridge that also have nodes defined on the
* K4Model
* @type {Boolean}
*/
set isTemporaryDevice(temporaryDeviceStatus) {
this._isTemporaryDevice = temporaryDeviceStatus;
}
get isTemporaryDevice() {
return this._isTemporaryDevice;
}
/**
* @description The id of the device. Retrieved from the model property
* using the id key in the {@link Bridge Bridge} opts
* @type {String}
*/
set id(id) {
this._id = id;
}
get id() {
return this._id;
}
/**
* @description The adapter mapping for this device
* @type {Object}
*/
set mapping(mapping) {
this._mapping = mapping;
}
/**
* @property {Object} constants The available constants
* @static
*/
static get constants() {
return {
TEMPORARY_DEVICE_PATH_GENERIC_PREFIX: 'temporaryDevice_'
};
}
/**
* @description Returns the command class for a given name
* @param {String} modelCmdName The command name on the K4Model
* @return {Command|null}
* @private
*/
getCommand(modelCmdName) {
if (!_isNil(this._mapping) &&
!_isNil(this._mapping.commands) &&
!_isNil(this._mapping.commands[modelCmdName])) {
const Command = this._mapping.commands[modelCmdName];
return new Command();
}
return null;
}
/**
* @description Processes a response to the appropriate variable/event
* @param {Response} response The response
* @private
*/
received(response) {
// loop through and trigger variables and events
this._adapter.k4.logging.debug(`Device ${this.devicePath} received response ${response.name}`);
this._emitter.emit(`received--${response.name}`, response);
// NOTE: should never actually have a device on the bridge that does not
// also have a mapping. However, if somehow that is the case, should
// gracefully skip.
if (_isNil(this._mapping)) {
this._adapter.k4.logging.system(`Device ${this.devicePath} does not have mapping object. Skipping check for matching variables and events`);
return;
}
// NOTE 2: If we are using a temporary device, we do not want to attempt
// to access the K4Model.
if (this.isTemporaryDevice === true) {
this._adapter.k4.logging.system(`Device ${this.devicePath} is a temporary device. Skipping check for matching K4Model variables and events`);
return;
}
for (let i in this._mapping.variables) {
let variable = this._mapping.variables[i];
// variables in mapping can be a list or single response name
if (Array.isArray(variable)) {
for (let j in variable) {
let v = variable[j];
this.checkVariable(i, v, response);
}
} else {
this.checkVariable(i, variable, response);
}
}
for (let i in this._mapping.events) {
let event = this._mapping.events[i];
// events in mapping can be a list or single response name
if (Array.isArray(event)) {
for (let j in event) {
let e = event[j];
this.checkEvent(i, e, response);
}
} else {
this.checkEvent(i, event, response);
}
}
}
/**
* @description Sets the cached state for a given K4Model variable
* @param {String} modelVarPath Path to the K4Model variable node
* @param {any} valueToSet The value to cache for the K4Model variable node
*/
updateModelVarCache(modelVarPath, valueToSet) {
if (!modelVarPath ||
_.isUndefined(valueToSet)) {
return;
}
this._modelVarsCache.set(modelVarPath, valueToSet);
}
/**
* @description Gets the latest cached state for a given K4Model variable
* @param {String} modelVarPath Path to the K4Model variable node
* @returns {any} The value cached for the K4Model variable node
*/
retrieveCachedModelVar(modelVarPath) {
if (!this._modelVarsCache.has(modelVarPath)) {
return null;
}
return this._modelVarsCache.get(modelVarPath);
}
/**
* @description Loads all the cached variable values into the live K4Model
*/
restoreAllModelVarsFromCache() {
if (this.isTemporaryDevice ||
!this._modelVarsCache) {
return;
}
this._modelVarsCache.forEach((cachedVal, modelVarPath) => {
const variablesParentPath = path.join(this.devicePath, 'variables');
const varName = _.trim(_.replace(modelVarPath, variablesParentPath, ''), '/');
const k4ModelVariableNode = this.model.child(`variables/${varName}`);
k4ModelVariableNode.set(cachedVal);
});
}
/**
* @description Updates appropriate variable (if any) from response
* @param {String} key K4Model variable associated with a device
* @param {String} variable The mapped response name for the K4model variable
* @param {Response} response The Response object to compare and check
* @private
*/
checkVariable(key, variable, response) {
// if the variable from mappings matches the response name, update the model variable
if (_lowerFirst(variable) === _lowerFirst(response.name)) {
const k4ModelVariableNode = this.model.child(`variables/${key}`);
if (_isNil(k4ModelVariableNode)) {
this._adapter.k4.logging.system(`Device ${this.devicePath} mapping specified non-existent K4Model variable named: ${key}. Will not attempt to set that variable.`);
return;
}
const responseVal = response.value;
k4ModelVariableNode.set(responseVal);
this.updateModelVarCache(k4ModelVariableNode.path(), responseVal);
}
}
/**
* @description Fires appropriate event (if any) from response
* @param {String} key K4Model event associated with a device
* @param {String} event The mapped response name for the K4model event
* @param {Response} response The Response object to compare and check
* @private
*/
checkEvent(key, event, response) {
let responseVal = null;
if (response &&
!_isNil(response.value)) {
responseVal = response.value;
if (!Array.isArray(responseVal)) {
responseVal = [responseVal];
}
}
// if the event from mappings matches the response name, fire the model event
if (event.indexOf('.') > -1) {
// allow for mapping matches like name.true where the name must match as well as the value
let parts = event.split('.');
if (_lowerFirst(parts[0]) === _lowerFirst(response.name) && parts[1] === response.value.toString()) {
const k4ModelEventNode = this.model.child(`events/${key}`);
if (_isNil(k4ModelEventNode)) {
this._adapter.k4.logging.system(`Device ${this.devicePath} mapping specified non-existent K4Model event named: ${key}. Will not attempt to fire that event.`);
return;
}
k4ModelEventNode.fire(responseVal);
}
} else if (_lowerFirst(event) === _lowerFirst(response.name)) {
const k4ModelEventNode = this.model.child(`events/${key}`);
if (_isNil(k4ModelEventNode)) {
this._adapter.k4.logging.system(`Device ${this.devicePath} mapping specified non-existent K4Model event named: ${key}. Will not attempt to fire that event.`);
return;
}
k4ModelEventNode.fire(responseVal);
}
}
};
module.exports = Device;