/*
* 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;
}());