Skip to content

Instantly share code, notes, and snippets.

@arjunmenon
Last active November 24, 2017 12:50
Show Gist options
  • Save arjunmenon/8ad89f7348442834efe6271e731221f4 to your computer and use it in GitHub Desktop.
Save arjunmenon/8ad89f7348442834efe6271e731221f4 to your computer and use it in GitHub Desktop.
var Client = require('./upnp_client.js')
var client = new Client('http://192.168.1.6:49152/description.xml');
console.log(client);
// Get the device description
client.getDeviceDescription(function(err, description) {
if(err) throw err;
console.log("starting device description module")
console.log(description);
console.log("ending device description module")
});
client.callAction('AVTransport', 'GetMediaInfo', { InstanceID: 0 }, function(err, result) {
if(err) throw err;
console.log(result); // => { NrTracks: '1', MediaDuration: ... }
});
require("tabris-js-node");
// var util = require("util");
var et = require('elementtree'); // remove the utils
function DeviceClient(url) {
this.url = url;
this.deviceDescription = null;
this.serviceDescriptions = {};
this.server = null;
this.listening = false;
this.subscriptions = {};
}
DeviceClient.prototype.getDeviceDescription = function(callback) {
var self = this;
// Use cache if available
if(this.deviceDescription) {
process.nextTick(function() {
callback(null, self.deviceDescription);
});
return;
}
// console.log("this.url "+this.url)
// console.log("self.url "+self.url)
fetch(this.url)
.then(response => response.text())
.then((bodyText) => {
var desc = parseDeviceDescription(bodyText, self.url);
self.deviceDescription = desc // Store in cache for next call
callback(null, desc); // returns the result
}).catch((err) => {
console.log("OKAY.THERE IS AN ERROR");
console.log(err);
});
};
function parseDeviceDescription(xml, url) {
var doc = et.parse(xml);
var desc = extractFields(doc.find('./device'), [
'deviceType',
'friendlyName',
'manufacturer',
'manufacturerURL',
'modelName',
'modelNumber',
'modelDescription',
'UDN'
]);
var nodes = doc.findall('./device/serviceList/service');
desc.services = {};
nodes.forEach(function(service) {
var tmp = extractFields(service, [
'serviceType',
'serviceId',
'SCPDURL',
'controlURL',
'eventSubURL'
]);
var id = tmp.serviceId;
delete tmp.serviceId;
desc.services[id] = tmp;
});
// Make URLs absolute
var baseUrl = extractBaseUrl(url);
// Reformat URLs from '/ctl/service' to 'http://host:port/ctl/service'
// It was fucking there job to give it pretty
Object.keys(desc.services).forEach(function(id) {
var service = desc.services[id];
service.SCPDURL = buildAbsoluteUrl(baseUrl, service.SCPDURL);
service.controlURL = buildAbsoluteUrl(baseUrl, service.controlURL);
service.eventSubURL = buildAbsoluteUrl(baseUrl, service.eventSubURL);
});
return desc;
};
function extractFields(node, fields) {
var data = {};
fields.forEach(function(field) {
var value = node.findtext('./' + field);
if(typeof value !== 'undefined') {
data[field] = value;
}
});
return data;
}
function buildAbsoluteUrl(base, url) {
if(url === '') return '';
if(url.substring(0, 4) === 'http') return url;
if(url[0] === '/') {
var root = base.split('/').slice(0, 3).join('/'); // http://host:port
return root + url;
} else {
return base + '/' + url;
}
}
function extractBaseUrl(url) {
return url.split('/').slice(0, -1).join('/'); // removes the last slash
}
DeviceClient.prototype.getServiceDescription = function(serviceId, callback) {
var self = this;
serviceId = resolveService(serviceId);
this.getDeviceDescription(function(err, desc) {
if(err) return callback(err);
var service = desc.services[serviceId];
if(!service) { // check if they are out of there mind
var err = new Error('Service ' + serviceId + ' not provided by device');
err.code = 'ENOSERVICE';
return callback(err);
}
// Use cache if available
if(self.serviceDescriptions[serviceId]) {
return callback(null, self.serviceDescriptions[serviceId]);
}
fetch(service.SCPDURL)
.then(response => response.text())
.then((bodyText) => {
var desc = parseServiceDescription(bodyText);
self.serviceDescriptions[serviceId] = desc; // Store in cache for next call
callback(null, desc); // returns the result
}).catch((err) => {
console.log("OKAY.THERE IS AN ERROR");
console.log(err);
}); // end fetch
}); // end getDeviceDescrioption
};
function resolveService(serviceId) {
return (serviceId.indexOf(':') === -1)
? 'urn:upnp-org:serviceId:' + serviceId
: serviceId;
}
function parseServiceDescription(xml) {
var doc = et.parse(xml);
var desc = {};
desc.actions = {};
var nodes = doc.findall('./actionList/action');
nodes.forEach(function(action) {
var name = action.findtext('./name');
var inputs = [];
var outputs = [];
var nodes = action.findall('./argumentList/argument');
nodes.forEach(function(argument) {
var arg = extractFields(argument, [
'name',
'direction',
'relatedStateVariable'
]);
var direction = arg.direction;
delete arg.direction;
if(direction === 'in') inputs.push(arg);
else outputs.push(arg);
});
desc.actions[name] = {
inputs: inputs,
outputs: outputs
};
});
desc.stateVariables = {};
var nodes = doc.findall('./serviceStateTable/stateVariable');
nodes.forEach(function(stateVariable) {
var name = stateVariable.findtext('./name');
var nodes = stateVariable.findall('./allowedValueList/allowedValue');
var allowedValues = nodes.map(function(allowedValue) {
return allowedValue.text;
});
desc.stateVariables[name] = {
dataType: stateVariable.findtext('./dataType'),
sendEvents: stateVariable.get('sendEvents'),
allowedValues: allowedValues,
defaultValue: stateVariable.findtext('./defaultValue')
};
});
return desc;
}
DeviceClient.prototype.callAction = function(serviceId, actionName, params, callback) {
var self = this;
serviceId = resolveService(serviceId);
this.getServiceDescription(serviceId, function(err, desc) {
if(err) return callback(err);
if(!desc.actions[actionName]) {
var err = new Error('Action ' + actionName + ' not implemented by service');
err.code = 'ENOACTION';
return callback(err);
}
var service = self.deviceDescription.services[serviceId];
var UA = "UPnP/1.0, UPNPClient/0.0.1";
var soapAction = service.serviceType + "#" + actionName;
var body = "<u:" + actionName + " xmlns:u=\"" + service.serviceType + "\">";
for(var x in params) {
body += "<" + x + ">" + params[x] + "</" + x + ">";
}
body += "</u:" + actionName + ">";
var payload = `<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>` +
body +
`</s:Body>
</s:Envelope>`;
var xmlHTTP = new tabris.XMLHttpRequest();
xmlHTTP.open('POST', service.controlURL, true);
xmlHTTP.setRequestHeader('SOAPAction', '"' + soapAction + '"');
xmlHTTP.setRequestHeader('Content-Type', 'text/xml; charset="utf-8"');
xmlHTTP.setRequestHeader('User-Agent', UA);
xmlHTTP.onreadystatechange = function() {
if(xmlHTTP.readyState === xmlHTTP.DONE) {
var doc = et.parse(xmlHTTP.responseText);
// Extract response outputs
var serviceDesc = self.serviceDescriptions[serviceId];
var actionDesc = serviceDesc.actions[actionName];
var outputs = actionDesc.outputs.map(function(desc) {
return desc.name;
});
var result = {};
outputs.forEach(function(name) {
result[name] = doc.findtext('.//' + name);
});
callback(null, result)
// console.log("THIS IS MINE ----"+xmlHTTP.responseText)
} else {
console.log("not OK: " + xmlHTTP.status + ": "+JSON.stringify(xmlHTTP))
}
}; // end xmlHTTP
xmlHTTP.send(payload);
}); // end this.getServiceDescription
};
module.exports = DeviceClient;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment