Skip to content

Commit

Permalink
Merge pull request #42 from stanford-oval/wip/import-device-factory
Browse files Browse the repository at this point in the history
Import utilities to create device factories from almond-cloud
  • Loading branch information
gcampax committed Oct 23, 2019
2 parents 9ff085c + f069edc commit e64627e
Show file tree
Hide file tree
Showing 5 changed files with 415 additions and 0 deletions.
2 changes: 2 additions & 0 deletions index.js
Expand Up @@ -23,6 +23,7 @@ const DeviceFactory = require('./lib/factory');
const { ImplementationError, UnsupportedError } = require('./lib/errors');
const BaseEngine = require('./lib/base_engine');
const BasePlatform = require('./lib/base_platform');
const DeviceConfigUtils = require('./lib/device_factory_utils');

const ThingTalk = require('thingtalk');

Expand Down Expand Up @@ -123,6 +124,7 @@ module.exports = {
HttpClient,
FileClient,
DeviceFactory,
DeviceConfigUtils,

// expose errors
ImplementationError,
Expand Down
176 changes: 176 additions & 0 deletions lib/device_factory_utils.js
@@ -0,0 +1,176 @@
// -*- mode: js; indent-tabs-mode: nil; js-basic-offset: 4 -*-
//
// This file is part of Thingpedia
//
// Copyright 2018 The Board of Trustees of the Leland Stanford Junior University
//
// Author: Giovanni Campagna <gcampagn@cs.stanford.edu>
//
// See COPYING for details
"use strict";

function clean(name) {
if (/^[vwgp]_/.test(name))
name = name.substr(2);
return name.replace(/_/g, ' ').replace(/([^A-Z ])([A-Z])/g, '$1 $2').toLowerCase();
}

function entityTypeToHTMLType(type) {
switch (type) {
case 'tt:password':
return 'password';
case 'tt:url':
return 'url';
case 'tt:phone_number':
return 'tel';
case 'tt:email_address':
return 'email';
default:
return 'text';
}
}

function getInputParam(config, name) {
for (let inParam of config.in_params) {
if (inParam.name === name)
return inParam.value.toJS();
}
return undefined;
}

function makeDeviceFactory(classDef, device) {
if (classDef.is_abstract)
return null;

const config = classDef.config;

function toFields(argMap) {
if (!argMap)
return [];
return Object.keys(argMap).map((k) => {
const type = argMap[k];
let htmlType;
if (type.isEntity)
htmlType = entityTypeToHTMLType(type.type);
else if (type.isNumber || type.isMeasure)
htmlType = 'number';
else if (type.isBoolean)
htmlType = 'checkbox';
else
htmlType = 'text';
return { name: k, label: clean(k), type: htmlType };
});
}

switch (config.module) {
case 'org.thingpedia.config.builtin':
return null;

case 'org.thingpedia.config.discovery.bluetooth':
return {
type: 'discovery',
category: device.category,
kind: classDef.kind,
text: device.name,
discoveryType: 'bluetooth'
};
case 'org.thingpedia.config.discovery.upnp':
return {
type: 'discovery',
category: device.category,
kind: classDef.kind,
text: device.name,
discoveryType: 'upnp'
};

case 'org.thingpedia.config.interactive':
return {
type: 'interactive',
category: device.category,
kind: classDef.kind,
text: device.name
};

case 'org.thingpedia.config.none':
return {
type: 'none',
category: device.category,
kind: classDef.kind,
text: device.name
};

case 'org.thingpedia.config.oauth2':
case 'org.thingpedia.config.custom_oauth':
return {
type: 'oauth2',
category: device.category,
kind: classDef.kind,
text: device.name
};

case 'org.thingpedia.config.form':
return {
type: 'form',
category: device.category,
kind: classDef.kind,
text: device.name,
fields: toFields(getInputParam(config, 'params'))
};

case 'org.thingpedia.config.basic_auth':
return {
type: 'form',
category: device.category,
kind: classDef.kind,
text: device.name,
fields: [
{ name: 'username', label: 'Username', type: 'text' },
{ name: 'password', label: 'Password', type: 'password' }
].concat(toFields(getInputParam(config, 'extra_params')))
};

default:
throw new Error(`Unrecognized config mixin ${config.module}`);
}
}

function getDiscoveryServices(classDef) {
if (classDef.is_abstract)
return [];

const config = classDef.config;
switch (config.module) {
case 'org.thingpedia.config.discovery.bluetooth': {
const uuids = getInputParam(config, 'uuids');
const deviceClass = getInputParam(config, 'device_class');

const services = uuids.map((u) => {
return {
discovery_type: 'bluetooth',
service: 'uuid-' + u.toLowerCase()
};
});
if (deviceClass) {
services.push({
discovery_type: 'bluetooth',
service: 'class-' + deviceClass
});
}
return services;
}
case 'org.thingpedia.config.discovery.upnp':
return getInputParam(config, 'search_target').map((st) => {
return {
discovery_type: 'upnp',
service: st.toLowerCase().replace(/^urn:/, '').replace(/:/g, '-')
};
});
default:
return [];
}
}

module.exports = {
makeDeviceFactory,
getDiscoveryServices
};
2 changes: 2 additions & 0 deletions test/index.js
Expand Up @@ -15,6 +15,8 @@ async function seq(array) {

seq([
('./test_unit'),
('./test_device_factories'),
('./test_discovery_services'),
('./test_string_format'),
('./test_version'),
('./test_class'),
Expand Down
172 changes: 172 additions & 0 deletions test/test_device_factories.js
@@ -0,0 +1,172 @@
// -*- mode: js; indent-tabs-mode: nil; js-basic-offset: 4 -*-
//
// This file is part of Almond Cloud
//
// Copyright 2018 The Board of Trustees of the Leland Stanford Junior University
//
// Author: Giovanni Campagna <gcampagn@cs.stanford.edu>
//
// See COPYING for details
"use strict";

const assert = require('assert');
const ThingTalk = require('thingtalk');

const DeviceFactoryUtils = require('../lib/device_factory_utils');

const TEST_CASES = [
[`abstract class @security-camera {}`, {
name: 'Security Camera',
category: 'physical',
}, null],

[`class @org.thingpedia.builtin.thingengine.builtin {
import loader from @org.thingpedia.builtin();
import config from @org.thingpedia.config.builtin();
}`, {
name: 'Security Camera',
category: 'physical',
}, null],

[`class @com.bing {
import loader from @org.thingpedia.v2();
import config from @org.thingpedia.config.none();
}`, {
name: "Bing Search",
category: 'data',
}, {
type: 'none',
text: "Bing Search",
kind: 'com.bing',
category: 'data'
}],

[`class @com.bodytrace.scale {
import loader from @org.thingpedia.v2();
import config from @org.thingpedia.config.basic_auth(extra_params=makeArgMap(serial_number : String));
}`, {
name: "BodyTrace Scale",
category: 'physical',
}, {
type: 'form',
text: "BodyTrace Scale",
kind: 'com.bodytrace.scale',
category: 'physical',
fields: [
{ name: 'username', label: 'Username', type: 'text' },
{ name: 'password', label: 'Password', type: 'password' },
{ name: 'serial_number', label: 'serial number', type: 'text' },
]
}],

[`class @com.bodytrace.scale2 {
import loader from @org.thingpedia.v2();
import config from @org.thingpedia.config.basic_auth();
}`, {
name: "BodyTrace Scale",
category: 'physical',
}, {
type: 'form',
text: "BodyTrace Scale",
kind: 'com.bodytrace.scale2',
category: 'physical',
fields: [
{ name: 'username', label: 'Username', type: 'text' },
{ name: 'password', label: 'Password', type: 'password' }
]
}],

[`class @org.thingpedia.rss {
import loader from @org.thingpedia.rss();
import config from @org.thingpedia.config.form(params=makeArgMap(url : Entity(tt:url)));
}`, {
primary_kind: "org.thingpedia.rss",
name: "RSS Feed",
category: 'data',
}, {
type: 'form',
text: "RSS Feed",
kind: 'org.thingpedia.rss',
category: 'data',
fields: [
{ name: 'url', label: 'url', type: 'url' },
]
}],

[`class @com.twitter {
import loader from @org.thingpedia.v2();
import config from @org.thingpedia.config.custom_oauth();
}`, {
name: "Twitter Account",
category: 'online',
}, {
type: 'oauth2',
text: "Twitter Account",
kind: 'com.twitter',
category: 'online',
}],

[`class @com.linkedin {
import loader from @org.thingpedia.v2();
import config from @org.thingpedia.config.oauth2(client_id="foo", client_secret="bar");
}`, {
name: "LinkedIn Account",
category: 'online',
}, {
type: 'oauth2',
text: "LinkedIn Account",
kind: 'com.linkedin',
category: 'online',
}],

[`class @com.lg.tv.webos2 {
import loader from @org.thingpedia.v2();
import config from @org.thingpedia.config.discovery.upnp(search_target=['urn:lge:com:service:webos:second-screen-1']);
}`, {
name: "LG TV",
category: 'physical',
}, {
type: 'discovery',
text: "LG TV",
kind: 'com.lg.tv.webos2',
category: 'physical',
discoveryType: 'upnp'
}],

[`class @org.thingpedia.bluetooth.speaker.a2dp {
import loader from @org.thingpedia.v2();
import config from @org.thingpedia.config.discovery.bluetooth(uuids=['0000110b-0000-1000-8000-00805f9b34fb']);
}`, {
name: "Bluetooth Speaker",
category: 'physical',
}, {
type: 'discovery',
text: "Bluetooth Speaker",
kind: 'org.thingpedia.bluetooth.speaker.a2dp',
category: 'physical',
discoveryType: 'bluetooth'
}],
];

async function testCase(i) {
console.log(`Test Case #${i+1}`);
const [classCode, device, expectedFactory] = TEST_CASES[i];

const classDef = ThingTalk.Grammar.parse(classCode).classes[0];
const generatedFactory = DeviceFactoryUtils.makeDeviceFactory(classDef, device);

try {
assert.deepStrictEqual(generatedFactory, expectedFactory);
} catch(e) {
console.error('Failed: ' + e.message);
if (process.env.TEST_MODE)
throw e;
}
}
async function main() {
for (let i = 0; i < TEST_CASES.length; i++)
await testCase(i);
}
module.exports = main;
if (!module.parent)
main();

0 comments on commit e64627e

Please sign in to comment.