<commit_before>module.exports = (function (rekey, addEvents, TransitionState, syncItCallbackToPromise, when, whenNode, whenCallback, whenFunction, arrayFilter, arrayMap, objectMap, objectKeys, SyncItConstant, SyncItControlBuffer) {

"use strict";

var AVAILABLE_AT_ALREADY = 3,
    AVAILABLE_AT_END = 2,
    AVAILABLE_AT_START = 1;

var Control = function(syncIt, eventSourceMonitor, asyncLocalStorage, uploadChangeFunc, conflictResolutionFunction) {
	this._syncIt = syncIt;
	this._eventSourceMonitor = eventSourceMonitor;
	this._uploadChangeFunc = uploadChangeFunc;
	this._datasets = [];
	this._monitoredCallbacks = [];
	this._conflictResolutionFunction = conflictResolutionFunction;
	this._asyncLocalStorage = asyncLocalStorage;
	this._connectingDatasets = [];
	this._connected = false;
	this._buffer = null;
	this._transitionState = (function() {
			var t = new TransitionState('DISCONNECTED'),
				states = {
					'DISCONNECTED': ['ANALYZE'],
					'ANALYZE': ['MISSING_DATASET', 'ALL_DATASET'],
					'MISSING_DATASET': ['PUSHING_DISCOVERY', 'DISCONNECTED', 'ERROR'],
					'ALL_DATASET': ['PUSHING_DISCOVERY', 'DISCONNECTED', 'ERROR'],
					'ADDING_DATASET': ['PUSHING_DISCOVERY', 'DISCONNECTED', 'ERROR'],
					'PUSHING_DISCOVERY': ['SYNCHED', 'PUSHING', 'DISCONNECTED', 'ERROR'],
					'PUSHING': ['PUSHING_DISCOVERY', 'SYNCHED', 'DISCONNECTED', 'ERROR'],
					'SYNCHED': ['ADDING_DATASET', 'PUSHING_DISCOVERY', 'DISCONNECTED', 'ERROR'],
					'ERROR': ['DISCONNECTED', 'ERROR']
				};
			for (var k in states) {
				if (states.hasOwnProperty(k)) {
					t.addState(k,states[k]);
				}
			}
			return t;
		})();

	var me = this;

	var stateFunctions = {
		'DISCONNECTED': this._stateDisconnected,
		'ANALYZE': this._stateAnalyze,
		'MISSING_DATASET': this._stateAddDataset.bind(this, AVAILABLE_AT_END),
		'ALL_DATASET': this._stateAddDataset.bind(this, AVAILABLE_AT_START),
		'ADDING_DATASET': this._stateAddDataset.bind(this, AVAILABLE_AT_ALREADY),
		'PUSHING_DISCOVERY': this._statePushingDiscovery,
		'PUSHING': this._statePushing,
		'SYNCHED': this._stateSynched,
		'ERROR': this._stateError
	};

	var serverMultiDataToArray = function(data) {
		var r = [];
		objectMap(data, function(v) {
			r = r.concat(v);
		});
		return r;
	};

	var syncItFeedToPromise = function(data) {
		return syncItCallbackToPromise(
			syncIt,
			syncIt.feed,
			[SyncItConstant.Error.OK, SyncItConstant.Error.NO_DATA_FOUND],
			serverMultiDataToArray(data),
			conflictResolutionFunction
		).then(function() {
			return data;
		});
	};

	this._eventSourceMonitor.on('messaged', function(message) {

		if (!message.hasOwnProperty('command')) {
			return;
		}

		if (message.command == 'queueitem') {
			me._buffer.add(message.data);
		}

		if (message.command == 'download') {

			syncItFeedToPromise(message.data).then(
				Control._updateKnownDatasetVersionsPromise.bind(this, asyncLocalStorage, me._datasets)
			).done(
				function() {
					me._transitionState.change('PUSHING_DISCOVERY');
					me._perhapsFireMonitoredCallbacks(true, me._currentUrl);
				},
				me._gotoError.bind(me)
			);

			me._buffer.start();

		}
	});

	this._eventSourceMonitor.on('disconnected', function() {
		me._transitionState.change('DISCONNECTED');
	});

	var connectedHandler = function(connObj) {
		me._currentUrl = connObj.url;
		me._perhapsFireMonitoredCallbacks(false, connObj.url);
	};

	this._eventSourceMonitor.on('url-changed', connectedHandler);
	this._eventSourceMonitor.on('connected', connectedHandler);

	this._transitionState.start();
	this._transitionState.on('changed-state', function(oldState, newState) {
		me._emit('entered-state', newState);
		stateFunctions[newState].call(me, newState);
	});

	syncIt.listenForAddedToPath(function() {
        if (me._transitionState.current() === 'SYNCHED') {
            me._transitionState.change('PUSHING_DISCOVERY');
        }
	});
};

Control._updateKnownDatasetVersionsPromise = function(asyncLocalStorage, datasets, messageData) {
	return Control._getKnownDatasetVersionsPromise(asyncLocalStorage, datasets).then(
		function(dataVersions) {
			var requiredVersionChanges = Control._getKnownVersionChanges(
				dataVersions,
				messageData
			);
			var r = [];
			objectMap(requiredVersionChanges, function(version, dataset) {
				r.push(whenCallback.call(
					asyncLocalStorage.setItem.bind(asyncLocalStorage),
					dataset,
					version
				));
			});
			return when.all(r);
		}
	);
};

Control.prototype._perhapsFireMonitoredCallbacks = function(downloaded, currentUrl) {

	var doIt = function(knownDatasets) {
		this._monitoredCallbacks = this._prepareForConnectedCallbacks(
			this._monitoredCallbacks,
			currentUrl,
			knownDatasets
		);
		this._fireWaitingMonitoredCallbacks();
	}.bind(this);

	if (downloaded) {
		return doIt(true);
	}

	this._asyncLocalStorage.findKeys('*', function(datasets) {
		doIt(datasets);
	});

};

Control.prototype._fireWaitingMonitoredCallbacks = function() {

	var me = this;

	this._monitoredCallbacks = arrayMap(this._monitoredCallbacks, function(callbackData) {
		if (callbackData === null) { return null; }
		if (callbackData.datasets.length === 0) {
			callbackData.callback.call(me);
			return null;
		}
		return callbackData;
	});
};

Control.prototype._extractInfoFromUrl = function(url) {
	var keys = [],
		values = [];
	var data = decodeURIComponent(url).split('&');
	arrayMap(data, function(urlComponent) {
		var vs = urlComponent.split('=');
		keys.push(vs[0].replace(/^[^\[]+\[/,'').replace(/\]$/,''));
		values.push( vs[1].length ? vs[1] : null);
	});
	return rekey(keys, values);
};

/**
 * ## Control._prepareForConnectedCallbacks()
 *
 * The idea of this function is to remove elements from
 * monitoredCallbacks[n][datasets] and if the array is empty it is ready to
 * call the callback.
 *
 * ### Parameters
 *
 *  * monitoredCallbacks - an array of Objects, when the objects "datasets" key is empty SyncItControl will fire the callback.
 *  * currentUrl - The URL that the EventSource is connected to.
 *  * alreadyConnectedDatasets - True or an array of Datasets. Which we already have some information about as we do not wait for the EventSource to connect to these before firing the callback.
 *
 */
Control.prototype._prepareForConnectedCallbacks = function(monitoredCallbacks, currentUrl, alreadyConnectedDatasets) {

	var currentDatasets = objectKeys(this._extractInfoFromUrl(currentUrl)),
		removeDataset = function(datasets, toRemoveDataset) {
				return arrayFilter(datasets, function(dataset) {
					return (dataset !== toRemoveDataset);
				});
			};

	monitoredCallbacks = arrayMap(monitoredCallbacks, function(callbackData) {
		if (callbackData === null) { return null; }
		arrayMap(currentDatasets, function(dataset) {
			if ((alreadyConnectedDatasets === true) || (alreadyConnectedDatasets.indexOf(dataset) > -1)) {
				callbackData.datasets = removeDataset(callbackData.datasets, dataset);
			}
		});
		return callbackData;
	});

	return monitoredCallbacks;
};

Control.prototype.getCurrentState = function() {
	return this._transitionState.current();
};

Control.prototype._gotoError = function() {
	throw new Error("AT ERROR");
	// this._transitionState.change('ERROR');
};

Control._getKnownVersionChanges = function(knownDatasetVersions, downloadedData) {

	var extractSequenceIdFromQueueitem = function(queueitem) {
		if (!queueitem.hasOwnProperty('_q')) { return null; }
		return queueitem._q;
	};
	var forIndividualDataset = function(knownVersion, downloadedData) {
		var highest = arrayMap(downloadedData, extractSequenceIdFromQueueitem).sort().pop();
		if (knownVersion === null) { return highest; }
		return ( highest > knownVersion ) ? highest : null;
	};

	var r = {};

	objectMap(downloadedData, function(v, k) {
		var storedVersion = knownDatasetVersions.hasOwnProperty(k) ? knownDatasetVersions[k] : null;
		var proposed = forIndividualDataset(storedVersion, v);
		if (proposed !== null) {
			r[k] = proposed;
		}
	});

	return r;
};

Control.prototype._getKnownDatasetVersionsPromise = function() {

	return Control._getKnownDatasetVersionsPromise(
		this._asyncLocalStorage,
		this._datasets
	);

};

Control._getKnownDatasetVersionsPromise = function(asyncLocalStorage, datasets) {

	var getVersionPromise = function(dataset) {
		return whenCallback.call(asyncLocalStorage.getItem.bind(asyncLocalStorage), dataset);
	};

	return when.all(arrayMap(datasets, getVersionPromise)).then(
		function(versions) {
			return rekey(datasets, versions);
		}
	);
};

Control.prototype._stateDisconnected = function() { };

Control.prototype._stateAnalyze = function() {
	var me = this;
	this._getKnownDatasetVersionsPromise().done(
		function(datasetAndVersions) {
			var haveNulls = false;
			objectMap(datasetAndVersions, function(v) {
				if (v === null) { haveNulls = true; }
			});
			me._transitionState.change(haveNulls ? 'MISSING_DATASET' : 'ALL_DATASET');
		},
		this._gotoError.bind(this)
	);
};

Control._convertToMessageDownloadFormat = function(arrayOfQueueitem) {
	var r = {};
	arrayMap(arrayOfQueueitem, function(queueitem) {
		if (!r.hasOwnProperty(queueitem.s)) { r[queueitem.s] = []; }
		r[queueitem.s].push(queueitem);
	});
	return objectMap(r, function(arrayOfQueueitemInDataset) {
		return arrayOfQueueitemInDataset.sort();
	});
};

Control.prototype._stateAddDataset = function(whenAvailable) {

	var me = this;

	this._buffer = new SyncItControlBuffer(
		function() { me._gotoError(); },
		function(queueitem, next) {
			me._syncIt.feed(
				[ queueitem ],
				me._conflictResolutionFunction,
				function(err) {
					if (err) { me._gotoError(); }
					Control._updateKnownDatasetVersionsPromise(
						me._asyncLocalStorage,
						me._datasets,
						Control._convertToMessageDownloadFormat([queueitem])
					).done(
						function() { next(null); },
						me._gotoError
					);
				}
			);
		},
		function() { },
		function() { }
	);

	if (whenAvailable == AVAILABLE_AT_START) {
		this._emit('available');
	}

	this._getKnownDatasetVersionsPromise().done(
		function(datasetAndVersions) {

			me._eventSourceMonitor.changeUrl(Control._prepareUrl(me._datasets, datasetAndVersions));
			if (!me._connected) {
				me._eventSourceMonitor.connect();
			}

		},
		this._gotoError.bind(this)
	);
};

Control._prepareUrl = function(fullDatasetList, knownDatasetVersions) {

	var prepareUrlPart = function(key, dataset, version) {
		var strVersion = '';
		if (version !== null) {
			strVersion = encodeURIComponent(version);
		}
		return encodeURIComponent(key + '[' + dataset + ']') + '=' + strVersion;
	};

	return arrayMap(fullDatasetList, function(dataset) {
		return prepareUrlPart(
			'dataset',
			dataset,
			knownDatasetVersions.hasOwnProperty(dataset) ? knownDatasetVersions[dataset] : ''
		);
	}).join('&');
};

Control.prototype.addDatasets = function(datasetNames, callback) {

	var datasetsLeft = arrayFilter(datasetNames, function(dsn) {
		return (this._datasets.indexOf(dsn) === -1);
	}.bind(this)).sort();

	if (datasetsLeft.length === 0) {
		if (callback !== undefined) {
			return callback(null, false, this._datasets);
		}
		return;
	}

	if (callback !== undefined) {
		this._monitoredCallbacks.push({
			datasets: datasetsLeft,
			callback: callback
		});
	}

	this._datasets = this._datasets.concat(datasetNames);
	this._datasets.sort();

	if (this._transitionState.current() === 'SYNCHED') {
		this._transitionState.change('ADDING_DATASET');
	}

};

Control.prototype._hasSyncItGotDataToPushPromise = function() {
	return syncItCallbackToPromise(
		this._syncIt,
		this._syncIt.getFirstInDatasets,
		[SyncItConstant.Error.OK, SyncItConstant.Error.NO_DATA_FOUND],
		this._datasets
	);
};

Control.prototype._findSyncItFirst = function(callbackWhenDataFound) {
	var transitionState = this._transitionState;
	when(this._hasSyncItGotDataToPushPromise()).done(
		function(data) {
			if (data === null) {
				return transitionState.change('SYNCHED');
			}
			return callbackWhenDataFound(data);
		}.bind(this),
		this._gotoError.bind(this)
	);
};

Control.prototype._statePushingDiscovery = function() {
	var transitionState = this._transitionState;
	this._findSyncItFirst(function() {
		return transitionState.change('PUSHING');
	});
};

Control.prototype._statePushing = function() {

	var transitionState = this._transitionState,
		uploadChangeFunc = this._uploadChangeFunc,
		syncIt = this._syncIt;

	this._findSyncItFirst(function(queueitem) {
		uploadChangeFunc(queueitem, function(err) {
			if (err) { return me._gotoError(); }
			syncIt.advance(function(status) {
				if (status !== SyncItConstant.Error.OK) {
					return me._gotoError();
				}
				return transitionState.change('PUSHING_DISCOVERY');
			});
		});
	});
};

Control.prototype._stateSynched = function() {
};

Control.prototype._stateError = function() {
};

Control.prototype.connect = function() {
	if (['DISCONNECTED', 'ERROR'].indexOf(this._transitionState.current()) == -1) {
		return;
	}
	this._transitionState.change('ANALYZE');
};

addEvents(Control, [
	'available',
	'uploaded-queueitem',
	'error-uploading-queueitem',
	'error-advancing-queueitem',
	'entered-state',
	'feed-version-error',
	'downloaded'
]);

return Control;

}(
	require('rekey'),
	require('add-events'),
	require('transition-state'),
	require('sync-it/syncItCallbackToPromise'),
	require('when'),
	require('when/node/function'),
	require('when/callbacks'),
	require('when/function'),
	require('mout/array/filter'),
	require('mout/array/map'),
	require('mout/object/map'),
	require('mout/object/keys'),
	require('sync-it/Constant'),
	require('syncit-control-buffer')
));

<commit_msg>Fix bug where "undefined" would be stored as a sequenceId<commit_after>module.exports = (function (rekey, addEvents, TransitionState, syncItCallbackToPromise, when, whenNode, whenCallback, whenFunction, arrayFilter, arrayMap, objectMap, objectKeys, SyncItConstant, SyncItControlBuffer) {

"use strict";

var AVAILABLE_AT_ALREADY = 3,
    AVAILABLE_AT_END = 2,
    AVAILABLE_AT_START = 1;

var Control = function(syncIt, eventSourceMonitor, asyncLocalStorage, uploadChangeFunc, conflictResolutionFunction) {
	this._syncIt = syncIt;
	this._eventSourceMonitor = eventSourceMonitor;
	this._uploadChangeFunc = uploadChangeFunc;
	this._datasets = [];
	this._monitoredCallbacks = [];
	this._conflictResolutionFunction = conflictResolutionFunction;
	this._asyncLocalStorage = asyncLocalStorage;
	this._connectingDatasets = [];
	this._connected = false;
	this._buffer = null;
	this._transitionState = (function() {
			var t = new TransitionState('DISCONNECTED'),
				states = {
					'DISCONNECTED': ['ANALYZE'],
					'ANALYZE': ['MISSING_DATASET', 'ALL_DATASET'],
					'MISSING_DATASET': ['PUSHING_DISCOVERY', 'DISCONNECTED', 'ERROR'],
					'ALL_DATASET': ['PUSHING_DISCOVERY', 'DISCONNECTED', 'ERROR'],
					'ADDING_DATASET': ['PUSHING_DISCOVERY', 'DISCONNECTED', 'ERROR'],
					'PUSHING_DISCOVERY': ['SYNCHED', 'PUSHING', 'DISCONNECTED', 'ERROR'],
					'PUSHING': ['PUSHING_DISCOVERY', 'SYNCHED', 'DISCONNECTED', 'ERROR'],
					'SYNCHED': ['ADDING_DATASET', 'PUSHING_DISCOVERY', 'DISCONNECTED', 'ERROR'],
					'ERROR': ['DISCONNECTED', 'ERROR']
				};
			for (var k in states) {
				if (states.hasOwnProperty(k)) {
					t.addState(k,states[k]);
				}
			}
			return t;
		})();

	var me = this;

	var stateFunctions = {
		'DISCONNECTED': this._stateDisconnected,
		'ANALYZE': this._stateAnalyze,
		'MISSING_DATASET': this._stateAddDataset.bind(this, AVAILABLE_AT_END),
		'ALL_DATASET': this._stateAddDataset.bind(this, AVAILABLE_AT_START),
		'ADDING_DATASET': this._stateAddDataset.bind(this, AVAILABLE_AT_ALREADY),
		'PUSHING_DISCOVERY': this._statePushingDiscovery,
		'PUSHING': this._statePushing,
		'SYNCHED': this._stateSynched,
		'ERROR': this._stateError
	};

	var serverMultiDataToArray = function(data) {
		var r = [];
		objectMap(data, function(v) {
			r = r.concat(v);
		});
		return r;
	};

	var syncItFeedToPromise = function(data) {
		return syncItCallbackToPromise(
			syncIt,
			syncIt.feed,
			[SyncItConstant.Error.OK, SyncItConstant.Error.NO_DATA_FOUND],
			serverMultiDataToArray(data),
			conflictResolutionFunction
		).then(function() {
			return data;
		});
	};

	this._eventSourceMonitor.on('messaged', function(message) {

		if (!message.hasOwnProperty('command')) {
			return;
		}

		if (message.command == 'queueitem') {
			me._buffer.add(message.data);
		}

		if (message.command == 'download') {

			syncItFeedToPromise(message.data).then(
				Control._updateKnownDatasetVersionsPromise.bind(this, asyncLocalStorage, me._datasets)
			).done(
				function() {
					me._transitionState.change('PUSHING_DISCOVERY');
					me._perhapsFireMonitoredCallbacks(true, me._currentUrl);
				},
				me._gotoError.bind(me)
			);

			me._buffer.start();

		}
	});

	this._eventSourceMonitor.on('disconnected', function() {
		me._transitionState.change('DISCONNECTED');
	});

	var connectedHandler = function(connObj) {
		me._currentUrl = connObj.url;
		me._perhapsFireMonitoredCallbacks(false, connObj.url);
	};

	this._eventSourceMonitor.on('url-changed', connectedHandler);
	this._eventSourceMonitor.on('connected', connectedHandler);

	this._transitionState.start();
	this._transitionState.on('changed-state', function(oldState, newState) {
		me._emit('entered-state', newState);
		stateFunctions[newState].call(me, newState);
	});

	syncIt.listenForAddedToPath(function() {
        if (me._transitionState.current() === 'SYNCHED') {
            me._transitionState.change('PUSHING_DISCOVERY');
        }
	});
};

Control._updateKnownDatasetVersionsPromise = function(asyncLocalStorage, datasets, messageData) {
	return Control._getKnownDatasetVersionsPromise(asyncLocalStorage, datasets).then(
		function(dataVersions) {
			var requiredVersionChanges = Control._getKnownVersionChanges(
				dataVersions,
				messageData
			);
			var r = [];
			objectMap(requiredVersionChanges, function(version, dataset) {
				r.push(whenCallback.call(
					asyncLocalStorage.setItem.bind(asyncLocalStorage),
					dataset,
					version
				));
			});
			return when.all(r);
		}
	);
};

Control.prototype._perhapsFireMonitoredCallbacks = function(downloaded, currentUrl) {

	var doIt = function(knownDatasets) {
		this._monitoredCallbacks = this._prepareForConnectedCallbacks(
			this._monitoredCallbacks,
			currentUrl,
			knownDatasets
		);
		this._fireWaitingMonitoredCallbacks();
	}.bind(this);

	if (downloaded) {
		return doIt(true);
	}

	this._asyncLocalStorage.findKeys('*', function(datasets) {
		doIt(datasets);
	});

};

Control.prototype._fireWaitingMonitoredCallbacks = function() {

	var me = this;

	this._monitoredCallbacks = arrayMap(this._monitoredCallbacks, function(callbackData) {
		if (callbackData === null) { return null; }
		if (callbackData.datasets.length === 0) {
			callbackData.callback.call(me);
			return null;
		}
		return callbackData;
	});
};

Control.prototype._extractInfoFromUrl = function(url) {
	var keys = [],
		values = [];
	var data = decodeURIComponent(url).split('&');
	arrayMap(data, function(urlComponent) {
		var vs = urlComponent.split('=');
		keys.push(vs[0].replace(/^[^\[]+\[/,'').replace(/\]$/,''));
		values.push( vs[1].length ? vs[1] : null);
	});
	return rekey(keys, values);
};

/**
 * ## Control._prepareForConnectedCallbacks()
 *
 * The idea of this function is to remove elements from
 * monitoredCallbacks[n][datasets] and if the array is empty it is ready to
 * call the callback.
 *
 * ### Parameters
 *
 *  * monitoredCallbacks - an array of Objects, when the objects "datasets" key is empty SyncItControl will fire the callback.
 *  * currentUrl - The URL that the EventSource is connected to.
 *  * alreadyConnectedDatasets - True or an array of Datasets. Which we already have some information about as we do not wait for the EventSource to connect to these before firing the callback.
 *
 */
Control.prototype._prepareForConnectedCallbacks = function(monitoredCallbacks, currentUrl, alreadyConnectedDatasets) {

	var currentDatasets = objectKeys(this._extractInfoFromUrl(currentUrl)),
		removeDataset = function(datasets, toRemoveDataset) {
				return arrayFilter(datasets, function(dataset) {
					return (dataset !== toRemoveDataset);
				});
			};

	monitoredCallbacks = arrayMap(monitoredCallbacks, function(callbackData) {
		if (callbackData === null) { return null; }
		arrayMap(currentDatasets, function(dataset) {
			if ((alreadyConnectedDatasets === true) || (alreadyConnectedDatasets.indexOf(dataset) > -1)) {
				callbackData.datasets = removeDataset(callbackData.datasets, dataset);
			}
		});
		return callbackData;
	});

	return monitoredCallbacks;
};

Control.prototype.getCurrentState = function() {
	return this._transitionState.current();
};

Control.prototype._gotoError = function() {
	throw new Error("AT ERROR");
	// this._transitionState.change('ERROR');
};

Control._getKnownVersionChanges = function(knownDatasetVersions, downloadedData) {

	var extractSequenceIdFromQueueitem = function(queueitem) {
		if (!queueitem.hasOwnProperty('_q')) { return null; }
		return queueitem._q;
	};

	var forIndividualDataset = function(knownVersion, downloadedData) {
		var highest = arrayMap(downloadedData, extractSequenceIdFromQueueitem).sort().pop();
		if (knownVersion === null) { return highest; }
		return ( highest > knownVersion ) ? highest : null;
	};

	var r = {};

	objectMap(downloadedData, function(v, k) {
		if ( (v === null) || (v.length === 0) ) { return; }
		var storedVersion = knownDatasetVersions.hasOwnProperty(k) ? knownDatasetVersions[k] : null;
		var proposed = forIndividualDataset(storedVersion, v);
		if (proposed !== null) {
			r[k] = proposed;
		}
	});

	return r;
};

Control.prototype._getKnownDatasetVersionsPromise = function() {

	return Control._getKnownDatasetVersionsPromise(
		this._asyncLocalStorage,
		this._datasets
	);

};

Control._getKnownDatasetVersionsPromise = function(asyncLocalStorage, datasets) {

	var getVersionPromise = function(dataset) {
		return whenCallback.call(asyncLocalStorage.getItem.bind(asyncLocalStorage), dataset);
	};

	return when.all(arrayMap(datasets, getVersionPromise)).then(
		function(versions) {
			return rekey(datasets, versions);
		}
	);
};

Control.prototype._stateDisconnected = function() { };

Control.prototype._stateAnalyze = function() {
	var me = this;
	this._getKnownDatasetVersionsPromise().done(
		function(datasetAndVersions) {
			var haveNulls = false;
			objectMap(datasetAndVersions, function(v) {
				if (v === null) { haveNulls = true; }
			});
			me._transitionState.change(haveNulls ? 'MISSING_DATASET' : 'ALL_DATASET');
		},
		this._gotoError.bind(this)
	);
};

Control._convertToMessageDownloadFormat = function(arrayOfQueueitem) {
	var r = {};
	arrayMap(arrayOfQueueitem, function(queueitem) {
		if (!r.hasOwnProperty(queueitem.s)) { r[queueitem.s] = []; }
		r[queueitem.s].push(queueitem);
	});
	return objectMap(r, function(arrayOfQueueitemInDataset) {
		return arrayOfQueueitemInDataset.sort();
	});
};

Control.prototype._stateAddDataset = function(whenAvailable) {

	var me = this;

	this._buffer = new SyncItControlBuffer(
		function() { me._gotoError(); },
		function(queueitem, next) {
			me._syncIt.feed(
				[ queueitem ],
				me._conflictResolutionFunction,
				function(err) {
					if (err) { me._gotoError(); }
					Control._updateKnownDatasetVersionsPromise(
						me._asyncLocalStorage,
						me._datasets,
						Control._convertToMessageDownloadFormat([queueitem])
					).done(
						function() { next(null); },
						me._gotoError
					);
				}
			);
		},
		function() { },
		function() { }
	);

	if (whenAvailable == AVAILABLE_AT_START) {
		this._emit('available');
	}

	this._getKnownDatasetVersionsPromise().done(
		function(datasetAndVersions) {

			me._eventSourceMonitor.changeUrl(Control._prepareUrl(me._datasets, datasetAndVersions));
			if (!me._connected) {
				me._eventSourceMonitor.connect();
			}

		},
		me._gotoError
	);
};

Control._prepareUrl = function(fullDatasetList, knownDatasetVersions) {

	var prepareUrlPart = function(key, dataset, version) {
		var strVersion = '';
		if (version !== null) {
			strVersion = encodeURIComponent(version);
		}
		return encodeURIComponent(key + '[' + dataset + ']') + '=' + strVersion;
	};

	return arrayMap(fullDatasetList, function(dataset) {
		return prepareUrlPart(
			'dataset',
			dataset,
			knownDatasetVersions.hasOwnProperty(dataset) ? knownDatasetVersions[dataset] : ''
		);
	}).join('&');
};

Control.prototype.addDatasets = function(datasetNames, callback) {

	var datasetsLeft = arrayFilter(datasetNames, function(dsn) {
		return (this._datasets.indexOf(dsn) === -1);
	}.bind(this)).sort();

	if (datasetsLeft.length === 0) {
		if (callback !== undefined) {
			return callback(null, false, this._datasets);
		}
		return;
	}

	if (callback !== undefined) {
		this._monitoredCallbacks.push({
			datasets: datasetsLeft,
			callback: callback
		});
	}

	this._datasets = this._datasets.concat(datasetNames);
	this._datasets.sort();

	if (this._transitionState.current() === 'SYNCHED') {
		this._transitionState.change('ADDING_DATASET');
	}

};

Control.prototype._hasSyncItGotDataToPushPromise = function() {
	return syncItCallbackToPromise(
		this._syncIt,
		this._syncIt.getFirstInDatasets,
		[SyncItConstant.Error.OK, SyncItConstant.Error.NO_DATA_FOUND],
		this._datasets
	);
};

Control.prototype._findSyncItFirst = function(callbackWhenDataFound) {
	var transitionState = this._transitionState;
	when(this._hasSyncItGotDataToPushPromise()).done(
		function(data) {
			if (data === null) {
				return transitionState.change('SYNCHED');
			}
			return callbackWhenDataFound(data);
		}.bind(this),
		this._gotoError.bind(this)
	);
};

Control.prototype._statePushingDiscovery = function() {
	var transitionState = this._transitionState;
	this._findSyncItFirst(function() {
		return transitionState.change('PUSHING');
	});
};

Control.prototype._statePushing = function() {

	var me = this;

	this._findSyncItFirst(function(queueitem) {
		me._uploadChangeFunc(queueitem, function(err) {
			if (err) { return me._gotoError(); }
			me._syncIt.advance(function(status) {
				if (status !== SyncItConstant.Error.OK) {
					return me._gotoError();
				}
				return me._transitionState.change('PUSHING_DISCOVERY');
			});
		});
	});
};

Control.prototype._stateSynched = function() {
};

Control.prototype._stateError = function() {
};

Control.prototype.connect = function() {
	if (['DISCONNECTED', 'ERROR'].indexOf(this._transitionState.current()) == -1) {
		return;
	}
	this._transitionState.change('ANALYZE');
};

addEvents(Control, [
	'available',
	'uploaded-queueitem',
	'error-uploading-queueitem',
	'error-advancing-queueitem',
	'entered-state',
	'feed-version-error',
	'downloaded'
]);

return Control;

}(
	require('rekey'),
	require('add-events'),
	require('transition-state'),
	require('sync-it/syncItCallbackToPromise'),
	require('when'),
	require('when/node/function'),
	require('when/callbacks'),
	require('when/function'),
	require('mout/array/filter'),
	require('mout/array/map'),
	require('mout/object/map'),
	require('mout/object/keys'),
	require('sync-it/Constant'),
	require('syncit-control-buffer')
));

<|endoftext|>


<commit_before>import { createSelector, createStructuredSelector } from 'reselect';
import isEmpty from 'lodash/isEmpty';
import { formatNumber } from 'utils/format';
import { aboveGroundToBelowGround } from 'utils/calculations';

const getData = state => state.data;
const getLocationName = state => state.locationName;
const getColors = state => state.colors;
const getSentences = state => state.config && state.config.sentences;
const getVariables = state => state.options && state.options.variables;
const getTitle = state => state.config.title;
const getSettings = state => state.settings;

export const calculateData = createSelector(
  [getData, getSettings, getVariables],
  (data, settings, variables) => {
    if (isEmpty(data)) return null;
    const soil = data && data.soilCarbon[settings.variable];
    const aboveGround = data.aboveGround[settings.variable];
    const aboveGroundCarbon = aboveGround * 0.5;
    const belowGround = aboveGroundToBelowGround(aboveGround) * 0.5;
    const total = soil + aboveGroundCarbon + belowGround;
    const activeVariable =
      variables && variables.find(v => v.value === settings.variable);
    const { unit } = activeVariable || {};

    return {
      soil,
      aboveGround: aboveGroundCarbon,
      belowGround,
      total,
      unit
    };
  }
);

export const parseData = createSelector(
  [calculateData, getColors],
  (data, colors) => {
    if (isEmpty(data)) return null;
    const { soil, aboveGround, belowGround, total, unit } = data || {};

    return [
      {
        label: 'Soil carbon',
        value: soil,
        unit,
        color: colors.carbon[0],
        percentage: soil / total * 100
      },
      {
        label: 'Above ground carbon',
        value: aboveGround,
        unit,
        color: colors.carbon[1],
        percentage: aboveGround / total * 100
      },
      {
        label: 'Below ground carbon',
        value: belowGround,
        unit,
        color: colors.carbon[2],
        percentage: belowGround / total * 100
      }
    ];
  }
);

export const parseSentence = createSelector(
  [calculateData, getLocationName, getSentences],
  (data, locationName, sentence) => {
    if (!data) return null;
    const { soil, aboveGround, belowGround, total, unit } = data || {};

    const allGround = aboveGround + belowGround;

    const params = {
      location: locationName,
      carbonStored: allGround > soil ? 'biomass' : 'soil',
      carbonValue: formatNumber({ num: total, unit })
    };

    return {
      sentence,
      params
    };
  }
);

export const parseTitle = createSelector(
  [getTitle, getLocationName],
  (title, location) => title.replace('{location}', location)
);

export default createStructuredSelector({
  data: parseData,
  sentence: parseSentence,
  title: parseTitle
});
<commit_msg>fix carbon stock widget for 0 data locations<commit_after>import { createSelector, createStructuredSelector } from 'reselect';
import isEmpty from 'lodash/isEmpty';
import { formatNumber } from 'utils/format';
import { aboveGroundToBelowGround } from 'utils/calculations';

const getData = state => state.data;
const getLocationName = state => state.locationName;
const getColors = state => state.colors;
const getSentences = state => state.config && state.config.sentences;
const getVariables = state => state.options && state.options.variables;
const getTitle = state => state.config.title;
const getSettings = state => state.settings;

export const calculateData = createSelector(
  [getData, getSettings, getVariables],
  (data, settings, variables) => {
    if (isEmpty(data)) return null;
    const soil =
      (data && data.soilCarbon && data.soilCarbon[settings.variable]) || 0;
    const aboveGround =
      (data && data.aboveGround && data.aboveGround[settings.variable]) || 0;
    const aboveGroundCarbon = aboveGround * 0.5;
    const belowGround = aboveGroundToBelowGround(aboveGround) * 0.5;
    const total = soil + aboveGroundCarbon + belowGround;
    const activeVariable =
      variables && variables.find(v => v.value === settings.variable);
    const { unit } = activeVariable || {};

    return {
      soil,
      aboveGround: aboveGroundCarbon,
      belowGround,
      total,
      unit
    };
  }
);

export const parseData = createSelector(
  [calculateData, getColors],
  (data, colors) => {
    if (isEmpty(data)) return null;
    const { soil, aboveGround, belowGround, total, unit } = data || {};

    return [
      {
        label: 'Soil carbon',
        value: soil,
        unit,
        color: colors.carbon[0],
        percentage: soil / total * 100
      },
      {
        label: 'Above ground carbon',
        value: aboveGround,
        unit,
        color: colors.carbon[1],
        percentage: aboveGround / total * 100
      },
      {
        label: 'Below ground carbon',
        value: belowGround,
        unit,
        color: colors.carbon[2],
        percentage: belowGround / total * 100
      }
    ];
  }
);

export const parseSentence = createSelector(
  [calculateData, getLocationName, getSentences],
  (data, locationName, sentence) => {
    if (!data) return null;
    const { soil, aboveGround, belowGround, total, unit } = data || {};

    const allGround = aboveGround + belowGround;

    const params = {
      location: locationName,
      carbonStored: allGround > soil ? 'biomass' : 'soil',
      carbonValue: formatNumber({ num: total, unit })
    };

    return {
      sentence,
      params
    };
  }
);

export const parseTitle = createSelector(
  [getTitle, getLocationName],
  (title, location) => title.replace('{location}', location)
);

export default createStructuredSelector({
  data: parseData,
  sentence: parseSentence,
  title: parseTitle
});
<|endoftext|>


<commit_before>const BroadlinkRMAccessory = require('./accessory');
const discoveredDevices = require('../helpers/devices.js');
const sendData = require('../helpers/sendData');

class AirConAccessory extends BroadlinkRMAccessory {

  constructor (log, config) {
    super(log, config)

    const { defaultCoolTemperature, defaultHeatTemperature, heatTemperature, minTemperature, maxTemperature, pseudoDeviceTemperature, replaceAutoMode, units } = config

    this.log = log;
    this.currentHeatingCoolingState = Characteristic.CurrentHeatingCoolingState.OFF
    this.targetTemperature = 0
    this.firstTemperatureUpdate = true

    config.minTemperature = minTemperature || 0
    config.maxTemperature = maxTemperature || 30

    config.temperatureDisplayUnits = (units && units.toLowerCase() === 'f') ? Characteristic.TemperatureDisplayUnits.FAHRENHEIT : Characteristic.TemperatureDisplayUnits.CELSIUS;

    // When a temperature hex doesn't exist we try to use the hex set for these
    // default temperatures
    config.defaultCoolTemperature = defaultCoolTemperature || 16
    config.defaultHeatTemperature = defaultHeatTemperature || 30

    // Used to determine when we should use the defaultHeatTemperature or the
    // defaultHeatTemperature
    config.heatTemperature = heatTemperature || 22

    // When we turn on the thermostat with Siri it comes thrugh as "auto" which
    // isn't particularly supported at this time so we convert the mode to cool
    // or heat
    // Note that this is only used when you use Siri or press Auto immediately
    // after launching Homebridge. The rest of the time we'll use your last known
    // temperature
    config.replaceAutoMode = replaceAutoMode || 'cool'

    if (config.pseudoDeviceTemperature > config.minTemperature) throw new Error(`The pseudoDeviceTemperature (${pseudoDeviceTemperature}) must be less than the minTemperature (${minTemperature})`)

    this.callbackQueue = {}
  }

  getServices () {
    const services = super.getServices();
    const { data, config, name } = this;
    const { minTemperature, maxTemperature } = config

    const service = new Service.Thermostat(name);

    // Required Characteristics
    service
      .getCharacteristic(Characteristic.CurrentHeatingCoolingState)
      .on('get', this.getCurrentHeatingCoolingState.bind(this));

    service
      .getCharacteristic(Characteristic.TargetHeatingCoolingState)
      .on('get', this.getTargetHeatingCoolingState.bind(this))
      .on('set', this.setTargetHeatingCoolingState.bind(this));

    service
      .getCharacteristic(Characteristic.CurrentTemperature)
      .on('get', this.getCurrentTemperature.bind(this));

    service
      .getCharacteristic(Characteristic.TargetTemperature)
      .on('get', this.getTargetTemperature.bind(this))
      .on('set', this.setTargetTemperature.bind(this));

    service
      .getCharacteristic(Characteristic.TemperatureDisplayUnits)
      .on('get', this.getTemperatureDisplayUnits.bind(this))
      .on('set', this.setTemperatureDisplayUnits.bind(this));

    //
    // service
    //   .getCharacteristic(Characteristic.On)
    //   .on('get', this.getOnOffState.bind(this))
    //   .on('set', this.setOnOffState.bind(this));

    service
      .getCharacteristic(Characteristic.Name)
      .on('get', this.getName.bind(this, name));

    service
      .getCharacteristic(Characteristic.CurrentTemperature)
      .setProps({
        minValue: minTemperature,
        maxValue: maxTemperature,
        minStep: 1
      });

    service
      .getCharacteristic(Characteristic.TargetTemperature)
      .setProps({
        minValue: minTemperature,
        maxValue: maxTemperature,
        minStep: 1
      });

    this.thermostatService = service
    services.push(service)

    return services
  }

  updateServiceHeatingCoolingState (value) {
    if (this.currentHeatingCoolingState === value) return

    this.currentHeatingCoolingState = value

    // this.log(`setCharacteristic for CurrentHeatingCoolingState and TargetHeatingCoolingState (${value})`)
    this.thermostatService.setCharacteristic(Characteristic.CurrentHeatingCoolingState, value);
    this.thermostatService.setCharacteristic(Characteristic.TargetHeatingCoolingState, value);
  }

  // Thermostat
  sendTemperature (temperature, callback) {
    const { config, data, host, log } = this
    const { defaultHeatTemperature, defaultCoolTemperature, heatTemperature } = config

    log(`Potentially sendTemperature (${temperature})`);

    let hasTemperatureChanged = (this.targetTemperature !== temperature)
    let hexData = data[`temperature${temperature}`]

    // You may not want to set the hex data for every single mode...
    if (!hexData) {
      const defaultTemperature = (temperature >= heatTemperature) ? defaultHeatTemperature : defaultCoolTemperature
      hexData = data[`temperature${defaultTemperature}`]

      if (!hexData) {
        const error = Error(`You need to provide a hex code for the given mode/temperature
          ({ "temperature${this.targetTemperature}": { "data": "HEXCODE", "pseudo-mode" : "auto/heat/cool" } })
          or at the very least, the default mode/temperature
          ({ "temperature${defaultTemperature}": { "data": "HEXCODE", "pseudo-mode" : "auto/heat/cool" } })`)

        log(error.message)

        return callback(error)
      }

      hasTemperatureChanged = (this.targetTemperature !== defaultTemperature)
      this.log(`Update to default temperature (${defaultTemperature})`);

      this.targetTemperature = defaultTemperature
    } else {
      this.targetTemperature = temperature
    }

    if (!hasTemperatureChanged && !this.firstTemperatureUpdate && this.currentHeatingCoolingState !== Characteristic.TargetHeatingCoolingState.OFF) return callback()

    this.firstTemperatureUpdate = false

    const mode = hexData['pseudo-mode']
    this.log(`sendTemperature (${this.targetTemperature}, ${mode})`);
    this.updateServiceHeatingCoolingState(this.heatingCoolingStateForConfigKey(hexData['pseudo-mode']))

    this.thermostatService.setCharacteristic(Characteristic.TargetTemperature, this.targetTemperature);

    this.lastUsedTemperature = this.targetTemperature
    this.lastUsedHeatingCoolingState= this.currentHeatingCoolingState

    sendData(host, hexData.data, callback, log)
  }

  setOnOffState (newState, callback) {
    const { log } = this
    log(`setOnOffState: ${newState}`);

    setTimeout(() => {
      this.thermostatService.setCharacteristic(Characteristic.TargetHeatingCoolingState, this.heatingCoolingStateForConfigKey('auto'));
    }, 100)

    callback()
  }

  getOnOffState (callback) {
    this.log(`getSwitchState: ${this.switchState}`);

    callback(null, this.switchState)
  }

	getCurrentHeatingCoolingState (callback) {
		this.log('getCurrentHeatingCoolingState');

    this.updateServiceHeatingCoolingState(this.currentHeatingCoolingState)

    let off = (!this.lastUsedHeatingCoolingState || (this.lastUsedHeatingCoolingState === this.heatingCoolingStateForConfigKey('off')))

    callback(null, !off);
	}

	getTargetHeatingCoolingState (callback) {
    const currentModeConfigKey = this.configKeyForCurrentHeatingCoolingState()
		this.log(`getTargetHeatingCoolingState (${currentModeConfigKey})`);

		callback(null, this.currentHeatingCoolingState); // success
	}

	setTargetHeatingCoolingState (value, callback) {
    const { config, data, host, log } = this
    const { defaultCoolTemperature, defaultHeatTemperature, replaceAutoMode } = config

    // Perform the auto -> cool/heat conversion descrived in constructor()
    if (replaceAutoMode && this.configKeyForHeatingCoolingState(value) === 'auto') {
      if (this.firstTemperatureUpdate || this.lastUsedHeatingCoolingState === this.heatingCoolingStateForConfigKey('auto')) {
     		log(`setTargetHeatingCoolingState (converting from auto to ${replaceAutoMode})`);

        setTimeout(() => {
          this.thermostatService.setCharacteristic(Characteristic.TargetHeatingCoolingState, this.heatingCoolingStateForConfigKey(replaceAutoMode));
        }, 300)
      } else {
        this.targetTemperature = this.lastUsedTemperature

        log(`setTargetHeatingCoolingState (converting from auto to last used - ${this.configKeyForHeatingCoolingState(this.lastUsedHeatingCoolingState)})`);
        setTimeout(() => {
          this.thermostatService.setCharacteristic(Characteristic.TargetHeatingCoolingState, this.lastUsedHeatingCoolingState);
        }, 300)
      }

      return callback()
    }

    if (value === undefined) return callback(); // Some calls are made to this without a value
    if (value === this.currentHeatingCoolingState) return callback()

    let temperature = this.targetTemperature

    if (value === Characteristic.TargetHeatingCoolingState.HEAT) {
      temperature = defaultHeatTemperature
    } else if (value === Characteristic.TargetHeatingCoolingState.COOL) {
      temperature = defaultCoolTemperature
    }

    const currentModeConfigKey = this.configKeyForHeatingCoolingState(value)
    log(`setTargetHeatingCoolingState (${currentModeConfigKey})`);

    if (currentModeConfigKey === 'off') {
      this.updateServiceHeatingCoolingState(value)
      sendData(host, data.off, callback, log)
    } else {
      this.sendTemperature(temperature, callback)
    }
	}

	getCurrentTemperature (callback) {
    const { config, host, log } = this;
    const { minTemperature, pseudoDeviceTemperature } = config

    // Some devices don't include a thermometer
    if (pseudoDeviceTemperature !== undefined) {
      log('getCurrentTemperature (ignored)')
      return callback(null, pseudoDeviceTemperature)
    }

    let device;

    if (host) {
      device = discoveredDevices[host];
    } else {
      const hosts = Object.keys(discoveredDevices)
      if (hosts.length === 0) {
        log(`getCurrentTemperature (no devices found)`);

        return callback(null, 0)
      }

      device = discoveredDevices[hosts[0]];
    }

    if (!device) {
      log(`getCurrentTemperature (no device found at ${host})`);

      return callback(null, 0)
    }

    const callbackIdentifier = Date.now()
    this.callbackQueue[callbackIdentifier] = callback

    // Make sure we're only calling one at a time
    if (Object.keys(this.callbackQueue).length > 1) return

    let onTemperature

    onTemperature = (temperature) => {
      this.currentTemperature = temperature;

      if (this.removeTemperatureListenerTimer) clearTimeout(this.removeTemperatureListenerTimer)
      device.removeListener('temperature', onTemperature);
      this.processQueuedCallbacks()

      log(`getCurrentTemperature (${temperature})`);
 		}

    // Add a 3 second timeout
    this.removeTemperatureListenerTimer = setTimeout(() => {
      device.removeListener('temperature');
      this.processQueuedCallbacks()

   		log(`getCurrentTemperature (3s timeout)`);
    }, 3000)

   	device.on('temperature', onTemperature);

 		device.checkTemperature();
	}

  processQueuedCallbacks () {
    Object.keys(this.callbackQueue).forEach((callbackIdentifier) => {
      const callback = this.callbackQueue[callbackIdentifier]
      callback(null, this.currentTemperature);

      delete this.callbackQueue[callbackIdentifier]
    })
  }

	getTargetTemperature (callback) {
		this.log(`getTargetTemperature (${this.targetTemperature})`);
		callback(null, this.targetTemperature)
	}

	setTargetTemperature (value, callback) {
		this.log(`setTargetTemperature (${value})`);

    this.sendTemperature(value, callback)
	}

	getTemperatureDisplayUnits (callback) {
    const { config, log } = this
    const { temperatureDisplayUnits } = config

		log(`getTemperatureDisplayUnits (${temperatureDisplayUnits})`);

		callback(null, temperatureDisplayUnits);
	}

	setTemperatureDisplayUnits (value, callback) {
    const { config, log } = this
    const { temperatureDisplayUnits } = config

		log(`setTemperatureDisplayUnits from ${temperatureDisplayUnits} to ${value}`);

		config.temperatureDisplayUnits = value;

		callback(null);
	}

  getName (name, callback) {
		this.log(`getName: ${name}`);

		callback(null, name);
	}

  configKeyForCurrentHeatingCoolingState () {
    return this.configKeyForHeatingCoolingState(this.currentHeatingCoolingState)
  }

  configKeyForHeatingCoolingState (state) {
    switch (state) {
      case Characteristic.TargetHeatingCoolingState.AUTO:
        return 'auto'
      case Characteristic.TargetHeatingCoolingState.COOL:
        return 'cool'
      case Characteristic.TargetHeatingCoolingState.HEAT:
        return 'heat'
      default:
        return 'off'
    }
  }

  heatingCoolingStateForConfigKey (configKey) {
    switch (configKey) {
      case 'off':
        return Characteristic.TargetHeatingCoolingState.OFF
      case 'cool':
        return Characteristic.TargetHeatingCoolingState.COOL
      case 'heat':
        return Characteristic.TargetHeatingCoolingState.HEAT
      default:
        return Characteristic.TargetHeatingCoolingState.AUTO
    }
  }
}

module.exports = AirConAccessory
<commit_msg>Fix aircon default targetTemprature having lower value than minTemperature<commit_after>const BroadlinkRMAccessory = require('./accessory');
const discoveredDevices = require('../helpers/devices.js');
const sendData = require('../helpers/sendData');

class AirConAccessory extends BroadlinkRMAccessory {

  constructor (log, config) {
    super(log, config)

    const { defaultCoolTemperature, defaultHeatTemperature, heatTemperature, minTemperature, maxTemperature, pseudoDeviceTemperature, replaceAutoMode, units } = config

    this.log = log;
    this.currentHeatingCoolingState = Characteristic.CurrentHeatingCoolingState.OFF
    this.targetTemperature = minTemperature
    this.firstTemperatureUpdate = true

    config.minTemperature = minTemperature || 0
    config.maxTemperature = maxTemperature || 30

    config.temperatureDisplayUnits = (units && units.toLowerCase() === 'f') ? Characteristic.TemperatureDisplayUnits.FAHRENHEIT : Characteristic.TemperatureDisplayUnits.CELSIUS;

    // When a temperature hex doesn't exist we try to use the hex set for these
    // default temperatures
    config.defaultCoolTemperature = defaultCoolTemperature || 16
    config.defaultHeatTemperature = defaultHeatTemperature || 30

    // Used to determine when we should use the defaultHeatTemperature or the
    // defaultHeatTemperature
    config.heatTemperature = heatTemperature || 22

    // When we turn on the thermostat with Siri it comes thrugh as "auto" which
    // isn't particularly supported at this time so we convert the mode to cool
    // or heat
    // Note that this is only used when you use Siri or press Auto immediately
    // after launching Homebridge. The rest of the time we'll use your last known
    // temperature
    config.replaceAutoMode = replaceAutoMode || 'cool'

    if (config.pseudoDeviceTemperature > config.minTemperature) throw new Error(`The pseudoDeviceTemperature (${pseudoDeviceTemperature}) must be less than the minTemperature (${minTemperature})`)

    this.callbackQueue = {}
  }

  getServices () {
    const services = super.getServices();
    const { data, config, name } = this;
    const { minTemperature, maxTemperature } = config

    const service = new Service.Thermostat(name);

    // Required Characteristics
    service
      .getCharacteristic(Characteristic.CurrentHeatingCoolingState)
      .on('get', this.getCurrentHeatingCoolingState.bind(this));

    service
      .getCharacteristic(Characteristic.TargetHeatingCoolingState)
      .on('get', this.getTargetHeatingCoolingState.bind(this))
      .on('set', this.setTargetHeatingCoolingState.bind(this));

    service
      .getCharacteristic(Characteristic.CurrentTemperature)
      .on('get', this.getCurrentTemperature.bind(this));

    service
      .getCharacteristic(Characteristic.TargetTemperature)
      .on('get', this.getTargetTemperature.bind(this))
      .on('set', this.setTargetTemperature.bind(this));

    service
      .getCharacteristic(Characteristic.TemperatureDisplayUnits)
      .on('get', this.getTemperatureDisplayUnits.bind(this))
      .on('set', this.setTemperatureDisplayUnits.bind(this));

    //
    // service
    //   .getCharacteristic(Characteristic.On)
    //   .on('get', this.getOnOffState.bind(this))
    //   .on('set', this.setOnOffState.bind(this));

    service
      .getCharacteristic(Characteristic.Name)
      .on('get', this.getName.bind(this, name));

    service
      .getCharacteristic(Characteristic.CurrentTemperature)
      .setProps({
        minValue: minTemperature,
        maxValue: maxTemperature,
        minStep: 1
      });

    service
      .getCharacteristic(Characteristic.TargetTemperature)
      .setProps({
        minValue: minTemperature,
        maxValue: maxTemperature,
        minStep: 1
      });

    this.thermostatService = service
    services.push(service)

    return services
  }

  updateServiceHeatingCoolingState (value) {
    if (this.currentHeatingCoolingState === value) return

    this.currentHeatingCoolingState = value

    // this.log(`setCharacteristic for CurrentHeatingCoolingState and TargetHeatingCoolingState (${value})`)
    this.thermostatService.setCharacteristic(Characteristic.CurrentHeatingCoolingState, value);
    this.thermostatService.setCharacteristic(Characteristic.TargetHeatingCoolingState, value);
  }

  // Thermostat
  sendTemperature (temperature, callback) {
    const { config, data, host, log } = this
    const { defaultHeatTemperature, defaultCoolTemperature, heatTemperature } = config

    log(`Potentially sendTemperature (${temperature})`);

    let hasTemperatureChanged = (this.targetTemperature !== temperature)
    let hexData = data[`temperature${temperature}`]

    // You may not want to set the hex data for every single mode...
    if (!hexData) {
      const defaultTemperature = (temperature >= heatTemperature) ? defaultHeatTemperature : defaultCoolTemperature
      hexData = data[`temperature${defaultTemperature}`]

      if (!hexData) {
        const error = Error(`You need to provide a hex code for the given mode/temperature
          ({ "temperature${this.targetTemperature}": { "data": "HEXCODE", "pseudo-mode" : "auto/heat/cool" } })
          or at the very least, the default mode/temperature
          ({ "temperature${defaultTemperature}": { "data": "HEXCODE", "pseudo-mode" : "auto/heat/cool" } })`)

        log(error.message)

        return callback(error)
      }

      hasTemperatureChanged = (this.targetTemperature !== defaultTemperature)
      this.log(`Update to default temperature (${defaultTemperature})`);

      this.targetTemperature = defaultTemperature
    } else {
      this.targetTemperature = temperature
    }

    if (!hasTemperatureChanged && !this.firstTemperatureUpdate && this.currentHeatingCoolingState !== Characteristic.TargetHeatingCoolingState.OFF) return callback()

    this.firstTemperatureUpdate = false

    const mode = hexData['pseudo-mode']
    this.log(`sendTemperature (${this.targetTemperature}, ${mode})`);
    this.updateServiceHeatingCoolingState(this.heatingCoolingStateForConfigKey(hexData['pseudo-mode']))

    this.thermostatService.setCharacteristic(Characteristic.TargetTemperature, this.targetTemperature);

    this.lastUsedTemperature = this.targetTemperature
    this.lastUsedHeatingCoolingState= this.currentHeatingCoolingState

    sendData(host, hexData.data, callback, log)
  }

  setOnOffState (newState, callback) {
    const { log } = this
    log(`setOnOffState: ${newState}`);

    setTimeout(() => {
      this.thermostatService.setCharacteristic(Characteristic.TargetHeatingCoolingState, this.heatingCoolingStateForConfigKey('auto'));
    }, 100)

    callback()
  }

  getOnOffState (callback) {
    this.log(`getSwitchState: ${this.switchState}`);

    callback(null, this.switchState)
  }

	getCurrentHeatingCoolingState (callback) {
		this.log('getCurrentHeatingCoolingState');

    this.updateServiceHeatingCoolingState(this.currentHeatingCoolingState)

    let off = (!this.lastUsedHeatingCoolingState || (this.lastUsedHeatingCoolingState === this.heatingCoolingStateForConfigKey('off')))

    callback(null, !off);
	}

	getTargetHeatingCoolingState (callback) {
    const currentModeConfigKey = this.configKeyForCurrentHeatingCoolingState()
		this.log(`getTargetHeatingCoolingState (${currentModeConfigKey})`);

		callback(null, this.currentHeatingCoolingState); // success
	}

	setTargetHeatingCoolingState (value, callback) {
    const { config, data, host, log } = this
    const { defaultCoolTemperature, defaultHeatTemperature, replaceAutoMode } = config

    // Perform the auto -> cool/heat conversion descrived in constructor()
    if (replaceAutoMode && this.configKeyForHeatingCoolingState(value) === 'auto') {
      if (this.firstTemperatureUpdate || this.lastUsedHeatingCoolingState === this.heatingCoolingStateForConfigKey('auto')) {
     		log(`setTargetHeatingCoolingState (converting from auto to ${replaceAutoMode})`);

        setTimeout(() => {
          this.thermostatService.setCharacteristic(Characteristic.TargetHeatingCoolingState, this.heatingCoolingStateForConfigKey(replaceAutoMode));
        }, 300)
      } else {
        this.targetTemperature = this.lastUsedTemperature

        log(`setTargetHeatingCoolingState (converting from auto to last used - ${this.configKeyForHeatingCoolingState(this.lastUsedHeatingCoolingState)})`);
        setTimeout(() => {
          this.thermostatService.setCharacteristic(Characteristic.TargetHeatingCoolingState, this.lastUsedHeatingCoolingState);
        }, 300)
      }

      return callback()
    }

    if (value === undefined) return callback(); // Some calls are made to this without a value
    if (value === this.currentHeatingCoolingState) return callback()

    let temperature = this.targetTemperature

    if (value === Characteristic.TargetHeatingCoolingState.HEAT) {
      temperature = defaultHeatTemperature
    } else if (value === Characteristic.TargetHeatingCoolingState.COOL) {
      temperature = defaultCoolTemperature
    }

    const currentModeConfigKey = this.configKeyForHeatingCoolingState(value)
    log(`setTargetHeatingCoolingState (${currentModeConfigKey})`);

    if (currentModeConfigKey === 'off') {
      this.updateServiceHeatingCoolingState(value)
      sendData(host, data.off, callback, log)
    } else {
      this.sendTemperature(temperature, callback)
    }
	}

	getCurrentTemperature (callback) {
    const { config, host, log } = this;
    const { minTemperature, pseudoDeviceTemperature } = config

    // Some devices don't include a thermometer
    if (pseudoDeviceTemperature !== undefined) {
      log('getCurrentTemperature (ignored)')
      return callback(null, pseudoDeviceTemperature)
    }

    let device;

    if (host) {
      device = discoveredDevices[host];
    } else {
      const hosts = Object.keys(discoveredDevices)
      if (hosts.length === 0) {
        log(`getCurrentTemperature (no devices found)`);

        return callback(null, 0)
      }

      device = discoveredDevices[hosts[0]];
    }

    if (!device) {
      log(`getCurrentTemperature (no device found at ${host})`);

      return callback(null, 0)
    }

    const callbackIdentifier = Date.now()
    this.callbackQueue[callbackIdentifier] = callback

    // Make sure we're only calling one at a time
    if (Object.keys(this.callbackQueue).length > 1) return

    let onTemperature

    onTemperature = (temperature) => {
      this.currentTemperature = temperature;

      if (this.removeTemperatureListenerTimer) clearTimeout(this.removeTemperatureListenerTimer)
      device.removeListener('temperature', onTemperature);
      this.processQueuedCallbacks()

      log(`getCurrentTemperature (${temperature})`);
 		}

    // Add a 3 second timeout
    this.removeTemperatureListenerTimer = setTimeout(() => {
      device.removeListener('temperature');
      this.processQueuedCallbacks()

   		log(`getCurrentTemperature (3s timeout)`);
    }, 3000)

   	device.on('temperature', onTemperature);

 		device.checkTemperature();
	}

  processQueuedCallbacks () {
    Object.keys(this.callbackQueue).forEach((callbackIdentifier) => {
      const callback = this.callbackQueue[callbackIdentifier]
      callback(null, this.currentTemperature);

      delete this.callbackQueue[callbackIdentifier]
    })
  }

	getTargetTemperature (callback) {
		this.log(`getTargetTemperature (${this.targetTemperature})`);
		callback(null, this.targetTemperature)
	}

	setTargetTemperature (value, callback) {
		this.log(`setTargetTemperature (${value})`);

    this.sendTemperature(value, callback)
	}

	getTemperatureDisplayUnits (callback) {
    const { config, log } = this
    const { temperatureDisplayUnits } = config

		log(`getTemperatureDisplayUnits (${temperatureDisplayUnits})`);

		callback(null, temperatureDisplayUnits);
	}

	setTemperatureDisplayUnits (value, callback) {
    const { config, log } = this
    const { temperatureDisplayUnits } = config

		log(`setTemperatureDisplayUnits from ${temperatureDisplayUnits} to ${value}`);

		config.temperatureDisplayUnits = value;

		callback(null);
	}

  getName (name, callback) {
		this.log(`getName: ${name}`);

		callback(null, name);
	}

  configKeyForCurrentHeatingCoolingState () {
    return this.configKeyForHeatingCoolingState(this.currentHeatingCoolingState)
  }

  configKeyForHeatingCoolingState (state) {
    switch (state) {
      case Characteristic.TargetHeatingCoolingState.AUTO:
        return 'auto'
      case Characteristic.TargetHeatingCoolingState.COOL:
        return 'cool'
      case Characteristic.TargetHeatingCoolingState.HEAT:
        return 'heat'
      default:
        return 'off'
    }
  }

  heatingCoolingStateForConfigKey (configKey) {
    switch (configKey) {
      case 'off':
        return Characteristic.TargetHeatingCoolingState.OFF
      case 'cool':
        return Characteristic.TargetHeatingCoolingState.COOL
      case 'heat':
        return Characteristic.TargetHeatingCoolingState.HEAT
      default:
        return Characteristic.TargetHeatingCoolingState.AUTO
    }
  }
}

module.exports = AirConAccessory
<|endoftext|>


<commit_before>/*
 * Script to obtain useful information from an Orange Your Account login in the UK.
 */

var system = require('system');

// Get the parameters from the launch script
var jsonString = system.args[1];
var params = JSON.parse(jsonString);

// Check we have the necessary parameters
if (!params.username)
{
	system.stderr.writeLine('This script requires a username');
	phantom.exit();
}
if (!params.password)
{
	system.stderr.writeLine('This script requires a password');
	phantom.exit();
}

function containsWarning(haystack, needle)
{
	if (haystack.indexOf(needle) === -1)
	{
		console.log('[warn] Search string `' + needle + '` not found');
	}
}

function login(params)
{
	/**
	 * This function is a class to hold the load handler methods
	 */
	function LoadHandler()
	{
		this.constants = new Object();
		this.constants.PAGE_LOGIN = 'login';
		this.constants.PAGE_EMAIL_REQUEST = 'email';

		/**
		 * Handles the loading of the login screen
		 *
		 * @param page
		 */
		this.onLoadLogin = function(page)
		{
			var pageId = page.evaluate(
				function(context)
				{
					var
						elementLoggedOut = document.querySelector('h1.PageTitleSignInEE'),
						titleLoggedOut = elementLoggedOut ? elementLoggedOut.innerText : '',

						elementEmail = document.querySelector('.login-flow h2'),
						titleEmail = elementEmail ? elementEmail.innerText : '',

						pageId = null
					;

					// Determine whether we are logged out...
					if (titleLoggedOut.indexOf('SERVICES ON ORANGE.CO.UK') > -1)
					{
						console.log('[ok] Found login screen');
						pageId = context.constants.PAGE_LOGIN;
					}
					// ... or logged in from a previous run
					else if (titleEmail.indexOf("We'd like to get to know you better") > -1)
					{
						console.log('[ok] Found email request screen, already logged in');
						pageId = context.constants.PAGE_EMAIL_REQUEST;
					}
					else
					{
						console.log('[debug] We received something unexpected');
						pageId = document.querySelector('body').textContent;
					}

					return pageId;
				},
				{
					constants: this.constants
				}
			);

			// Only make an attempt to log on if this is the login page
			if (pageId === this.constants.PAGE_LOGIN)
			{
				page.watcherId = 'LoginSubmitted';
				page.evaluate(
					function(params)
					{
						// Set up the credentials and submit the form
						document.querySelector('input[name=LOGIN]').value = params.username;
						document.querySelector('input[name=PASSWORD]').value = params.password;
						document.querySelector('div.inner_left_ee form').submit();
					},
					page.watcherParams
				);
			}
		};

		/**
		 * Handles the screen after the creds have been submitted
		 *
		 * If the credentials are wrong this is sometimes called twice
		 *
		 * @param page
		 */
		this.onLoadLoginSubmitted = function(page)
		{
			page.evaluate(
				function()
				{
					// Test for wrong credentials
					var error = document.querySelector('div.inner_left_ee p.error');
					if (error)
					{
						if (error.indexOf('Please enter a valid username and password') !== -1)
						{
							console.log(
								'[error] The username and password settings are incorrect'
							);
							phantom.exit();
						}
					}
				}
			);

			// Test for preferred email address screen, skip if found
			console.log(
				page.contents
			);

		};
	}

	// Logon to the system
	var
		page = require('webpage').create(),
		loadHandler = new LoadHandler()
	;

	page.onLoadStarted = function() {
		console.log('[ok] Page load started: ' + page.watcherId);
	};

	/**
	 * This is called, I think, when a redirect happens
	 *
	 * @param targetUrl
	 */
	page.onUrlChanged = function(targetUrl) {
		console.log('[debug] New URL: ' + targetUrl + ' on page: ' + page.watcherId);
	};

	page.onLoadFinished = function() {
		console.log('[ok] Page load finished:' + page.watcherId);

		// Call the custom handler, passing the page object in
		loadHandler['onLoad' + page.watcherId](page);
	};

	// Thanks to https://www.princeton.edu/~crmarsh/phantomjs/
	page.onConsoleMessage = function(msg)
	{
		console.log('Console log: ' + msg);
	};

	var url = 'https://web.orange.co.uk/r/login/';
	page.watcherId = 'Login';
	page.watcherParams = params;
	page.open(
		url,
		function(status)
		{
			if (status === 'fail')
			{
				system.stderr.writeLine("[error] Can't access home page to log on");
			}

			//phantom.exit();
		}
	);
}

login(params);

// @todo If it says "Preferred email address" then go to "https://web.orange.co.uk/id/profilemanagement.php?rm=SkipThisStep"
// @todo Check it says "hello customer"
// @todo Inject username into "emailormsisdn"
// @todo Inject password into "password"
// @todo Submit via "#SignInForm_EE form button"
<commit_msg>Made slightly more progress<commit_after>/*
 * Script to obtain useful information from an Orange Your Account login in the UK.
 */

var system = require('system');

// Get the parameters from the launch script
var jsonString = system.args[1];
var params = JSON.parse(jsonString);

// Check we have the necessary parameters
if (!params.username)
{
	system.stderr.writeLine('This script requires a username');
	phantom.exit();
}
if (!params.password)
{
	system.stderr.writeLine('This script requires a password');
	phantom.exit();
}

function containsWarning(haystack, needle)
{
	if (haystack.indexOf(needle) === -1)
	{
		console.log('[warn] Search string `' + needle + '` not found');
	}
}

function login(params)
{
	/**
	 * This function is a class to hold the load handler methods
	 */
	function LoadHandler()
	{
		this.constants = new Object();
		this.constants.PAGE_LOGIN = 'login';
		this.constants.PAGE_EMAIL_REQUEST = 'email';

		/**
		 * Handles the loading of the login screen
		 *
		 * @param page
		 * @param status
		 */
		this.onLoadLogin = function(page, status)
		{
			// We get a failure here if we're already logged on, so let's see if we can redirect
			if (status === 'fail')
			{
				console.log('[debug] Receive failure when finding login page, may already be logged on');
				page.watcherId = 'TryAccountPage';
				page.open('https://www.youraccount.orange.co.uk/sss/jfn?entry=true');

				return;
			}

			var pageId = page.evaluate(
				function(context)
				{
					var
						elementLoggedOut = document.querySelector('h1.PageTitleSignInEE'),
						titleLoggedOut = elementLoggedOut ? elementLoggedOut.innerText : '',

						elementEmail = document.querySelector('.login-flow h2'),
						titleEmail = elementEmail ? elementEmail.innerText : '',

						pageId = null
					;

					// Determine whether we are logged out...
					if (titleLoggedOut.indexOf('SERVICES ON ORANGE.CO.UK') > -1)
					{
						console.log('[ok] Found login screen');
						pageId = context.constants.PAGE_LOGIN;
					}
					// ... or logged in from a previous run
					else if (titleEmail.indexOf("We'd like to get to know you better") > -1)
					{
						console.log('[ok] Found email request screen, already logged in');
						pageId = context.constants.PAGE_EMAIL_REQUEST;
					}
					else
					{
						console.log('[debug] We received something unexpected');
						pageId = document.querySelector('body').textContent;
					}

					return pageId;
				},
				{
					constants: this.constants
				}
			);

			// Only make an attempt to log on if this is the login page
			if (pageId === this.constants.PAGE_LOGIN)
			{
				page.watcherId = 'LoginSubmitted';
				page.evaluate(
					function(params)
					{
						// Set up the credentials and submit the form
						document.querySelector('input[name=LOGIN]').value = params.username;
						document.querySelector('input[name=PASSWORD]').value = params.password;
						document.querySelector('div.inner_left_ee form').submit();
					},
					page.watcherParams
				);
			}
		};

		/**
		 * Handles the screen after the creds have been submitted
		 *
		 * If the credentials are wrong this is sometimes called twice
		 *
		 * @param page
		 * @param status
		 */
		this.onLoadLoginSubmitted = function(page, status)
		{
			page.evaluate(
				function()
				{
					// Test for wrong credentials
					var error = document.querySelector('div.inner_left_ee p.error');
					if (error)
					{
						if (error.indexOf('Please enter a valid username and password') !== -1)
						{
							console.log(
								'[error] The username and password settings are incorrect'
							);
							phantom.exit();
						}
					}
				}
			);

			// Test for preferred email address screen, skip if found
			console.log(
				page.contents
			);
		};

		this.onLoadTryAccountPage = function(page, status)
		{
			console.log('Here is the account page, already logged on: ' + status);
		};
	}

	// Logon to the system
	var
		page = require('webpage').create(),
		loadHandler = new LoadHandler()
	;

	page.onLoadStarted = function() {
		console.log('[ok] Page load started: ' + page.watcherId);
	};

	/**
	 * This is called, I think, when a redirect happens
	 *
	 * @param targetUrl
	 */
	page.onUrlChanged = function(targetUrl) {
		console.log('[debug] New URL: ' + targetUrl + ' on page: ' + page.watcherId);
	};

	page.onLoadFinished = function(status) {
		console.log('[ok] Page load finished, page: ' + page.watcherId + ', status: ' + status);

		// Call the custom handler, passing in interesting params
		loadHandler['onLoad' + page.watcherId](page, status);
	};

	// Thanks to https://www.princeton.edu/~crmarsh/phantomjs/
	page.onConsoleMessage = function(msg)
	{
		console.log('Console log: ' + msg);
	};

	page.watcherId = 'Login';
	page.watcherParams = params;
	page.open('https://web.orange.co.uk/r/login/');
}

login(params);

// @todo If it says "Preferred email address" then go to "https://web.orange.co.uk/id/profilemanagement.php?rm=SkipThisStep"
// @todo Check it says "hello customer"
// @todo Inject username into "emailormsisdn"
// @todo Inject password into "password"
// @todo Submit via "#SignInForm_EE form button"
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
({
	PANELS_DEF      : {},  // Definitions of registered panels
    PANELS_OWNER    : {},  // Owner relationship who creates the panel (key) owned by -> value
    PANELS_STACK    : [],  // The Panel Stack ordering
    PANELS_INSTANCE : {},  // Registered instances

	initialize: function(cmp) {
        var containerManager = this.cmLib.containerManager;
        var sharedContainer  = cmp.get('v.useSharedContainer');

        this.containerManager = sharedContainer ? containerManager.getSharedInstance() : containerManager.createInstance(cmp.find('container'));
        this.initializeRegisteredPanels(cmp);
    },

    /*
    * Store internal defs
    * @private
    */
    initializeRegisteredPanels: function (cmp, newPanels) {
        this.containerManager.registerContainers(newPanels || cmp.get('v.registeredPanels') || []);
    },

    /*
    * Create panel
    * @public
    */
    createPanel: function (cmp, config) {
        var panelConfig      = config.panelConfig || {};
        var referenceElement = panelConfig.referenceElement;

        panelConfig.referenceElement = null;

        // Create panel instance
        var panel = this.createPanelInstance(cmp, config);

        if (referenceElement) {
            panel.set('v.referenceElement', referenceElement);
        }

        // Save instance config
        this.PANELS_INSTANCE[panel.getGlobalId()] = {
            panel           : panel,
            destroyCallback : config.onDestroy
        };

        // Set owner
        this.setPanelOwner(panel, config.owner);

        // Render
        this.renderPanelInstance(cmp, panel, config);

        // Stack panel
        this.stackPanel(panel);

        if (config.onCreate) {
            config.onCreate(panel);
        }
    },
    /*
    * Get Active panel
    * @public
    */
    getActivePanel: function (cmp, callback) {
        // TODO: Instead of assuming is the last one (which doesnt guarantee that is "active")
        // Change the logic on active to make sure we save that state internally
        var stack = this.PANELS_STACK;
        callback(stack[stack.length - 1]);
    },
    /*
    * Sets the context in which the panel is created
    * This is mostly to figure out the the relationship between two panels
    * This function should be called before stacking a new panel
    */
    setPanelOwner: function (panel, givenOwner) {
        var owner = givenOwner;
        if (!owner) {
            var previousPanel = this.PANELS_STACK[this.PANELS_STACK.length - 1],
                previousBody = previousPanel && previousPanel.get('v.body');

            owner = !$A.util.isEmpty(previousBody) ? previousBody[0].getGlobalId() : null;
        }

        this.PANELS_OWNER[panel.getGlobalId()] = owner;
    },

    /*
    * Stack a panel in our internal structures
    */
    stackPanel: function (panel) {
        var stackManager = this.smLib.stackManager;

        this.PANELS_ZINDEX++;
        this.PANELS_STACK.push(panel);
        stackManager.bringToFront(panel);
    },

    /*
     * stack an element as if it was a panel
     */
    stackElement: function (cb) {
        this.PANELS_ZINDEX++;
        cb(this.PANELS_ZOFFSET + this.PANELS_ZINDEX);
    },

    /*
    * Create panel instance
    * @private
    */
    createPanelInstance: function (cmp, config) {
        var panel = this.containerManager.createContainer({
                containerType          : config.panelType,
                containerConfig        : config.panelConfig,
                containerValueProvider : cmp
        });

        var header  = panel.get('v.header'),
            body    = panel.get('v.body'),
            footer  = panel.get('v.footer'),
            avp, i, length;

        if (!$A.util.isEmpty(body)) {
            body[0].setAttributeValueProvider(panel);
            avp = body[0];
        } else {
            avp = panel;
        }

        if (!$A.util.isEmpty(header)) {
            for (i = 0, length = header.length; i < length; i++) {
                header[i].setAttributeValueProvider(avp);
            }
        }
        if (!$A.util.isEmpty(footer)) {
            for (i = 0, length = footer.length; i < length; i++) {
                footer[i].setAttributeValueProvider(avp);
            }
        }

        return panel;
    },

    beforeShow: function(cmp, config) {
        var panelParam = config.panelInstance,
            panelId    = $A.util.isComponent(panelParam) ? panelParam.getGlobalId() : panelParam,
            panelObj   = this.PANELS_INSTANCE[panelId],
            panel      = panelObj.panel;

        var activeElement = document.activeElement;

        if (activeElement) {
            cmp.returnFocus = activeElement;
        }

        $A.assert(panelObj, 'Couldnt find instance to show');

        // de-active all other panels except the one currently shown
        this.deactivateAllPanelInstances(cmp, panel);
    },

     /*
    * Destroy panel instance
    * @private
    */
    destroyPanelInstance: function (cmp, config) {
        var stack      = this.PANELS_STACK,
            panelParam = config.panelInstance,
            panelId    = $A.util.isComponent(panelParam) ? panelParam.getGlobalId() : panelParam,
            panelObj   = this.PANELS_INSTANCE[panelId],
            panel      = panelObj.panel,
            index      = stack.indexOf(panel);

        $A.assert(panelObj, 'Couldnt find instance to destroy');
        $A.assert(index > -1, 'Couldnt find the reference in the stack');

        stack.splice(index, 1);

        this.containerManager.destroyContainer(panel);

        delete this.PANELS_OWNER[panelId];
        delete this.PANELS_INSTANCE[panelId];

        if (cmp.returnFocus) {
            cmp.returnFocus.focus();
        }

        // Notify the destroy
        config.onDestroy && config.onDestroy();
        if (panelObj.destroyCallback) {
            panelObj.destroyCallback(panelId);
        }

        this.activateNextPanel(cmp);
    },

    /**
     * Activate the candidate panel
     * @param cmp
     * @private
     */
    activateNextPanel: function() {
        //find the panel to active
        for (var panel, i = this.PANELS_STACK.length - 1; i >= 0; i--) {
            panel = this.PANELS_STACK[i];
            if (panel && panel.isValid() && panel.get('v.visible') && !panel.destroyPending) {
                panel.setActive(true);
                break;
            }
        }
    },

    /**
     * De-activate all the panels except the active one
     * @param cmp
     */
    deactivateAllPanelInstances: function(cmp, activePanel) {
        for (var panel, i = this.PANELS_STACK.length - 1; i >= 0; i--) {
            panel = this.PANELS_STACK[i];
            if (panel && panel.isValid() && panel !== activePanel) {
                panel.setActive(false);
            }
        }
    },

    /*
    * Rendering the panel
    * We call $A.render because we want to render the component immediately
    * so we can send it back synchronously to the user
    * @private
    */
    renderPanelInstance: function (cmp, panel, config) {
        this.containerManager.renderContainer(panel, config);
    },
    notifyPanelContent: function (content, config) {
        var validInterface = config.typeOf ? content.isInstanceOf(config.typeOf) : true,
            validMethod    = content[config.action];

        if (validInterface && validMethod) {
            content[config.action](config.payload); // dispatch the method
        }
    },
    broadcastNotify: function (cmp, source, config) {
        var scope         = config.scope,
            currentTarget = config.currentTarget || this.getContainerPanelId(source),
            stack         = this.PANELS_STACK,
            owner         = this.PANELS_OWNER[currentTarget],
            panel, content, i;

        $A.assert(!scope || (scope !== 'all' || scope !== 'owner' || scope !== "self"), 'Invalid target (all || owner)');

        if (scope === 'all') {
            for (i = stack.length - 1; i >= 0; --i) {
                panel = stack[i];
                if (currentTarget !== panel.getGlobalId()) { // Dont notify itself
                	content = panel.get('v.body')[0];
                    this.notifyPanelContent(content, config);
                }
            }
        } else if (scope === 'self') {
        	if (currentTarget) {
                currentTarget = $A.getComponent(currentTarget);
                this.notifyPanelContent(currentTarget.get('v.body')[0], config);
        	}
        } else if (owner) {
            var ownerCmp = $A.getComponent(owner);
            if (ownerCmp && ownerCmp.isValid()) {
            	this.notifyPanelContent(ownerCmp, config);
            }
        }
    },
    getContainerPanelId: function (source) {
        var provider = source;
        while (provider) {
            if (provider.isInstanceOf("ui:panelType")) {
                return provider.getGlobalId();
            }
            source = provider.getAttributeValueProvider();
            provider = source !== provider ? source : null;
        }
    }
})// eslint-disable-line semi
<commit_msg>fixing default panel owner<commit_after>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
({
	PANELS_DEF      : {},  // Definitions of registered panels
    PANELS_OWNER    : {},  // Owner relationship who creates the panel (key) owned by -> value
    PANELS_STACK    : [],  // The Panel Stack ordering
    PANELS_INSTANCE : {},  // Registered instances

	initialize: function(cmp) {
        var containerManager = this.cmLib.containerManager;
        var sharedContainer  = cmp.get('v.useSharedContainer');

        this.containerManager = sharedContainer ? containerManager.getSharedInstance() : containerManager.createInstance(cmp.find('container'));
        this.initializeRegisteredPanels(cmp);
    },

    /*
    * Store internal defs
    * @private
    */
    initializeRegisteredPanels: function (cmp, newPanels) {
        this.containerManager.registerContainers(newPanels || cmp.get('v.registeredPanels') || []);
    },

    /*
    * Create panel
    * @public
    */
    createPanel: function (cmp, config) {
        var panelConfig      = config.panelConfig || {};
        var referenceElement = panelConfig.referenceElement;

        panelConfig.referenceElement = null;

        // Create panel instance
        var panel = this.createPanelInstance(cmp, config);

        if (referenceElement) {
            panel.set('v.referenceElement', referenceElement);
        }

        // Save instance config
        this.PANELS_INSTANCE[panel.getGlobalId()] = {
            panel           : panel,
            destroyCallback : config.onDestroy
        };

        // Set owner
        this.setPanelOwner(panel, config.owner);

        // Render
        this.renderPanelInstance(cmp, panel, config);

        // Stack panel
        this.stackPanel(panel);

        if (config.onCreate) {
            config.onCreate(panel);
        }
    },
    /*
    * Get Active panel
    * @public
    */
    getActivePanel: function (cmp, callback) {
        // TODO: Instead of assuming is the last one (which doesnt guarantee that is "active")
        // Change the logic on active to make sure we save that state internally
        var stack = this.PANELS_STACK;
        callback(stack[stack.length - 1]);
    },
    /*
    * Sets the context in which the panel is created
    * This is mostly to figure out the the relationship between two panels
    * This function should be called before stacking a new panel
    */
    setPanelOwner: function (panel, givenOwner) {
        var owner = givenOwner;
        if (!owner) {
            var previousPanel = this.PANELS_STACK[this.PANELS_STACK.length - 1],
                previousBody = previousPanel && previousPanel.get('v.body');

            owner = $A.util.isEmpty(previousBody) ? panel.getGlobalId() : previousBody[0].getGlobalId();
        }

        this.PANELS_OWNER[panel.getGlobalId()] = owner;
    },

    /*
    * Stack a panel in our internal structures
    */
    stackPanel: function (panel) {
        var stackManager = this.smLib.stackManager;

        this.PANELS_ZINDEX++;
        this.PANELS_STACK.push(panel);
        stackManager.bringToFront(panel);
    },

    /*
     * stack an element as if it was a panel
     */
    stackElement: function (cb) {
        this.PANELS_ZINDEX++;
        cb(this.PANELS_ZOFFSET + this.PANELS_ZINDEX);
    },

    /*
    * Create panel instance
    * @private
    */
    createPanelInstance: function (cmp, config) {
        var panel = this.containerManager.createContainer({
                containerType          : config.panelType,
                containerConfig        : config.panelConfig,
                containerValueProvider : cmp
        });

        var header  = panel.get('v.header'),
            body    = panel.get('v.body'),
            footer  = panel.get('v.footer'),
            avp, i, length;

        if (!$A.util.isEmpty(body)) {
            body[0].setAttributeValueProvider(panel);
            avp = body[0];
        } else {
            avp = panel;
        }

        if (!$A.util.isEmpty(header)) {
            for (i = 0, length = header.length; i < length; i++) {
                header[i].setAttributeValueProvider(avp);
            }
        }
        if (!$A.util.isEmpty(footer)) {
            for (i = 0, length = footer.length; i < length; i++) {
                footer[i].setAttributeValueProvider(avp);
            }
        }

        return panel;
    },

    beforeShow: function(cmp, config) {
        var panelParam = config.panelInstance,
            panelId    = $A.util.isComponent(panelParam) ? panelParam.getGlobalId() : panelParam,
            panelObj   = this.PANELS_INSTANCE[panelId],
            panel      = panelObj.panel;

        var activeElement = document.activeElement;

        if (activeElement) {
            cmp.returnFocus = activeElement;
        }

        $A.assert(panelObj, 'Couldnt find instance to show');

        // de-active all other panels except the one currently shown
        this.deactivateAllPanelInstances(cmp, panel);
    },

     /*
    * Destroy panel instance
    * @private
    */
    destroyPanelInstance: function (cmp, config) {
        var stack      = this.PANELS_STACK,
            panelParam = config.panelInstance,
            panelId    = $A.util.isComponent(panelParam) ? panelParam.getGlobalId() : panelParam,
            panelObj   = this.PANELS_INSTANCE[panelId],
            panel      = panelObj.panel,
            index      = stack.indexOf(panel);

        $A.assert(panelObj, 'Couldnt find instance to destroy');
        $A.assert(index > -1, 'Couldnt find the reference in the stack');

        stack.splice(index, 1);

        this.containerManager.destroyContainer(panel);

        delete this.PANELS_OWNER[panelId];
        delete this.PANELS_INSTANCE[panelId];

        if (cmp.returnFocus) {
            cmp.returnFocus.focus();
        }

        // Notify the destroy
        config.onDestroy && config.onDestroy();
        if (panelObj.destroyCallback) {
            panelObj.destroyCallback(panelId);
        }

        this.activateNextPanel(cmp);
    },

    /**
     * Activate the candidate panel
     * @param cmp
     * @private
     */
    activateNextPanel: function() {
        //find the panel to active
        for (var panel, i = this.PANELS_STACK.length - 1; i >= 0; i--) {
            panel = this.PANELS_STACK[i];
            if (panel && panel.isValid() && panel.get('v.visible') && !panel.destroyPending) {
                panel.setActive(true);
                break;
            }
        }
    },

    /**
     * De-activate all the panels except the active one
     * @param cmp
     */
    deactivateAllPanelInstances: function(cmp, activePanel) {
        for (var panel, i = this.PANELS_STACK.length - 1; i >= 0; i--) {
            panel = this.PANELS_STACK[i];
            if (panel && panel.isValid() && panel !== activePanel) {
                panel.setActive(false);
            }
        }
    },

    /*
    * Rendering the panel
    * We call $A.render because we want to render the component immediately
    * so we can send it back synchronously to the user
    * @private
    */
    renderPanelInstance: function (cmp, panel, config) {
        this.containerManager.renderContainer(panel, config);
    },
    notifyPanelContent: function (content, config) {
        var validInterface = config.typeOf ? content.isInstanceOf(config.typeOf) : true,
            validMethod    = content[config.action];

        if (validInterface && validMethod) {
            content[config.action](config.payload); // dispatch the method
        }
    },
    broadcastNotify: function (cmp, source, config) {
        var scope         = config.scope,
            currentTarget = config.currentTarget || this.getContainerPanelId(source),
            stack         = this.PANELS_STACK,
            owner         = this.PANELS_OWNER[currentTarget],
            panel, content, i;

        $A.assert(!scope || (scope !== 'all' || scope !== 'owner' || scope !== "self"), 'Invalid target (all || owner)');

        if (scope === 'all') {
            for (i = stack.length - 1; i >= 0; --i) {
                panel = stack[i];
                if (currentTarget !== panel.getGlobalId()) { // Dont notify itself
                	content = panel.get('v.body')[0];
                    this.notifyPanelContent(content, config);
                }
            }
        } else if (scope === 'self') {
        	if (currentTarget) {
                currentTarget = $A.getComponent(currentTarget);
                this.notifyPanelContent(currentTarget.get('v.body')[0], config);
        	}
        } else if (owner) {
            var ownerCmp = $A.getComponent(owner);
            if (ownerCmp && ownerCmp.isValid()) {
            	this.notifyPanelContent(ownerCmp, config);
            }
        }
    },
    getContainerPanelId: function (source) {
        var provider = source;
        while (provider) {
            if (provider.isInstanceOf("ui:panelType")) {
                return provider.getGlobalId();
            }
            source = provider.getAttributeValueProvider();
            provider = source !== provider ? source : null;
        }
    }
})// eslint-disable-line semi
<|endoftext|>


<commit_before>(function () {

    Script.include("/~/system/libraries/utils.js");

    var enabled = false;

    var animStateHandlerID;
    var currentVal = 1;

    var lerpDelta = 0.01;
    var lerpInterval = null;
    var unlerpInterval = null;

    var ZERO = 0;
    var ONE = 1;
    var SECS_TO_MS = 1000;
    var START_FASTER = 2;

    var OVERRIDDEN_DRIVE_KEYS = [
        DriveKeys.TRANSLATE_X,
        DriveKeys.TRANSLATE_Y,
        DriveKeys.TRANSLATE_Z,
        DriveKeys.STEP_TRANSLATE_X,
        DriveKeys.STEP_TRANSLATE_Y,
        DriveKeys.STEP_TRANSLATE_Z
    ];

    function animStateHandler(props) {
        return { ikOverlayAlpha: currentVal };
    }

    function setUp() {

        animStateHandlerID = MyAvatar.addAnimationStateHandler(animStateHandler, ["ikOverlayAlpha"]);
    }

    function enable(dt) {
        currentVal = ONE;

        enabled = true;

        lerpInterval = Script.setInterval(function () {
            currentVal = currentVal - lerpDelta;
            if (currentVal <= ZERO) {
                currentVal = ZERO;
                cleanUpLerp();
            }
        }, dt * SECS_TO_MS / START_FASTER);
    }

    function disable(dt) {
        enabled = false;
        cleanUpLerp();

        unlerpInterval = Script.setInterval(function () {
            currentVal = currentVal + lerpDelta;
            if (currentVal >= ONE) {
                currentVal = ONE;
                cleanUpUnLerp();
            }
        }, dt * SECS_TO_MS);

    }

    function cleanUpLerp() {
        if (lerpInterval !== null) {
            Script.clearInterval(lerpInterval);
            lerpInterval = null;
        }
    }

    function cleanUpUnLerp() {
        if (unlerpInterval !== null) {
            Script.clearInterval(unlerpInterval);
            unlerpInterval = null;
        }
    }

    function update(dt) {

        var areHipsInPucks = Controller.getPoseValue(Controller.Standard.Hips).valid;

        if (areHipsInPucks) {

            var hasActiveDriveKey = false;

            for (var i in OVERRIDDEN_DRIVE_KEYS) {
                if (MyAvatar.getRawDriveKey(OVERRIDDEN_DRIVE_KEYS[i]) !== 0.0) {
                    hasActiveDriveKey = true;
                    break;
                }
            }

            if (hasActiveDriveKey && enabled) {
                // pressing drive key and mappings enabled
                // do nothing
                return;
            } else if (hasActiveDriveKey && !enabled) {
                // pressing drive key and mappings NOT enabled
                // enable mappings
                enable(dt);
            } else if (!hasActiveDriveKey && enabled) {
                // NOT pressing drive key and mappings enabled
                // disable mappings
                disable(dt);
            }
        }
    }

    setUp();
    Script.update.connect(update);

    Script.scriptEnding.connect(function () {
        MyAvatar.removeAnimationStateHandler(animStateHandlerID);
        cleanUpLerp();
    });

})();<commit_msg>added script instructions<commit_after>/*
Created by Robin Wilson June 2018

Script allows users in full body trackers to utilize High Fidelity's avatar
movement animations.

Instructions:
1. Load and run script before or after calibrating Hifi's full body tracking
2. Hold down any directional movement button on the controllers and the avatar
will smoothly move into the default walk/run animation
3. Release and the avatar's hands will smoothly move back into your tracker positions
*/

(function () {

    Script.include("/~/system/libraries/utils.js");

    var enabled = false;

    var animStateHandlerID;
    var currentVal = 1;

    var lerpDelta = 0.01;
    var lerpInterval = null;
    var unlerpInterval = null;

    var ZERO = 0;
    var ONE = 1;
    var SECS_TO_MS = 1000;
    var START_FASTER = 2;

    var OVERRIDDEN_DRIVE_KEYS = [
        DriveKeys.TRANSLATE_X,
        DriveKeys.TRANSLATE_Y,
        DriveKeys.TRANSLATE_Z,
        DriveKeys.STEP_TRANSLATE_X,
        DriveKeys.STEP_TRANSLATE_Y,
        DriveKeys.STEP_TRANSLATE_Z
    ];

    function animStateHandler(props) {
        return { ikOverlayAlpha: currentVal };
    }

    function setUp() {

        animStateHandlerID = MyAvatar.addAnimationStateHandler(animStateHandler, ["ikOverlayAlpha"]);
    }

    function enable(dt) {
        currentVal = ONE;

        enabled = true;

        lerpInterval = Script.setInterval(function () {
            currentVal = currentVal - lerpDelta;
            if (currentVal <= ZERO) {
                currentVal = ZERO;
                cleanUpLerp();
            }
        }, dt * SECS_TO_MS / START_FASTER);
    }

    function disable(dt) {
        enabled = false;
        cleanUpLerp();

        unlerpInterval = Script.setInterval(function () {
            currentVal = currentVal + lerpDelta;
            if (currentVal >= ONE) {
                currentVal = ONE;
                cleanUpUnLerp();
            }
        }, dt * SECS_TO_MS);

    }

    function cleanUpLerp() {
        if (lerpInterval !== null) {
            Script.clearInterval(lerpInterval);
            lerpInterval = null;
        }
    }

    function cleanUpUnLerp() {
        if (unlerpInterval !== null) {
            Script.clearInterval(unlerpInterval);
            unlerpInterval = null;
        }
    }

    function update(dt) {

        var areHipsInPucks = Controller.getPoseValue(Controller.Standard.Hips).valid;

        if (areHipsInPucks) {

            var hasActiveDriveKey = false;

            for (var i in OVERRIDDEN_DRIVE_KEYS) {
                if (MyAvatar.getRawDriveKey(OVERRIDDEN_DRIVE_KEYS[i]) !== 0.0) {
                    hasActiveDriveKey = true;
                    break;
                }
            }

            if (hasActiveDriveKey && enabled) {
                // pressing drive key and mappings enabled
                // do nothing
                return;
            } else if (hasActiveDriveKey && !enabled) {
                // pressing drive key and mappings NOT enabled
                // enable mappings
                enable(dt);
            } else if (!hasActiveDriveKey && enabled) {
                // NOT pressing drive key and mappings enabled
                // disable mappings
                disable(dt);
            }
        }
    }

    setUp();
    Script.update.connect(update);

    Script.scriptEnding.connect(function () {
        MyAvatar.removeAnimationStateHandler(animStateHandlerID);
        cleanUpLerp();
    });

})();<|endoftext|>