Source: services/device-polling.js

'use strict';

/**
 * @copyright Kiwigrid GmbH 2014-2020
 * @module keta.services.DevicePolling
 * @description DevicePolling provider
 */

angular.module('keta.services.DevicePolling', [])
	/**
	 * @class ketaDevicePolling
	 * @propertyOf keta.services.DevicePolling
	 * @description
	 * <p>
	 *   A service providing polling for devices and tag values as a replacement
	 *   for the listener functionality that is removed in the Kiwicloud device service
	 *   version 12.
	 * </p>
	 * <p>
	 *   It has been added as a temporary solution to be used until apps using listeners
	 *   are transitioned to work without that functionality.
	 * </p>
	 */
	.service('ketaDevicePolling', function DevicePolling(
		$interval, $log, $q,
		ketaEventBusDispatcher, ketaEventBusManager, ketaDeviceEvent, ketaDeviceSetPollers
	) {
		var DEFAULT_POLL_INTERVAL_SECONDS = 15;
		var MILLISECONDS_PER_SECOND = 1000;

		var isValid = function(queryReply) {
			return queryReply
				&& queryReply.code === ketaEventBusDispatcher.RESPONSE_CODE_OK
				&& angular.isDefined(queryReply.result)
				&& angular.isDefined(queryReply.result.items);
		};

		var logFetchQueryReply = function(queryParameters, reply) {
			$log.request(['deviceservice', {
				action: 'getDevices',
				params: queryParameters
			}, reply], $log.ADVANCED_FORMATTER);
		};

		var fetchDevices = function(eventBus, queryParameters) {
			var deferred = $q.defer();
			ketaEventBusDispatcher.send(eventBus, 'deviceservice', {
				action: 'getDevices',
				params: queryParameters
			}, function(reply) {
				if (!isValid(reply)) {
					logFetchQueryReply(reply);
					return;
				}

				if (ketaEventBusManager.inDebugMode()) {
					logFetchQueryReply(reply);
				}
				deferred.resolve(reply);
			});
			return deferred.promise;
		};

		var compareGuids = function(thisDevice, thatDevice) {
			if (thisDevice.guid < thatDevice.guid) {
				return -1;
			}
			if (thisDevice.guid > thatDevice.guid) {
				return 1;
			}
			return 0;
		};

		var emitDeletionEvent = function(device, receiver) {
			receiver(ketaDeviceEvent.create(ketaDeviceEvent.DELETED, device));
		};

		var emitCreationEvent = function(device, receiver) {
			receiver(ketaDeviceEvent.create(ketaDeviceEvent.CREATED, device));
		};

		var emitUpdateEvent = function(device, receiver) {
			receiver(ketaDeviceEvent.create(ketaDeviceEvent.UPDATED, device));
		};

		var compareDeviceLists = function(currentDevices, fetchedDevices, changeEventProcessor) {
			var c = 0;
			var f = 0;
			while (c < currentDevices.length && f < fetchedDevices.length) {
				if (currentDevices[c].guid < fetchedDevices[f].guid) {
					emitDeletionEvent(currentDevices[c], changeEventProcessor);
					c++;
				} else if (currentDevices[c].guid > fetchedDevices[f].guid) {
					emitCreationEvent(fetchedDevices[f], changeEventProcessor);
					f++;
				} else if (!angular.equals(currentDevices[c], fetchedDevices[f])) {
					emitUpdateEvent(fetchedDevices[f], changeEventProcessor);
					c++;
					f++;
				} else {
					c++;
					f++;
				}
			}
			for (; c < currentDevices.length; c++) {
				emitDeletionEvent(currentDevices[c], changeEventProcessor);
			}
			for (; f < fetchedDevices.length; f++) {
				emitCreationEvent(fetchedDevices[f], changeEventProcessor);
			}
		};

		/**
		 * @name pollDevices
		 * @function
		 * @description
		 * <p>
		 *   Poll devices defined by the query parameters in the specified interval.
		 * </p>
		 * <p>
		 *   If a device matching the query changes, an event is emitted to the
		 *   processing function.
		 * </p>
		 * <p>
		 *   This function is intended as a temporary replacement for the removed
		 *   registerDeviceSetListener endpoint of the device service.
		 * </p>
		 * @param {EventBus} eventBus An event bus instance to communicate with the
		 *   device service
		 * @param {Object} queryParameters An object containing the parameters for the
		 *   device service endpoint to query devices
		 * @param {function} changeEventProcessor A function receiving an emitted
		 *   device event upon a change in matching devices
		 * @param {number} [intervalSeconds=15] The time between each query
		 * @returns {void}
		 */
		this.pollDevices = function(eventBus, queryParameters, changeEventProcessor, intervalSeconds) {
			intervalSeconds = intervalSeconds || DEFAULT_POLL_INTERVAL_SECONDS;

			var currentDevices;
			fetchDevices(eventBus, queryParameters).then(function(reply) {
				currentDevices = reply.result.items.sort(compareGuids);
			});

			var poller = $interval(function() {
				fetchDevices(eventBus, queryParameters).then(function(reply) {
					var fetchedDevices = reply.result.items.sort(compareGuids);
					if (currentDevices) {
						compareDeviceLists(currentDevices, fetchedDevices, changeEventProcessor);
					}
					currentDevices = fetchedDevices;
				});
			}, intervalSeconds * MILLISECONDS_PER_SECOND);
			ketaDeviceSetPollers.add(poller);
		};

		var emitTagValues = function(changeEvent, receiver) {
			if (changeEvent.getType() !== ketaDeviceEvent.CREATED
				&& changeEvent.getType() !== ketaDeviceEvent.UPDATED) {
				return;
			}

			var tagValuesObject = changeEvent.getDevice().tagValues;

			// Object#keys has better browser compatibility than Object#values
			var tagValues = Object.keys(tagValuesObject).map(function(tagName) {
				return tagValuesObject[tagName];
			});
			receiver(tagValues);
		};

		/**
		 * @name pollTagValues
		 * @function
		 * @description
		 * <p>
		 *   Poll tag values defined by the device query parameters in the specified
		 *   interval.
		 * </p>
		 * <p>
		 *   If a device matching the query is added or changes, its tag values are
		 *   emitted to the processing function.
		 * </p>
		 * <p>
		 *   This function is intended as a temporary replacement for the removed
		 *   registerTagValueListener endpoint of the device service.
		 * </p>
		 * @param {EventBus} eventBus An event bus instance to communicate with the
		 *   device service
		 * @param {Object} queryParameters An object containing the parameters for the
		 *   device service endpoint to query devices
		 * @param {function} tagValuesProcessor A function receiving the emitted
		 *   array of tag values upon changes in matching devices
		 * @param {number} [intervalSeconds=15] The time between each query
		 * @returns {void}
		 */
		this.pollTagValues = function(eventBus, queryParameters, tagValuesProcessor, intervalSeconds) {
			this.pollDevices(eventBus, queryParameters, function(changeEvent) {
				emitTagValues(changeEvent, tagValuesProcessor);
			}, intervalSeconds);
		};
	});