classes/device.js

'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;