Source: stash.js

/* 
 * Distributed 2015 by the Smart TV Alliance. All rights reserved. 
 * LICENSE: Apache License, Version 2.0, http://www.apache.org/licenses/LICENSE-2.0 
 */

/*
 * (C) Copyright IBM Corp. 2013, 2015  
 */

/**
 * STASH Library - SmartTV Alliance Smart Home Libary
 * http://www.smarttv-alliance.org/ 
 */
window.stash = (function() {

	function Stash() {
	}

	/*
	 * STASH: Helper functionality
	 */
	function clone(obj) {
		if (null == obj || "object" != typeof obj)
			return obj;
		var copy = obj.constructor();
		for ( var attr in obj) {
			if (obj.hasOwnProperty(attr))
				copy[attr] = obj[attr];
		}
		return copy;
	}

	function getNamedItem(list, name) {
		if (typeof (list) == 'undefined' || list == null
				|| typeof (name) == 'undefined' || name == null) {
			return null;
		}

		for ( var prop in list) {
			if (list.hasOwnProperty(prop) && prop != null) {
				var pObj = list[prop];
				if (pObj.name === name) {
					return pObj;
				}
			}
		}
		return null;
	}

	function loadHistoryFromLocalStorage(ep, existingDevice) {
		if (typeof (stash) == 'undefined'
				|| stash.enableHistoryToLocalStore == false) {
			return;
		}

		if (typeof (ep) == 'undefined' || ep == null
				|| typeof (existingDevice) == 'undefined'
				|| existingDevice == null) {
			return;
		}

		log("stash.loadHistoryFromLocalStorage", ep, existingDevice);

		// try to load from localStorage
		var history = {};
		if (typeof (localStorage.history) != 'undefined'
				&& localStorage.history != null) {
			try {
				history = JSON.parse(localStorage.history);
			} catch (e) {
			}
		}

		log("stash.loadHistoryFromLocalStorage", history);

		if (history == null
				|| typeof (history[ep.name]) == 'undefined'
				|| history[ep.name] == null
				|| typeof (history[ep.name][existingDevice.name]) == 'undefined'
				|| history[ep.name][existingDevice.name] == null) {
			return;
		}

		var timestampSort = function(a, b) {
			if (a.timestamp < b.timestamp)
				return -1;
			if (a.timestamp > b.timestamp)
				return 1;
			return 0;
		};

		log("stash.loadHistoryFromLocalStorage: looping");

		// loop through all properties and load the history
		// object
		for ( var prop in existingDevice.properties) {
			var property = existingDevice.properties[prop];
			if (typeof (history[ep.name][existingDevice.name][property.name]) != 'undefined'
					&& history[ep.name][existingDevice.name][property.name] != null) {
				if (typeof (property.history) != 'undefined'
						&& property.history != null) {
					// do not replace but merge, even if this means
					// computational effort

					// assumption is that to a give timestamp only a single
					// value can be there, so create an object to clear out
					// duplicates
					var timestamped = {};
					property.history.forEach(function(entry) {
						timestamped[entry.timestamp] = clone(entry);
					});
					history[ep.name][existingDevice.name][property.name]
							.forEach(function(entry) {
								timestamped[entry.timestamp] = clone(entry);
							});

					log("stash.loadHistoryFromLocalStorage", timestamped);

					// directly work on the history object from now
					property.history = [];

					// collect it back ...
					for ( var t in timestamped) {
						property.history.push(timestamped[t]);
					}

					// ... and sort
					property.history.sort(timestampSort);

					// save it directly back to localStore?
				} else {
					// plain replace as nothing available yet
					property.history = clone(history[ep.name][existingDevice.name][property.name]);
				}
			}
		}
	}

	function storeHistoryToLocalStorage(ep, existingDevice) {
		if (typeof (stash) == 'undefined' || stash.enableHistory == false) {
			return;
		}

		if (typeof (existingDevice) == 'undefined' || existingDevice == null) {
			return;
		}

		// try to load from localStorage
		var history = {};
		if (typeof (localStorage.history) != 'undefined'
				&& localStorage.history != null) {
			try {
				history = JSON.parse(localStorage.history);
			} catch (e) {
			}
		}

		// loop through all properties and store the history
		// objects
		for ( var prop in existingDevice.properties) {
			var property = existingDevice.properties[prop];
			if (typeof (history[ep.name]) == 'undefined'
					|| history[ep.name] == null) {
				history[ep.name] = {};
			}
			if (typeof (history[ep.name][existingDevice.name]) == 'undefined'
					|| history[ep.name][existingDevice.name] == null) {
				history[ep.name][existingDevice.name] = {};
			}
			history[ep.name][existingDevice.name][property.name] = clone(property.history);
		}

		// save back to localStorage
		localStorage.history = JSON.stringify(history);
	}

	function mergeProperties(subProps, baseProps) {
		var newProps = baseProps;
		if (subProps) {
			for ( var prop in subProps) {
				if (subProps.hasOwnProperty(prop) && prop != null) {
					var pObj = subProps[prop];
					var bObj = getNamedItem(baseProps, pObj.name);
					if (!bObj) {
						newProps.push(pObj);
					} else {
						// TODO probably remove!
						// merge content if the same type
						if (pObj.is === bObj.is) {
							// merge display
							if (pObj.display !== bObj.display) {
								// split by
								var pOptions = pObj ? pObj.display.split(":")
										: [];
								var bOptions = bObj ? bObj.display.split(":")
										: [];
								for (var i = 0; i < bOptions.length; i++) {
									var entry = bOptions[i];
									var index = -1;
									for (var j = 0; j < pOptions.length; j++) {
										if (pOptions[j] == entry) {
											index = j;
										}
									}
									if (index == -1) {
										pOptions.push(entry);
									}
								}
								pObj.display = pOptions.join(":");
							}
						}
					}
				}
			}
		}

		return newProps;
	}

	function extend(base, sub) {
		var subProperties = sub.prototype.properties;
		sub.prototype = new base;
		var baseProperties = sub.prototype.properties;
		// merge properties
		sub.prototype.properties = mergeProperties(subProperties,
				baseProperties);
		sub.prototype.constructor = sub;
		sub.constructor = base.prototype.constructor;
	}

	function log() {
		// only log when debug is enabled
		if (typeof (stash) != 'undefined' && stash.debug && window.console
				&& console.log) {
			Array.prototype.unshift.call(arguments, new Date().toISOString()
					.replace(/T/, ' ').replace(/\..+/, '')
					+ ':');
			console.log.apply(console, arguments);
		}
	}

	function createEndpoint(name, endpointAddress, version) {
		if (typeof (name) == 'undefined'
				|| typeof (endpointAddress) == 'undefined'
				|| typeof (version) == 'undefined') {
			throw new Error('Parameters for adding endpoint not valid!');
		}

		// verify that name is not used
		for ( var epi in endpoints) {
			if (endpoints[epi].name == name) {
				throw new Error('Endpoint name already taken!');
			}
		}

		var newEp = null;
		if (version == "obix.v1") {
			newEp = new EndpointObixV1(name, endpointAddress);
		} else if (version == "obix.v2") {
			newEp = new EndpointObixV2(name, endpointAddress);
		} else if (version == "simple.v1") {
			newEp = new EndpointSimpleV1(name, endpointAddress);
		} else {
			throw new Error('Unsupported Endpoint version ' + version + '!');
		}
		return newEp;
	}

	/*
	 * STASH: Data model of the appliance types and property data types
	 */
	/**
	 * @namespace Property
	 * @constructor
	 * @param {string}
	 *            obix The obix class name.
	 * @param {string}
	 *            name The property name.
	 * @param {string}
	 *            displayName The display name.
	 * @param {string}
	 *            value The value (if already available).
	 * @param {string}
	 *            display The possible display.
	 * @param {boolean}
	 *            writeable true, if this property is writable else false
	 * @param {string}
	 *            min The minimum value (if applicable).
	 * @param {string}
	 *            max The maximum value (if applicable).
	 * @param {string}
	 *            unit The unit of the value (if applicable).
	 */
	var Property = function Property(obix, name, displayName, value, display,
			writeable, min, max, unit) {

		this.obix = obix || "obj";
		// no history for the initial definition of the property
		this.val = value || null;
		this.name = name || null;
		this.displayName = displayName || name || null;
		this.display = display || null;
		this.writeable = writeable || true;
		this.min = min || null;
		this.max = max || null;
		this.unit = unit || null;

		// the library can optionally also store a history of device data
		this.history = [];

		// parses a JSON Object which contains properties (OBIX specification)
		// to a property object
		this.parse = function(jsonObject) {
			if (jsonObject.hasOwnProperty("name")) {
				this.name = jsonObject.name;
			}
			if (jsonObject.hasOwnProperty("displayName")) {
				this.displayName = jsonObject.displayName;
			} else {
				// fallback if there is no separate display name
				this.displayName = this.name;
			}
			if (jsonObject.hasOwnProperty("val")) {
				this.setValue(jsonObject.val, jsonObject.timestamp);
			}
			if (jsonObject.hasOwnProperty("display")) {
				this.display = jsonObject.display;
			}
			if (jsonObject.hasOwnProperty("writeable")) {
				this.writeable = jsonObject.writeable;
			}
			if (jsonObject.hasOwnProperty("min")) {
				this.min = jsonObject.min;
			}
			if (jsonObject.hasOwnProperty("max")) {
				this.max = jsonObject.max;
			}
			if (jsonObject.hasOwnProperty("unit")) {
				this.unit = jsonObject.unit;
			}
		};

		// support the history
		this.setValue = function(value, timestamp) {
			if (typeof (timestamp) == 'undefined' || timestamp == null) {
				timestamp = Math.floor(new Date().getTime() / 1000);
			}

			if (stash.enableHistory) {
				// TODO can it also support "average"?
				this.history.push({
					"value" : value,
					"timestamp" : timestamp
				});
			}
			this.val = value;
		};

	};

	/**
	 * @namespace Str
	 * @description Denotes a String property
	 * @constructor
	 * @extends Property
	 */
	var Str = function Str(name, displayName, value, display, writeable, min,
			max) {
		return new Property("str", name, displayName, value, display,
				writeable, min, max);
	};

	/**
	 * @namespace Int
	 * @description Denotes a property of type Integer
	 * @constructor
	 * @extends Property
	 */
	var Int = function Int(name, displayName, value, display, writeable, min,
			max, unit) {
		return new Property("int", name, displayName, value, display,
				writeable, min, max, unit);
	};

	/**
	 * @namespace Double
	 * @description Denotes a property of type Double
	 * @constructor
	 * @extends Property
	 */
	var Double = function Double(name, displayName, value, display, writeable,
			min, max, unit) {
		return new Property("double", name, displayName, value, display,
				writeable, min, max, unit);
	};

	/**
	 * @namespace Real
	 * @description Denotes a property of type Real
	 * @constructor
	 * @extends Property
	 */
	var Real = function Real(name, displayName, value, display, writeable, min,
			max, unit) {
		return new Property("real", name, displayName, value, display,
				writeable, min, max, unit);
	};

	/**
	 * @namespace Bool
	 * @description Denotes a boolean property
	 * @constructor
	 * @extends Property
	 */
	var Bool = function Bool(name, displayName, value, display, writeable) {
		return new Property("bool", name, displayName, value, display,
				writeable);
	};

	/**
	 * @namespace Obj
	 * @description Denotes the base object class for all device types
	 * @constructor
	 */
	var Obj = function Obj(name, displayName, is, location) {
		this.name = name || "";
		this.displayName = displayName || name || null;
		this.is = is || "";
		this.location = location || "";
	};

	// base device object
	/**
	 * @namespace Device
	 * @description Base device class, which is bound to an Endpoint
	 * @constructor
	 * @extends Obj
	 */
	var Device = function Device(name, displayName, is, location) {
		this.ep = null;

		// for the simple protocol define a pendingUpdates queue
		this.pendingUpdates = {};

		// TODO NLS
		this.propertyConstraints = [
				Bool("status", "status", null, null, false),
				Real("power", "power", null, null, false),
				Str("name", "name", null, null, false),
				Str("location", "location", null, null, false),
				Real("energy", "energy", null, null, false),
				Str("error", "error", null, null, false),
				Str("vendorCode", "vendorCode", null, null, false) ];

		this.getProperty = function(propName) {
			return getNamedItem(this.properties, propName);
		};

		// update full device to handle history correctly
		this.updateDevice = function(device) {

			// base Obj properties
			if (this.is != device.is) {
				this.is = device.is;
			}
			if (this.displayName != device.displayName) {
				this.displayName = device.displayName;
			}
			if (this.location != device.location) {
				this.location = device.location;
			}

			// device properties
			for ( var p in device.properties) {
				var dProperty = device.properties[p];
				var property = this.getProperty(dProperty.name);
				if (property == null) {
					this.properties.push(dProperty);
				} else {
					// do "historic" update
					if (dProperty.val != property.val) {
						property.setValue(dProperty.val);
					}
				}
			}
		};

		// to support simple protocol updates which provides a list of
		// acknowledges of the given transaction id
		this.updateProperties = function(pendingTid, pendingProperties) {
			if (pendingTid && pendingProperties
					&& typeof (this.pendingUpdates[pendingTid]) != 'undefined') {

				log("Device.updateProperties: looking in pendingUpdates for "
						+ pendingTid + " and " + pendingProperties,
						this.pendingUpdates[pendingTid]);

				// look for the given properties
				for (p in pendingProperties) {
					if (typeof (this.pendingUpdates[pendingTid][pendingProperties[p]]) != 'undefined') {
						var property = this.getProperty(pendingProperties[p]);
						if (property != null) {
							property
									.setValue(this.pendingUpdates[pendingTid][pendingProperties[p]]);
						} else {
							log("Device.updateProperties: could not find property "
									+ p + ", status not updated!");
						}
					}
				}

				// remove tid row
				delete this.pendingUpdates[pendingTid];
			}
		};

		/**
		 * Returns the history of a property.
		 * 
		 * @function Device~getHistory
		 * 
		 * @param {string}
		 *            propName The property name.
		 * @param {string}
		 *            start The timestamp which defines the beginning of the
		 *            returned history.
		 * @param {string}
		 *            end The timestamp which defines the ending of the returned
		 *            history.
		 * @param {string}
		 *            [aggregation] Currently ignored.
		 * @param {string}
		 *            [scope] Currently ignored.
		 * @returns {History}
		 */
		this.getHistory = function(propName, start, end, aggregation, scope) {
			// TODO currently we are ignoring aggregation and scope
			if (typeof (propName) == 'undefined'
					|| typeof (start) == 'undefined'
					|| typeof (end) == 'undefined') {
				return {};
			}

			var property = this.getProperty(propName);
			if (property != null) {
				// TODO that is slow, but as the list is currently not supported
				// it cannot be done faster
				var history = [];
				for ( var entry in property.history) {
					if (property.history[entry].timestamp >= start
							&& property.history[entry].timestamp <= end) {
						history.push(property.history[entry]);
					}
				}

				return {
					"start" : start,
					"end" : end,
					"aggregation" : aggregation,
					items : history
				};

			} else {
				log("Device.getHistory: could not find property " + p
						+ ", status not updated!");
			}

			return {};
		};

		return this;
	};
	extend(Obj, Device);

	/**
	 * @namespace Washer
	 * @description Washer
	 * @extends Device
	 */
	var Washer = function Washer() {
		this.is = "sta:Washer";
		this.className = "Washer";
	};
	extend(Device, Washer);

	/**
	 * @namespace Dryer
	 * @description Dryer
	 * @extends Device
	 */
	var Dryer = function Dryer() {
		this.is = "sta:Dryer";
		this.className = "Dryer";
	};
	extend(Device, Dryer);

	/**
	 * @namespace WasherDryerCombo
	 * @description WasherDryerCombo
	 * @extends Washer
	 * @extends Dryer
	 */
	var WasherDryerCombo = function WasherDryerCombo() {
		this.is = "sta:WasherDryerCombo";
		this.className = "WasherDryerCombo";
	};
	extend(Washer, WasherDryerCombo);
	extend(Dryer, WasherDryerCombo);

	/**
	 * @namespace AirConditioner
	 * @description AirConditioner
	 * @extends Device
	 */
	var AirConditioner = function AirConditioner() {
		this.is = "sta:AirConditioner";
		this.className = "AirConditioner";

		this.propertyConstraints = [
				Int("targetTemperature", "targetTemperature", null, null, false),
				Int("operationMode", "operationMode", null, null, false) ];
	};
	extend(Device, AirConditioner);

	/**
	 * @namespace Refrigerator
	 * @description Refrigerator
	 * @extends Device
	 */
	var Refrigerator = function Refrigerator() {
		this.is = "sta:Refrigerator";
		this.className = "Refrigerator";

		this.propertyConstraints = [ Double("targetTemperature",
				"targetTemperature", null, null, false) ];

	};
	extend(Device, Refrigerator);

	/**
	 * @namespace Cleaner
	 * @description Cleaner
	 * @extends Device
	 */
	var Cleaner = function Cleaner() {
		this.is = "sta:Cleaner";
		this.className = "Cleaner";
	};
	extend(Device, Cleaner);

	/**
	 * @namespace Light
	 * @description Light
	 * @extends Device
	 */
	var Light = function Light() {
		this.is = "sta:Light";
		this.className = "Light";
	};
	extend(Device, Light);

	var deviceTypes = [ new Washer(), new Dryer(), new WasherDryerCombo(),
			new AirConditioner(), new Refrigerator(), new Cleaner(),
			new Light() ];

	/*
	 * STASH: Endpoint is the abstraction for an appliance, a gateway or a cloud
	 * service which supports the STASH protocol over WebSocket. There are three
	 * variants of endpoints: 1) obix.v1: support LG devices for CES2014 2)
	 * obix.v2: simplified obix as in specification draft 3) simple.v1: simple
	 * protocol as in specification draft, supports Toshiba devices for CES2014
	 */

	/* obix.v1 */
	var EndpointObixV1 = function EndpointObixV1(name, endpointAddress) {
		this.name = name;
		this.endpointAddress = endpointAddress;
		this.reqId = 0;
		this.disconnected = false;
		this.ondeviceupdate = null;
		this.devices = [];
		var that = this;

		// parses a JSON Object which contains devices (OBIX specification) to a
		// list of device objects
		this.parseDevice = function(device, jsonObject) {
			if (typeof (device) == 'undefined' || device == null
					|| typeof (jsonObject) == 'undefined' || jsonObject == null) {
				return;
			}
			device.name = jsonObject.name;
			device.displayName = jsonObject.displayName || jsonObject.name
					|| null;
			device.is = jsonObject.is;
			device.location = jsonObject.location;
			device.properties = [];

			for (var i = 0; i < jsonObject.children.length; i++) {
				var newProp = null;
				if (jsonObject.children[i].obix.toLowerCase() == "int") {
					newProp = new Int();
					newProp.parse(jsonObject.children[i]);
				} else if (jsonObject.children[i].obix.toLowerCase() == "bool") {
					newProp = new Bool();
					newProp.parse(jsonObject.children[i]);
				} else if (jsonObject.children[i].obix.toLowerCase() == "str") {
					newProp = new Str();
					newProp.parse(jsonObject.children[i]);
				} else if (jsonObject.children[i].obix.toLowerCase() == "real") {
					newProp = new Real();
					newProp.parse(jsonObject.children[i]);
				} else {
					log("EndpointObixV1.parseDevice: Could not parse element",
							jsonObject.children[i]);
				}
				if (typeof (newProp) != 'undefined' && newProp != null) {
					device.properties.push(newProp);
				}
			}
		};

		this.websocketOnclose = function(evt) {
			log("EndpointObixV1.websocketOnclose was called, callback present? "
					+ (that.disconnected != null));
			if (!that.disconnected) {
				that.connect();
			}
		};

		this.websocketOnmessage = function(evt) {
			try {
				log(evt);
				var jsonObject = JSON.parse(evt.data);
				if (jsonObject.is != null && jsonObject.is == "obix:Lobby") {
					that.reqId++;
					var request = {
						"obix" : "obj",
						"is" : "obix:Request",
						"rid" : that.reqId,
						"children" : [ {
							"obix" : "op",
							"name" : "add",
							"is" : "obix:Watch",
							"children" : [ {
								"obix" : "obj",
								"is" : "obix:WatchIn",
								"children" : [ {
									"obix" : "list",
									"name" : "hrefs",
									"children" : [ {
										"obix" : "uri",
										"val" : "/device/"
									} ]
								} ]
							} ]
						} ]
					};
					var message = "{\"obix\":\"obj\",\"is\":\"obix:Request\",\"rid\":\""
							+ that.reqId
							+ "\",\"children\":[{\"obix\":\"op\",\"name\":\"add\",\"is\":\"obix:Watch\","
							+ "\"children\":[{\"obix\":\"obj\",\"is\":\"obix:WatchIn\",\"children\":[{\"obix\":\"list\","
							+ "\"name\":\"hrefs\",\"children\":[{\"obix\":\"uri\",\"val\":\"/device/\"}]}]}]}]}";
					log("DEBUG! comparing " + message + "? "
							+ (JSON.stringify(request) === message));
					that.websocket.send(message);
				}
				if (jsonObject.rid != null && jsonObject.is != null
						&& jsonObject.rid == that.reqId
						&& jsonObject.is == "obix:Response") {
					that.devices = [];
					for (var i = 0; i < jsonObject.children[0].children.length; i++) {
						var newDevice = new Device();
						try {
							that.parseDevice(newDevice,
									jsonObject.children[0].children[i]);
						} catch (e) {
							// if there was an error during parsing ignore that
							// device
							log("EndpointObixV1.websocketOnmessage: " + e);
							continue;
						}
						newDevice.ep = that;
						that.devices.push(newDevice);

						// as it is a new device merge the history
						loadHistoryFromLocalStorage(that, newDevice);
					}
					if (typeof (that.onconnect) != 'undefined'
							&& that.onconnect != null) {
						log(
								"EndpointObixV1.websocketOnmessage: onconnect is defined",
								that.devices);
						that.onconnect();
					}
				}
				if (jsonObject.is != null && jsonObject.is == "obix:Update") {
					var nDevice = new Device();
					try {
						that.parseDevice(nDevice,
								jsonObject.children[0].children[0].children[0]);
					} catch (e) {
						// if there was an error during parsing, ignore that
						// device
						log("EndpointSimpleV1.websocketOnmessage: " + e);
						return;
					}
					var existingDevice = getNamedItem(that.devices,
							nDevice.name);
					if (existingDevice == null) {
						that.devices.push(nDevice);

						// as it is a new device merge the history
						loadHistoryFromLocalStorage(that, nDevice);
					} else {
						nDevice.ep = that;
						existingDevice.updateDevice(nDevice);
					}

					try {
						that.ondeviceupdate(clone(nDevice));
					} catch (e) {
						log("EndpointObixV1.websocketOnmessage.ondeviceupdate",
								e);
					}

					// save history to localStore
					existingDevice = getNamedItem(that.devices, nDevice.name);
					if (existingDevice != null) {
						storeHistoryToLocalStorage(that, existingDevice);
					}
				}
			} catch (e) {
				log("EndpointObixV1.websocketOnmessage", e);
			}
		};
	};

	/**
	 * @namespace EndpointObixV1
	 */
	EndpointObixV1.prototype = {

		/* no-op but defined to stay compatible with api */
		poll : function() {
		},

		/**
		 * Returns the devices of an endpoint.
		 * 
		 * @function EndpointObixV1~getDevices
		 * 
		 * @returns {Array}
		 */
		getDevices : function() {
			return this.devices;
		},

		/**
		 * Closes the connection to an endpoint.
		 * 
		 * @function EndpointObixV1~disconnect
		 */
		disconnect : function() {
			this.disconnected = true;
			try {
				log("EndpointObixV1.disconnect: disconnecting...");
				this.websocket.close();
				log("EndpointObixV1.disconnect: disconnected");
			} catch (e) {
				log("EndpointObixV1.disconnect", e);
			}
		},

		/**
		 * Opens the connection to an endpoint.
		 * 
		 * @function EndpointObixV1~connect
		 * 
		 * @param {function}
		 *            callback A function which will be executed directly after
		 *            the connection is open.
		 * @param {function}
		 *            errorcallback A function which will be executed if the
		 *            connection fails.
		 */
		connect : function(callback, errorcallback) {
			try {
				this.onconnect = callback;
				this.websocket = new WebSocket(this.endpointAddress);
				this.websocket.onmessage = this.websocketOnmessage;
				this.websocket.onclose = this.websocketOnclose;
				var that = this;
				this.websocket.onerror = function() {
					that.disconnected = true;
					if (typeof (errorcallback) != 'undefined'
							&& errorcallback != null) {
						errorcallback();
					}
				};
			} catch (e) {
				log("EndpointObixV1.connect", e);
			}
		},

		/**
		 * Sets a property value of a device.
		 * 
		 * @function EndpointObixV1~setProperty
		 * 
		 * @param {string}
		 *            name The device name.
		 * @param {string}
		 *            propName The property name.
		 * @param value
		 *            The new value.
		 * 
		 */
		setProperty : function(name, propName, value) {
			if (typeof (name) == 'undefined' || name == null
					|| typeof (propName) == 'undefined' || propName == null
					|| typeof (value) == 'undefined') {
				log("EndpointObixV1.setProperty: invalid parameters given!");
				return;
			}

			var type = null;
			var href = null;
			for ( var d in this.devices) {
				var device = this.devices[d];
				if (device.name == name) {
					log("EndpointObixV1.setProperty: device " + name + " found");
					for (p in device.properties) {
						var property = device.properties[p];
						if (property.name == propName) {
							type = property.obix;
							log("EndpointObixV1.setProperty: property "
									+ propName + " found with type " + type);

							// verify min / max
							if (typeof (property.min) != 'undefined'
									&& property.min != null) {
								if (value < property.min) {
									throw "Property " + propName
											+ " cannot be set to " + value
											+ " as below min!";
								}
							}
							if (typeof (property.max) != 'undefined'
									&& property.max != null) {
								if (value > property.max) {
									throw "Property " + propName
											+ " cannot be set to " + value
											+ " as above max!";
								}
							}

							break;
						}
					}
					href = device.href;
					break;
				}
			}

			// TODO that seems incorrect as one children level is missing
			var request = {
				"obix" : "obj",
				"is" : "obix:Request",
				"rid" : this.reqId,
				"children" : [ {
					"obix" : "obj",
					"href" : href,
					"name" : name
				}, {
					"obix" : type,
					"name" : propName,
					"value" : value
				} ]
			};
			var jsonString = "{\"obix\":\"obj\",\"is\":\"obix:Request\",\"rid\":\""
					+ this.reqId
					+ "\",\"children\":"
					+ "[{\"obix\":\"obj\",\"href\":\"/device/"
					+ name
					+ "\",\"name\":\""
					+ name
					+ "\"},{\"obix\":\""
					+ type
					+ "\",\"name\":\""
					+ propName
					+ "\",\"value\":"
					+ value
					+ "}]}";
			log("EndpointObixV1.setProperty: sending json " + jsonString);
			log("DEBUG! comparing " + jsonString + " "
					+ JSON.stringify(request));
			this.websocket.send(jsonString);
		}

	};

	/* obix.v2 * */
	var EndpointObixV2 = function EndpointObixV2(name, endpointAddress) {

		// creates an endpoint with name and address
		this.name = name;
		this.endpointAddress = endpointAddress;

		// request id counter to distinct requests
		this.reqId = 0;

		// request id arrays
		this.getDevicesRids = [];
		this.watchRids = [];

		// watch id to receive the unsolicited updates from
		this.watchId = 0;

		this.disconnected = false;

		// function which will be set by the client
		this.ondeviceupdate = null;

		// initial empty list of devices
		this.devices = [];

		var that = this;

		// parses a JSON Object which contains devices (OBIX specification) to a
		// list of device objects
		this.parseDevice = function(device, jsonObject) {
			if (typeof (device) == 'undefined' || device == null
					|| typeof (jsonObject) == 'undefined' || jsonObject == null) {
				return;
			}
			device.name = jsonObject.name;
			device.displayName = jsonObject.displayName || jsonObject.name
					|| null;
			device.is = jsonObject.is;
			device.href = jsonObject.href;
			device.location = jsonObject.location;
			device.properties = [];

			for (var i = 0; i < jsonObject.children.length; i++) {
				var newProp = null;
				if (jsonObject.children[i].obix.toLowerCase() == "int") {
					newProp = new Int();
					newProp.parse(jsonObject.children[i]);
				} else if (jsonObject.children[i].obix.toLowerCase() == "bool") {
					newProp = new Bool();
					newProp.parse(jsonObject.children[i]);
				} else if (jsonObject.children[i].obix.toLowerCase() == "str") {
					newProp = new Str();
					newProp.parse(jsonObject.children[i]);
				} else if (jsonObject.children[i].obix.toLowerCase() == "real") {
					newProp = new Real();
					newProp.parse(jsonObject.children[i]);
				} else {
					log("EndpointObixV2.parseDevice: Could not parse element",
							jsonObject.children[i]);
				}
				if (typeof (newProp) != 'undefined' && newProp != null) {
					device.properties.push(newProp);
				}
			}
		};

		this.websocketOnclose = function(evt) {
			// do automatic reconnect
			if (!that.disconnected) {
				that.connect();
			}
		};

		this.websocketOnmessage = function(evt) {
			try {
				log(
						"EndpointObixV2.websocketOnmessage: Received from Server: ",
						evt.data);
				var jsonObject = JSON.parse(evt.data);
				if (jsonObject.is != null && jsonObject.is == "obix:Lobby") {
					that.poll();
				}
				if (jsonObject.rid != null && jsonObject.is != null
						&& jsonObject.is == "obix:Response") {
					log("EndpointObixV2.websocketOnmessage: rid: ",
							jsonObject.rid, "getDevicesRids: ",
							that.getDevicesRids);
					var jsonObjectList = jsonObject.children[0];
					var getDevicesRidFound = false;
					var watchRidFound = false;
					for ( var i in that.getDevicesRids) {
						if (that.getDevicesRids[i] == jsonObject.rid) {
							getDevicesRidFound = true;
						}
					}
					for ( var j in that.watchRids) {
						if (that.watchRids[j] == jsonObject.rid) {
							watchRidFound = true;
						}
					}
					if (getDevicesRidFound) {
						// refresh devices information
						that.devices = [];
						if (jsonObjectList.children) {
							for (var i = 0; i < jsonObjectList.children.length; i++) {

								var newDevice = null;

								var foundSubType = false;
								for ( var dti in deviceTypes) {
									var dt = deviceTypes[dti];
									if (dt.is == jsonObjectList.children[i].is) {
										newDevice = dt;
										log(
												"EndpointObixV2.websocketOnmessage: device: ",
												newDevice);
										foundSubType = true;
										break;
									}
								}

								if (foundSubType == false) {
									newDevice = new Device();
								}

								try {
									that.parseDevice(newDevice,
											jsonObjectList.children[i]);
									log("parsing", newDevice);
								} catch (e) {
									log("parsing error", e);
									continue;
								}
								newDevice.ep = that;
								that.devices.push(newDevice);

								// as it is a new device merge the history
								loadHistoryFromLocalStorage(that, nDevice);
							}
						}
						if (typeof (that.onconnect) != 'undefined'
								&& that.onconnect != null) {
							that.onconnect();
						}
					}
					if (watchRidFound) {
						// add device to the created watch
						jsonObject = jsonObject.children[0];
						that.watchId = jsonObject.href.slice(7);
						that.reqId++;

						var request = {
							"obix" : "obj",
							"is" : "obix:Invoke",
							"rid" : that.reqId,
							"href" : "/watch/" + that.watchId + "/add",
							"children" : [ {
								"obix" : "obj",
								"is" : "obix:WatchIn",
								"children" : [ {
									"obix" : "list",
									"name" : "hrefs",
									"children" : [ {
										"obix" : "uri",
										"val" : "/device/"
									} ]
								} ]
							} ]
						};
						log(
								"EndpointObixV2.constructor: sending request over WebSocket",
								request);
						that.websocket.send(JSON.stringify(request));
					}

				}
				if (jsonObject.is != null && jsonObject.is == "obix:Update") {
					var nDevice = new Device();
					that.parseDevice(nDevice,
							jsonObject.children[0].children[0].children[0]);
					var existingDevice = getNamedItem(that.devices,
							nDevice.name);
					if (existingDevice == null) {
						that.devices.push(nDevice);

						// as it is a new device merge the history
						loadHistoryFromLocalStorage(that, nDevice);
					} else {
						nDevice.ep = that;
						existingDevice.updateDevice(nDevice);
					}

					if (typeof (that.ondeviceupdate) != 'undefined'
							&& that.ondeviceupdate != null) {
						try {
							that.ondeviceupdate(clone(nDevice));
						} catch (e) {
							log(
									"EndpointObixV2.websocketOnmessage.ondeviceupdate",
									e);
						}

						// save history to localStore if flag is set
						existingDevice = getNamedItem(that.devices,
								nDevice.name);
						if (existingDevice != null) {
							storeHistoryToLocalStorage(that, existingDevice);
						}
					}
				}
			} catch (e) {
				log("EndpointObixV2.websocketOnmessage", e);
			}
		};
	};

	/*
	 * External API for Endpoint: - poll - watch - getDevices - connect -
	 * disconnect - setProperty
	 */
	/**
	 * @namespace EndpointObixV2
	 */
	EndpointObixV2.prototype = {

		/**
		 * Refreshes the device list of an endpoint.
		 * 
		 * @function EndpointObixV2~poll
		 */
		poll : function() {
			this.reqId++;
			this.getDevicesRids.push(this.reqId);

			var request = {
				"obix" : "obj",
				"is" : "obix:Read",
				"rid" : this.reqId,
				"href" : "/device/"
			};
			log("EndpointObixV2.poll: sending request over WebSocket", request);
			this.websocket.send(JSON.stringify(request));
		},

		/**
		 * After running this function, the device list of an endpoint will be
		 * refreshed automatically, as soon as a device changes.
		 * 
		 * @function EndpointObixV2~watch
		 */
		watch : function() {
			this.reqId++;
			this.watchRids.push(this.reqId);

			var request = {
				"obix" : "obj",
				"is" : "obix:Invoke",
				"rid" : this.reqId,
				"href" : "/watchService/make"
			};
			log("EndpointObixV2.watch: sending request over WebSocket", request);
			this.websocket.send(JSON.stringify(request));
		},

		/**
		 * Returns the devices of an endpoint.
		 * 
		 * @function EndpointObixV2~getDevices
		 * 
		 * @returns {Array}
		 */
		getDevices : function() {
			return this.devices;
		},

		/**
		 * Closes the connection to an endpoint.
		 * 
		 * @function EndpointObixV2~disconnect
		 */
		disconnect : function() {
			this.disconnected = true;
			try {
				log("EndpointObixV2.disconnect: disconnecting...");
				this.websocket.close();
				log("EndpointObixV2.disconnect: disconnected");
			} catch (e) {
				log("EndpointObixV2.disconnect", e);
			}
		},

		/**
		 * Opens the connection to an endpoint.
		 * 
		 * @function EndpointObixV2~connect
		 * 
		 * @param {function}
		 *            callback A function which will be executed directly after
		 *            the connection is open.
		 * @param {function}
		 *            errorcallback A function which will be executed if the
		 *            connection fails.
		 */
		connect : function(callback, errorcallback) {
			try {
				this.onconnect = callback;
				this.websocket = new WebSocket(this.endpointAddress);
				this.websocket.onmessage = this.websocketOnmessage;
				this.websocket.onclose = this.websocketOnclose;
				var that = this;
				this.websocket.onerror = function() {
					that.disconnected = true;
					if (typeof (errorcallback) != 'undefined'
							&& errorcallback != null) {
						errorcallback();
					}
				};
			} catch (e) {
				log("EndpointObixV2.connect", e);
			}
		},

		/**
		 * Sets a property value of a device.
		 * 
		 * @function EndpointObixV2~setProperty
		 * 
		 * @param {string}
		 *            name The device name.
		 * @param {string}
		 *            propName The property name.
		 * @param value
		 *            The new value.
		 * 
		 */
		// sets the value of a property
		setProperty : function(name, propName, value) {
			if (typeof (name) == 'undefined' || name == null
					|| typeof (propName) == 'undefined' || propName == null
					|| typeof (value) == 'undefined') {
				log("EndpointObixV2.setProperty: invalid parameters given!");
				return;
			}
			var device = null;
			for ( var dti in deviceTypes) {
				var dt = deviceTypes[dti];
				if (dt.name == name) {
					device = dt;
					log("EndpointObixV2.websocketOnmessage: device: ", device);
					break;
				}
			}

			var property = device.getProperty(propName);
			if (property == null) {
				log("EndpointObixV2.setProperty Could not set property as "
						+ propName + " not found!");
				return;
			}

			var type = property.obix;

			// verify type, we only do min/max for int or real
			if (type == "int" || type == "real") {
				// verify that value is of that type and if needed parse it from
				// string
				if (typeof (value) == "string") {
					if (type == "int") {
						value = parseInt(value);
					} else if (type == "real") {
						value = parseFloat(value);
					}
					// throw an error if it is not a valid number
					if (isNaN(value)) {
						throw "Property " + propName + " cannot be set to "
								+ value + " as it is not a number!";
					}
				}

				// verify min / max
				if (typeof (property.min) != 'undefined' && property.min != null) {
					if (value < property.min) {
						throw "Property " + propName + " cannot be set to "
								+ value + " as below min!";
					}
				}
				if (typeof (property.max) != 'undefined' && property.max != null) {
					if (value > property.max) {
						throw "Property " + propName + " cannot be set to "
								+ value + " as above max!";
					}
				}
			}

			this.reqId++;

			var request = {
				"obix" : "obj",
				"is" : "obix:Write",
				"rid" : this.reqId,
				"href" : device.href,
				"children" : [ {
					"obix" : type,
					"name" : propName,
					"val" : value
				} ]
			};
			log("EndpointObixV2.setProperty: sending request over WebSocket",
					request);
			this.websocket.send(JSON.stringify(request));
		}
	};

	/* simple.v1 * */

	var EndpointSimpleV1 = function EndpointSimpleV1(name, endpointAddress) {
		this.name = name;
		this.reqId = 1;
		this.endpointAddress = endpointAddress;
		this.disconnected = false;
		this.ondeviceupdate = null;
		this.devices = [];
		var that = this;

		this.websocketOnclose = function(evt) {
			log("EndpointSimpleV1.websocketOnclose was called, check for reconnect? "
					+ (that.disconnected != null));
			if (!that.disconnected) {
				that.connect();
			}
		};

		// parses a JSON Object which contains a device (STA simple
		// specification) to a
		// device
		this.parseDevice = function(device, jsonObject) {
			if (typeof (device) == 'undefined' || device == null
					|| typeof (jsonObject) == 'undefined' || jsonObject == null) {
				throw "No device model or definition given";
			}

			// sample 1:
			// "deviceId": "ToshibaLEDCeilingLight",
			// "is": "LEDCeilingLight",
			// "vendorCode": "Toshiba",
			// "displayName": "My LED Ceiling Light",
			// "location": "Living Room"

			// sample 2:
			// "deviceId": "ToshibaLEDCeilingLight",
			// "attributes": { "status": false, "dimmingLevel": 50 }

			if (typeof (jsonObject.deviceId) == 'undefined'
					|| jsonObject.deviceId == null) {
				throw "No device id given";
			}

			device.name = jsonObject.deviceId;

			if (typeof (jsonObject.is) != "undefined") {
				device.is = jsonObject.is;
			}
			if (typeof (jsonObject.location) != "undefined") {
				device.location = jsonObject.location;
			}
			device.displayName = jsonObject.displayName || jsonObject.name
					|| null;

			device.vendorCode = jsonObject.vendorCode || null;

			// just add attributes to the device
			if (typeof (device.properties) != "undefined"
					|| device.properties == null) {
				device.properties = [];
			}

			// TODO CES limitation Toshiba light

			// You can call "getAttribute" with different "tid" multiple times.
			// However, each device has a different size of queue or a different
			// number of queue for incoming JSON requests. I believe that we
			// should define a flow control mechanism.

			// TODO getAttribute polling after one second
			// TODO status : bool (true or false, not 1 or 0)

			// device.displayName = "Toshiba LED Ceiling Leight"; // already
			// defined

			if (typeof (jsonObject.attributes) != "undefined"
					&& jsonObject.attributes != null) {
				for ( var key in jsonObject.attributes) {
					if (jsonObject.attributes.hasOwnProperty(key)) {
						var newProp = null;
						if ((isNaN(jsonObject.attributes[key]))
								&& jsonObject.attributes[key] != "false"
								&& jsonObject.attributes[key] != "true") {
							device.properties.push(new Str(key, key,
									jsonObject.attributes[key], null, null));
						} else if (!isNaN(jsonObject.attributes[key])) {
							device.properties.push(new Real(key, key,
									jsonObject.attributes[key], null, true, 1,
									100, null));
						} else if (jsonObject.attributes[key] == "false"
								|| jsonObject.attributes[key] == "true") {
							device.properties.push(new Bool(key, key,
									jsonObject.attributes[key], "false:true",
									true));
						} else {
							log(
									"EndpointObixV2.parseDevice: Could not parse element",
									jsonObject.attributes[key]);
						}
					}
				}
			}
		};

		this.websocketOnmessage = function(evt) {
			try {
				log("EndpointSimpleV1.websocketOnmessage parsing", evt);
				var jsonObject = JSON.parse(evt.data);
				log("EndpointSimpleV1.websocketOnmessage parsed", jsonObject);
				if (jsonObject.type != null && jsonObject.type == "response"
						&& jsonObject.payload) {
					if (jsonObject.payload.devices) {
						// that contains all devices
						that.devices = [];
						for (var i = 0; i < jsonObject.payload.devices.length; i++) {

							var newDevice = null;

							var foundSubType = false;
							for ( var dti in deviceTypes) {
								var dt = deviceTypes[dti];
								if (dt.is == jsonObject.payload.devices[i].is) {
									newDevice = dt;
									log(
											"EndpointObixV2.websocketOnmessage: device: ",
											newDevice);
									foundSubType = true;
									break;
								}
							}

							if (foundSubType == false) {
								newDevice = new Device();
							}

							try {
								that.parseDevice(newDevice,
										jsonObject.payload.devices[i]);
							} catch (e) {
								// if there was an error during parsing, ignore
								// that device
								log("EndpointSimpleV1.websocketOnmessage: " + e);
								continue;
							}

							newDevice.ep = that;
							that.devices.push(newDevice);

							// here no properties are delivered, meaning cannot
							// load here from history

							// send request for device details
							// TODO CES limitation!
							var request = {
								"type" : "request",
								"tid" : that.reqId,
								"payload" : {
									"deviceId" : newDevice.name,
									"getAttributes" : null
								}
							};

							try {
								log(
										"EndpointSimpleV1: sending request over WebSocket",
										request);
								that.websocket.send(JSON.stringify(request));
								that.reqId++;
							} catch (e) {
								log("EndpointSimpleV1.websocketOnmessage.1", e);
								continue;
							}

							if (typeof (that.ondeviceupdate) != 'undefined'
									&& that.ondeviceupdate != null) {
								try {
									// strip out everything before giving
									that.ondeviceupdate(newDevice);
								} catch (e) {
									log(
											"EndpointSimpleV1.websocketOnmessage.ondeviceupdate.1",
											e);
								}

								// save history to localStore
								var existingDevice = getNamedItem(that.devices,
										newDevice.name);
								if (existingDevice != null) {
									storeHistoryToLocalStorage(that,
											existingDevice);
								}
							}
						}
						if (typeof (that.onconnect) != 'undefined'
								&& that.onconnect != null) {
							log(
									"EndpointSimpleV1.websocketOnmessage: onconnect is defined, already have following devices:",
									that.devices);
							try {
								that.onconnect();
							} catch (e) {
								log("EndpointSimpleV1.websocketOnmessage.2", e);
							}
						}
					} else if (jsonObject.payload.deviceId
							&& jsonObject.payload.attributes) {
						// parse update
						var nDevice = new Device();
						try {
							that.parseDevice(nDevice, jsonObject.payload);
						} catch (e) {
							// if there was an error during parsing, ignore that
							// device
							log("EndpointSimpleV1.websocketOnmessage: " + e);
							return;
						}
						nDevice.ep = that;
						var existingDevice = getNamedItem(that.devices,
								nDevice.name);
						if (existingDevice == null) {
							nDevice.ep = that;
							that.devices.push(nDevice);

							// as it is a new device merge the history
							loadHistoryFromLocalStorage(that, nDevice);

						} else {
							// TODO merge sets?
							existingDevice.properties = nDevice.properties;
						}

						if (typeof (that.ondeviceupdate) != 'undefined'
								&& that.ondeviceupdate != null) {
							try {
								nDevice.ep = that;
								that.ondeviceupdate(clone(nDevice));
							} catch (e) {
								log(
										"EndpointSimpleV1.websocketOnmessage.ondeviceupdate.2",
										e);
							}

							// save history to localStore
							existingDevice = getNamedItem(that.devices,
									nDevice.name);
							if (existingDevice != null) {
								storeHistoryToLocalStorage(that, existingDevice);
							}
						}
					} else if (jsonObject.payload.deviceId
							&& jsonObject.payload.accepted) {
						// sample is "accepted": [ "status", "dimmingLevel" ]
						for ( var d in that.devices) {
							if (typeof (that.devices[d]) != 'undefined'
									&& that.devices[d] != null
									&& that.devices[d].name == jsonObject.payload.deviceId) {
								that.devices[d].updateProperties(
										jsonObject.tid,
										jsonObject.payload.accepted);

								if (typeof (that.ondeviceupdate) != 'undefined'
										&& that.ondeviceupdate != null) {
									try {
										that.ondeviceupdate(that.devices[d]);
									} catch (e) {
										log(
												"EndpointSimpleV1.websocketOnmessage.ondeviceupdate.3",
												e);
									}

									// save history to localStore
									var existingDevice = getNamedItem(
											that.devices, that.devices[d].name);
									if (existingDevice != null) {
										storeHistoryToLocalStorage(that,
												existingDevice);
									}
								}

								break;
							}
						}
					} else {
						log(
								"EndpointSimpleV1.websocketOnmessage: error parsing",
								jsonObject, "unknown message type");
					}
				} else {
					log("EndpointSimpleV1.websocketOnmessage: error parsing",
							jsonObject, "not a response or no payload given");
				}
			} catch (e) {
				log("EndpointSimpleV1.websocketOnmessage", e);
			}
		};
	};

	/**
	 * @namespace EndpointSimpleV1
	 */
	EndpointSimpleV1.prototype = {

		/**
		 * Returns the devices of an endpoint.
		 * 
		 * @function EndpointSimpleV1~getDevices
		 * 
		 * @returns {Array}
		 */
		getDevices : function() {
			return this.devices;
		},

		/**
		 * Closes the connection to an endpoint.
		 * 
		 * @function EndpointSimpleV1~disconnect
		 */
		disconnect : function() {
			this.disconnected = true;
			try {
				log("EndpointSimpleV1.disconnect: disconnecting...");
				this.websocket.close();
				log("EndpointSimpleV1.disconnect: disconnected");
			} catch (e) {
				log("EndpointSimpleV1.disconnect", e);
			}
		},

		/**
		 * Opens the connection to an endpoint.
		 * 
		 * @function EndpointSimpleV1~connect
		 * 
		 * @param {function}
		 *            callback A function which will be executed directly after
		 *            the connection is open.
		 * @param {function}
		 *            errorcallback A function which will be executed if the
		 *            connection fails.
		 */
		connect : function(callback, errorcallback) {
			try {
				var that = this;

				this.onconnect = callback;
				this.websocket = new WebSocket(this.endpointAddress);
				this.websocket.onmessage = this.websocketOnmessage;
				this.websocket.onclose = this.websocketOnclose;
				this.websocket.onopen = function() {
					that.disconnected = false;
					that.poll();

					if (typeof (callback) != 'undefined' && callback != null) {
						log("EndpointSimpleV1.connect: callback is defined, now calling");
						callback();
					}

					// TODO CES limitation
					// newEp.pollTimer = setInterval(function() { if
					// (!newEp.disconnected) { newEp.poll(); } }, 30000);
					// TODO CES limitation end
				};
				this.websocket.onerror = function() {
					that.disconnected = true;
					if (typeof (errorcallback) != 'undefined'
							&& errorcallback != null) {
						errorcallback();
					}
				};
			} catch (e) {
				log("EndpointSimpleV1.connect", e);
			}
		},

		/**
		 * Sets a property value of a device.
		 * 
		 * @function EndpointSimpleV1~setProperty
		 * 
		 * @param {string}
		 *            name The device name.
		 * @param {string}
		 *            propName The property name.
		 * @param value
		 *            The new value.
		 * 
		 */
		setProperty : function(name, propName, value) {
			if (typeof (name) == 'undefined' || name == null
					|| typeof (propName) == 'undefined' || propName == null
					|| typeof (value) == 'undefined') {
				log("EndpointSimpleV1.setProperty: invalid parameters given!");
				return;
			}
			var device = null;
			var property = null;
			for ( var d in this.devices) {
				device = this.devices[d];
				if (device.name == name) {
					log("EndpointSimpleV1.setProperty: device " + name
							+ " found");
					for (p in device.properties) {
						property = device.properties[p];
						if (property.name == propName) {
							type = property.obix;
							log("EndpointSimpleV1.setProperty: property "
									+ propName + " found with type " + type);

							// verify min / max
							if (typeof (property.min) != 'undefined'
									&& property.min != null) {
								if (value < property.min) {
									throw "Property " + propName
											+ " cannot be set to " + value
											+ " as below min!";
								}
							}
							if (typeof (property.max) != 'undefined'
									&& property.max != null) {
								if (value > property.max) {
									throw "Property " + propName
											+ " cannot be set to " + value
											+ " as above max!";
								}
							}
							break;
						}
					}
					break;
				}
			}

			if (device == null) {
				log("EndpointSimpleV1.setProperty: could not find device "
						+ name + ", cannot set value!");
				return;
			}

			var request = {
				"type" : "request",
				"tid" : this.reqId,
				"payload" : {
					"deviceId" : name,
					"setAttributes" : {}
				}
			};
			request.payload.setAttributes[propName] = value;

			device.pendingUpdates[this.reqId] = {};
			device.pendingUpdates[this.reqId][propName] = value;
			log("EndpointSimpleV1.setProperty updated pending updates ",
					device.pendingUpdates[this.reqId]);

			log("EndpointSimpleV1.setProperty sending "
					+ JSON.stringify(request));
			if (this.disconnected == false && this.websocket) {
				this.websocket.send(JSON.stringify(request));
			}
		},

		/**
		 * Refreshes the device list of an endpoint.
		 * 
		 * @function EndpointSimpleV1~poll
		 */
		poll : function() {
			var request = {
				"type" : "request",
				"tid" : this.reqId,
				"payload" : {
					"getDevices" : null
				}
			};
			if (typeof (this.websocket) != 'undefined' && !this.disconnected) {
				log("EndpointSimpleV1.poll: do polling", request);
				try {
					this.websocket.send(JSON.stringify(request));
					this.reqId++;
				} catch (e) {
					log("EndpointSimpleV1.poll", e);
				}
			}
		}

	};

	var endpoints = [];
	var discoveredEndpoints = [];

	/*-
	 * STASH:
	 * 
	 * The main API:
	 *   - addEndpoint
	 *   - getEndpoint
	 *   - getEndpoints
	 *   - removeEndpoint
	 *   - getDevices
	 *   - discoverEndpoints
	 *   - getDiscoveredEndpoints
	 * 
	 */

	/**
	 * @namespace stash
	 * 
	 * @property {string} version The Version of the STASH-Library
	 * @property {boolean} debug If set to true, outputs for debugging purposes
	 *           are logged.
	 * @property {boolean} enableHistory If set to true, the history of all
	 *           devices is saved locally.
	 * @property {boolean} enableHistoryToLocalStore If set to true, the history
	 *           of all devices is saved in the locale store.
	 */
	var stash = {

		version : '1.0.20150129-2042',
		messageFormatVersion : 'v1',
		debug : false,
		baseUrn : 'urn:smarttv-alliance-org:service:smarthome:1.0',

		enableHistory : true,
		enableHistoryToLocalStore : true,

		/**
		 * Returns all added endpoints.
		 * 
		 * @function stash~getEndpoints
		 * 
		 * @returns {Array}
		 */
		getEndpoints : function() {
			return endpoints;
		},

		/**
		 * Returns a certain endpoint.
		 * 
		 * @function stash~getEndpoint
		 * 
		 * @param {string}
		 *            name The name of an endpoint.
		 * @returns {Endpoint}
		 */
		getEndpoint : function(name) {
			for ( var e in endpoints) {
				if (endpoints[e].name == name) {
					return endpoints[e];
				}
			}

			log("stash.getEndpoint: could not find endpoint with name " + name);
			return null;
		},

		/**
		 * 
		 * Adds an endpoint (obix.v1 version), tries to connect to it and, if
		 * successful, saves its devices in a list of device objects.
		 * 
		 * @function stash~addEndpoint
		 * 
		 * @param {string}
		 *            name The name of an endpoint.
		 * @param {string}
		 *            endpointAddress The internet address of an Endpoint.
		 * @returns {Endpoint} Returns a new Endpoint.
		 */
		addEndpoint : function(name, endpointAddress) {
			if (typeof (name) == 'undefined'
					|| typeof (endpointAddress) == 'undefined') {
				throw new Error('Parameters for adding endpoint not valid!');
			}

			// verify that name is not used
			for ( var epi in endpoints) {
				if (endpoints[epi].name == name) {
					throw new Error('Endpoint name already taken!');
				}
			}

			var newEp = new EndpointObixV1(name, endpointAddress);
			endpoints.push(newEp);
			return newEp;
		},

		/**
		 * Adds an endpoint, tries to connect to it and, if successful, saves
		 * its devices in a list of device objects.
		 * 
		 * @function stash~addEndpoint
		 * 
		 * @param {string}
		 *            name The name of an endpoint.
		 * @param {string}
		 *            endpointAddress The internet address of an endpoint.
		 * @param {string}
		 *            version The version of an endpoint, currently "obix.v1",
		 *            "obix.v2", "simple.v1" are supported.
		 * @returns {Endpoint} Returns a new endpoint.
		 */
		addEndpoint : function(name, endpointAddress, version) {
			newEp = createEndpoint(name, endpointAddress, version);
			if (newEp != null) {
				endpoints.push(newEp);
			} else {
				log("stash.addEndpoint: no valid endpoint version given, could not create endpoint!");
			}
			return newEp;
		},

		/**
		 * Removes a certain endpoint.
		 * 
		 * @function stash~removeEndpoint
		 * 
		 * @param {string}
		 *            name The name of an endpoint.
		 * 
		 */
		removeEndpoint : function(name) {
			var newEndpoints = [];
			for ( var e in endpoints) {
				if (endpoints[e].name != name) {
					newEndpoints.push(endpoints[e]);
				} else {
					endpoints[e].disconnect();
					if (typeof (endpoints[e].pollTimer) != 'undefined') {
						try {
							clearInterval(endpoints[e].pollTimer);
						} catch (e) {
						}
					}
				}
			}
			endpoints = newEndpoints;
		},

		/**
		 * Returns all devices from all endpoints.
		 * 
		 * @function stash~getDevices
		 * 
		 * @returns {Array}
		 */
		getDevices : function() {
			var allDevices = [];
			for ( var e in endpoints) {
				var endpointDevices = endpoints[e].getDevices();
				for ( var d in endpointDevices) {
					allDevices.push(endpointDevices[d]);
				}
			}
			return allDevices;
		},

		/**
		 * Triggers a search for endpoints found via network discovery
		 * 
		 * @function stash~discoverEndpoints
		 * 
		 * @param {requestCallback}
		 *            (callback) An optional callback function which is called
		 *            with the discovered endpoints as parameter after the
		 *            discovery finished successfully.
		 * @param {requestCallback}
		 *            (error_callaback) An optional error callback function
		 *            which is called with the error as parameter if the
		 *            discovery failed.
		 * 
		 * @returns true if the discovery of endpoints using NSD could be
		 *          triggered false if there was an error triggering the
		 *          discovery
		 */
		discoverEndpoints : function(callback, error_callback) {
			if (navigator.getNetworkServices) {
				discoveredEndpoints.length = 0;
				// using promises as described in the NSD API documentation
				navigator
						.getNetworkServices([ "upnp:" + stash.baseUrn ])
						.then(
								function(servicesManager) {
									log(
											"stash.discoverEndpoints: success, found "
													+ servicesManager.length
													+ " services",
											servicesManager);
									for (var i = 0; i < servicesManager.length; i++) {
										var s = servicesManager[i];

										try {
											log(
													"stash.discoverEndpoints: found service",
													s, "with config=", s.config);

											// parse the given upnp
											// configuration
											var config = (new window.DOMParser())
													.parseFromString(s.config,
															"text/xml");
											var friendlyNameElement = config.documentElement
													.getElementsByTagName('friendlyName');
											var friendlyName = friendlyNameElement
													&& friendlyNameElement.length > 0 ? config.documentElement
													.getElementsByTagName('friendlyName')[0].value
													: null;
											if (!friendlyName) {
												friendlyName = "Endpoint "
														+ Math.random()
																.toString(36)
																.substring(2);
											}

											// url is something like
											// 'wss://<endpointAddress>?encoding=<encoding>&version=<version>'
											// currently only version v1 is
											// supported
											var url = document
													.createElement('a');
											url.href = s.url;
											var endpointAddress = url.protocol
													+ "//" + url.host
													+ url.pathname;

											var searchObject = [];
											var queries = url.search.replace(
													/^\?/, '').split('&');
											for (var j = 0; j < queries.length; j++) {
												var split = queries[j]
														.split('=');
												searchObject[split[0]] = split[1];
											}

											// defaulting to obix protocol (as
											// in
											// the SDK emulator)
											var encoding = searchObject['encoding'] ? searchObject['encoding']
													: 'obix.v2';

											try {
												var endpoint = createEndpoint(
														friendlyName,
														endpointAddress,
														encoding);
												log(
														"stash.discoverEndpoints: created endpoint out of host="
																+ url.host
																+ ", address="
																+ endpointAddress
																+ ", encoding="
																+ encoding,
														endpoint);
												if (endpoint != null) {
													discoveredEndpoints
															.push(endpoint);
												}
											} catch (inner) {
												log(
														"stash.discoverEndpoints: encountered error during creation of endpoint",
														inner);

											}
										} catch (e) {
											log(
													"stash.discoverEndpoints: encountered error during discovery",
													e);

										}

									}
									if (callback) {
										callback(discoveredEndpoints);
									}
								},
								function(error) {
									// on error, do nothing besides logging
									log(
											"stash.discoverEndpoints: error occured during retrieving network services",
											error);
									if (error_callback) {
										error_callback(error);
									}
								});
				// it takes some seconds until the results are in and the callback is called
				return true;
			} else {
				log("stash.discoverEndpoints: no network services API available in this browser for searching!")
				return false;
			}
		},

		/**
		 * 
		 * 
		 * @function stash~getDiscoveredEndpoints
		 */
		getDiscoveredEndpoints : function() {
			return discoveredEndpoints;
		}

	};

	return stash;
}());