Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Import utilities to create device factories from almond-cloud
This is code that has been copied to almond-cmdline and almond-server to implement "developer mode", and it's best if shared. Also, this simplifies adding new kinds of loaders or config modules, cause almond-cloud no longer needs to change.
- Loading branch information
Showing
4 changed files
with
327 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: device.primary_kind, | ||
text: device.name, | ||
discoveryType: 'bluetooth' | ||
}; | ||
case 'org.thingpedia.config.discovery.upnp': | ||
return { | ||
type: 'discovery', | ||
category: device.category, | ||
kind: device.primary_kind, | ||
text: device.name, | ||
discoveryType: 'upnp' | ||
}; | ||
|
||
case 'org.thingpedia.config.interactive': | ||
return { | ||
type: 'interactive', | ||
category: device.category, | ||
kind: device.primary_kind, | ||
text: device.name | ||
}; | ||
|
||
case 'org.thingpedia.config.none': | ||
return { | ||
type: 'none', | ||
category: device.category, | ||
kind: device.primary_kind, | ||
text: device.name | ||
}; | ||
|
||
case 'org.thingpedia.config.oauth2': | ||
case 'org.thingpedia.config.custom_oauth': | ||
return { | ||
type: 'oauth2', | ||
category: device.category, | ||
kind: device.primary_kind, | ||
text: device.name | ||
}; | ||
|
||
case 'org.thingpedia.config.form': | ||
return { | ||
type: 'form', | ||
category: device.category, | ||
kind: device.primary_kind, | ||
text: device.name, | ||
fields: toFields(getInputParam(config, 'params')) | ||
}; | ||
|
||
case 'org.thingpedia.config.basic_auth': | ||
return { | ||
type: 'form', | ||
category: device.category, | ||
kind: device.primary_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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
// -*- 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 = [ | ||
[`class @com.bing { | ||
import loader from @org.thingpedia.v2(); | ||
import config from @org.thingpedia.config.none(); | ||
}`, { | ||
primary_kind: "com.bing", | ||
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)); | ||
}`, { | ||
primary_kind: "com.bodytrace.scale", | ||
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 @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(); | ||
}`, { | ||
primary_kind: "com.twitter", | ||
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"); | ||
}`, { | ||
primary_kind: "com.linkedin", | ||
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']); | ||
}`, { | ||
primary_kind: "com.lg.tv.webos2", | ||
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']); | ||
}`, { | ||
primary_kind: "org.thingpedia.bluetooth.speaker.a2dp", | ||
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(); |