plugins/pairer.js

'use strict';

/**
 * @class
 * @description Plugin to add and remove devices from bridge and model on pairing and unpairing.
 */

class Pairer {
	/**
	 * @constructor
	 * @param {Object} opts General settings for Pairer plugin
	 * @param {Adapter} adapter The adapter object associated with this Pairer plugin
	 *
	 */
	constructor(opts, adapter) {
		this._opts = opts;
		this.adapter = adapter;
		this._deviceFingerprintUtility = new this.adapter.bridge.classes.DeviceFingerprintUtility(adapter);
	}

	/**
	 * @description The adapter instance
	 * @type {Adapter}
	 */
	set adapter(adapter) {
		this._adapter = adapter;
	}

	get adapter() {
		return this._adapter;
	}

	/**
	 * @description The instance of the DeviceFingerprintUtility that the Pairer
	 * can harness
	 * @type {DeviceFingerprintUtility}
	 */
	get deviceFingerprintUtility() {
		return this._deviceFingerprintUtility;
	}

	/**
	 * @description Checks if device should be configured on
	 * pairing
	 * @param {String} nodePath K4Model node for desired device
	 */
	shouldConfigureOnPair(nodePath) {
		if (!nodePath) {
			throw new Error('No K4Model node path supplied');
		}

		const modelNode = this.adapter.k4.model.child(nodePath);
		if (!modelNode) {
			throw new Error('Unable to obtain K4Model node from path supplied');
		}

		const reply = {
			autoSet: true,
			autoConfig: false
		};

		const mapping = this.adapter.mapping[modelNode.cls()];
		if (mapping && mapping.config && typeof mapping.config.onPairing !== 'undefined'
			&& mapping.config.onPairing !== null) {
			reply.autoConfig = mapping.config.onPairing;
		}

		return reply;
	}

	/**
	 * @description Populates properties on node for paired device
	 * @param {String} nodePath K4Model node to populate (node or path)
	 * @param {Object} properties The device properties to set on K4Model
	 * @returns {Promise}
	 */
	setNode(nodePath, properties) {
		if (!nodePath) {
			return Promise.reject('No K4Model node or path supplied');
		}

		const modelNode = this.adapter.k4.model.child(nodePath);
		if (!modelNode) {
			return Promise.reject('Unable to obtain K4Model node from path supplied');
		}

		if (!properties) {
			return Promise.reject('No properties supplied: unable to set properties');
		}

		const setNodePropsPromises = [];
		Object.keys(properties).forEach((propName) => {
			const propVal = properties[propName];
			setNodePropsPromises.push(new Promise((resolve, reject) => {
				if (typeof propVal === 'object') {
					modelNode.child(`properties/${propName}`).update(propVal, (err) => {
						if (err) {
							reject(new Error(err));
						} else {
							resolve();
						}
					});
				} else {
					modelNode.child(`properties/${propName}`).set(propVal, (err) => {
						if (err) {
							reject(new Error(err));
						} else {
							resolve();
						}
					});
				}
			}));
		});

		return Promise.all(setNodePropsPromises);
	}

	/**
	 * @description Nullifies properties on node for unpaired device
	 * @param {String} nodePath K4Model node to clear (node or path)
	 * @param {Object} [properties] Device properties to clear on K4Model
	 * @returns {Promise}
	 */
	clearNode(nodePath, properties) {
		if (!nodePath) {
			throw new Error('No K4Model node path supplied');
		}

		const modelNode = this.adapter.k4.model.child(nodePath);
		if (!modelNode) {
			throw new Error('Unable to obtain K4Model node from path supplied');
		}

		// TODO: Will we need to clear other properties in addition to
		// networkId?
		if (properties) {
			const clearNodePropsPromises = [];
			Object.keys(properties).forEach((propName) => {
				clearNodePropsPromises.push(new Promise((resolve, reject) => {
					const propVal = properties[propName];
					if (typeof propVal === 'object') {
						modelNode.child(`properties/${propName}`).update({}, (err) => {
							if (err) {
								reject(new Error(err));
							} else {
								resolve();
							}
						});
					} else {
						modelNode.child(`properties/${propName}`).set(null, (err) => {
							if (err) {
								reject(new Error(err));
							} else {
								resolve();
							}
						});
					}
				}));
			});

			return Promise.all(clearNodePropsPromises);
		}

		return new Promise((resolve, reject) => {
			modelNode.child('properties/networkId').set(null, (err) => {
				if (err) {
					reject(new Error(err));
				} else {
					resolve();
				}
			});
		});
	}

	/**
	 * @description Builds a bridge Device object from K4Model node
	 * @param {String} devicePath The K4Model node for the device
	 * @returns {Device} The actual device object built by the bridge
	 */
	constructDevice(devicePath) {
		if (!devicePath) {
			throw new Error('No K4Model node or path supplied');
		}

		const modelNode = this.adapter.k4.model.child(devicePath);
		if (!modelNode) {
			throw new Error(`Unable to obtain K4Model node from supplied path: ${devicePath}`);
		}

		// the node is a new device to add to device list
		this.adapter.bridge.buildDevice(modelNode);

		const device = this.adapter.bridge.devices[modelNode.path()];
		if (!device) {
			throw new Error(`Unable to add device: ${modelNode.path()}`);
		}

		return device;
	}

	/**
	 * @description Builds a bridge Device object from a
	 * K4Model Class Name (cls)
	 * @param {String} deviceClsName The K4Model Class Name (cls)
	 * @param {Number} deviceId The device id
	 * @returns {Device} The actual device object built by the bridge
	 */
	constructTemporaryDevice(deviceClsName, deviceId) {
		if (!deviceClsName ||
			typeof deviceClsName !== 'string' ||
			deviceClsName.length === 0) {
			throw new Error('[constructTemporaryDevice] No device cls supplied');
		}

		if (!deviceId ||
			typeof deviceId !== 'number' ||
			!Number.isSafeInteger(deviceId)) {
			throw new Error('[constructTemporaryDevice] Node device id supplied');
		}

		if (!this.adapter.mapping ||
			!this.adapter.mapping[deviceClsName]) {
			throw new Error('[constructTemporaryDevice] No mapping available for cls: ' + deviceClsName);
		}

		const tempDevice = new this.adapter.bridge.classes.Device(this.adapter);
		tempDevice.mapping = this.adapter.mapping[deviceClsName];
		tempDevice.devicePath = `${this.adapter.bridge.classes.Device.constants.TEMPORARY_DEVICE_PATH_GENERIC_PREFIX}${deviceClsName}_${Date.now()}`;
		tempDevice.id = deviceId;
		tempDevice.isTemporaryDevice = true;

		// Actually add device to the bridge
		this.adapter.bridge.devices[tempDevice.devicePath] = tempDevice;
		return tempDevice;
	}
};

module.exports = Pairer;