Most devices require some kind of authentication, for example a password, an OAuth 2.0 access token, or local interactive pairing via some protocol. Developers can specify how the device should be configured in the device class by importing a config
module from one of the following supported mixins:
@org.thingpedia.config.none
: for devices with no authentication at all@org.thingpedia.config.form
: for devices with no authentication but require extra information from the user to configure@org.thingpedia.config.basic_auth
: for devices that use traditional username and password@org.thingpedia.config.oauth2
: for OAuth 1.0 and 2.0 style authentication@org.thingpedia.config.discovery.upnp
: for authentication by discovery and local interactive pairing via UPnP protocol@org.thingpedia.config.discovery.bluetooth
: for authentication by discovery and local interactive pairing via Bluetooth protocolSome public services require no authentication to request for data. In this case, no config
module needs to be imported for the device explicitly and the one from @org.thingpedia.config.none
will be automatically used.
If an API key is required, you can specify it as follows
import config from @org.thingpedia.config.none(api_key=<your-api-key>);
If some other information is required from the user, you can use the config
module from @org.thingpedia.config.form
, where you can define the configuration parameters you need. For example, in RSS feed, users can type in the URL of the RSS feed when they configure the device. The device imports the config
module as follows:
import config from @org.thingpedia.config.form(params=makeArgMap(name:String, url:String));
If a device does not provide an OAuth interface, a traditional username/password method is supported. It can be considered as a special case of @org.thingpedia.config.form
with two fields builtin: username
and password
. If needed, additional parameters can be specified with extra_params
:
import config from @org.thingpedia.config.basic_auth(extra_params=makeArgMap(...))
Note: this is not recommended if OAuth is available.
If the device has a JS package, this configuration module is equivalent to a form
configuration. If the device uses a well-known protocol like REST, the Authorization
header will also be set on HTTP requests.
Most of online accounts use the standard OAuth 2.0 protocol nowadays. OAuth support can be imported as follows:
import config from @org.thingpedia.config.oauth2(
client_id=<your client id>,
client_secret=<your client secret>,
authorize=<authorize URL>,
get_access_token=<access token URL>,
scope=<array of OAuth scopes>,
get_profile=<profile URL>,
profile=<array of profile fields>
);
For example, OAuth support for Spotify can be declared as:
import config from @org.thingpedia.config.oauth2(
client_id="...",
client_secret="...",
authorize="https://accounts.spotify.com/authorize"^^tt:url,
get_access_token="https://accounts.spotify.com/api/token"^^tt:url,
scope=["streaming", "playlist-read-collaborative", "playlist-modify-private",
"playlist-read-private", "playlist-modify-public", "user-read-email",
"user-read-private", "user-read-playback-state", "user-read-currently-playing",
"user-modify-playback-state", "user-read-recently-played", "user-top-read",
"user-follow-read", "user-follow-modify", "user-library-read",
"user-library-modify"],
get_profile="https://api.spotify.com/v1/me"^^tt:url,
profile=["id", "display_name"]
);
The authorize
, get_access_token
and scope
fields should be retrieved from the documentation of the service.
The get_profile
URL is an optional URL that will be called, under OAuth authentication, to retrieve information about the logged in user. If specified, it should be an endpoint that returns a JSON object when called. The specific fields named in the profile
key will be extracted of that object and stored in the state of the device. Later, those field can be used in the JS code, as well as the #_[name]
and #_[description]
annotations of the device.
If the API does not offer a simple profile URL, or if custom code is desired to load the device, a device class can expose the loadFromOAuth2
static method, which should return a configured device instance. For example:
class MyDevice {
static async loadFromOAuth2(engine, accessToken, refreshToken, extraData) {
// do something, compute state object
return new MyDevice(engine, state);
}
}
For more details, see the SDK reference.
If your device uses OAuth-style authentication that is different from RFC 6749 (for example, OAuth 1.0), you must implement the loadFromCustomOAuth
and completeCustomOAuth
methods in your device class.
The loadFromCustomOAuth
static method will be called at the beginning of process. It should do whatever preparation to access the remote service and return an array with two elements:
The OAuth call should be set to redirect to platform.getOrigin() + '/devices/oauth2/callback/' + <your kind>
.
If the service has redirect URI validation, you should use the following as origins:
Pseudo-code example:
static async loadFromCustomOAuth(engine, req) {
await prepareForOAuth2();
return ['https://api.example.com/1.0/authorize?redirect_uri=' +
platform.getOrigin() + '/devices/oauth2/callback/com.example',
{ 'com-example-session': 'state' }];
}
The second part of the OAuth process is handled by the completeCustomOAuth
static method.
The method will be called with the redirect URL (including the query part, which will contain the authentication code) and any session items.
In this method can use the authentication code produced by the callback
to obtain the real access token, and then save it to the database. The method should return a configured device instance. In pseudo-code:
static async completeCustomOAuth(engine, url, session) {
const parsed = Url.parse(url);
const [accessToken, refreshToken] = await getAccessToken(parsed.code);
const profile = await getProfile(accessToken);
return new MyDevice(engine, {
kind: 'com.example',
accessToken: accessToken,
refreshToken: refreshToken,
userId: profile.id });
});
}
For more details, see the SDK reference.
This feature is not yet supported in Almond 1.99. The following documentation is only applicable to Almond 1.*. It is unclear whether local connectivity and discovery will be supported in the future. Device authors are advised to use a local gateway such as Home Assistant to connect to IoT devices.
Local discovery in Thingpedia relies on the thingpedia-discovery nodejs module, which contains the generic code to run the discovery protocols and to match the discovery information to a specific interface.
If your device supports discovery, your must implement the loadFromDiscovery(engine, publicData, privateData)
static method method. publicData
and privateData
are objects that contain information derived from the discovery protocol, and are discovery protocol specific; privateData
contains user identifying information (such as serial numbers and HW addresses), while publicData
contains the generic capabilities inferred by discovery and is sent to Thingpedia to match the interface. publicData.kind
is the identifier for the discovery protocol in use.
The return value from loadFromDiscovery
should be an instance of your device class appropriately configured. This device will not be used (and will not be initialized) until the user confirms the configuration.
You must also implement completeDiscovery(delegate)
, which will be invoked when the user clicks on your device among the many discovered. The delegate object provided is a way to interact with the user. It has the following methods:
confirm(question)
: ask the user a yes or no question, for example to confirm a pairing coderequestCode(question)
: request a free-form response from the user, for example a password or PIN codeThe usual template for a completeDiscovery
implementation will thus be:
async completeDiscovery(delegate) {
const code = await this.service.pair();
const confirmed = await delegate.confirm("Does the code %s match what you see on the device?".format(code));
if (confirmed) {
await this.service.confirm();
return this;
} else {
throw new Error('Unable to verify the pairing code');
}
}
Furthermore, your device should implement updateFromDiscovery(publicData, privateData)
, which is called when a device that was already configured is rediscovered. You can use this method to update any cached data about the device based on the new advertisement, for example to update the Bluetooth alias.
Finally, your device must set this.descriptors
to a list of protocol specific device descriptors that will help the generic code recognize if a device was already configured or not, and must set state.discoveredBy
to engine.ownTier
.
Discovery data:
publicData.kind
: bluetooth
publicData.uuids
: array of lower case Bluetooth UUIDspublicData.class
: the numeric Bluetooth classprivateData.address
: lower case canonical Bluetooth HW addressprivateData.alias
: Bluetooth alias (human readable name)privateData.paired
: if Bluetooth pairing happened alreadyprivateData.trusted
: if the device is trusted to access services on the hostdescriptor
: bluetooth/
followed by the HW addressThingpedia matching of interfaces is based on UUIDs and the device class.
The config mixin has two parameters:
uuids
(array of strings): the list of UUIDs are you interested in; for example, to support A2DP (BT speakers) use the UUID 000110b-0000-1000-8000-00805f9b34fb
.device_class
(enum: audio_video
, computer
, health
, imaging
, misc
, networking
, peripheral
, phone
, toy
, wearable
)In case multiple Thingpedia devices match a specific physical device, the Thingpedia device capable of handling the most UUIDs is chosen.