Web of Things (WoT) Scripting API

W3C Group Note

More details about this document
This version:
https://www.w3.org/TR/2023/NOTE-wot-scripting-api-20231003/
Latest published version:
https://www.w3.org/TR/wot-scripting-api/
Latest editor's draft:
https://w3c.github.io/wot-scripting-api/
History:
https://www.w3.org/standards/history/wot-scripting-api/
Commit history
Editors:
Zoltan Kis (Intel)
Daniel Peintner (Siemens AG)
Cristiano Aguzzi (Invited Expert)
Johannes Hund (Former Editor, when at Siemens AG)
Kazuaki Nimura (Former Editor, at Fujitsu Ltd.)
Feedback:
GitHub w3c/wot-scripting-api (pull requests, new issue, open issues)
public-wot-wg@w3.org with subject line [wot-scripting-api] … message topic … (archives)
Repository
On GitHub
File a bug
Contributors
Contributors on GitHub

Abstract

The Web of Things is made of entities (Things) that can describe their capabilities in a machine-interpretable Thing Description (TD) and expose these capabilities through the WoT Interface, that is, network interactions modeled as Properties (for reading and writing values), Actions (to execute remote procedures with or without return values) and Events (for signaling notifications).

The main Web of Things (WoT) concepts are described in the Web of Things (WoT) Architecture 1.1 specification.

Scripting is an optional building block in WoT and it is typically used in gateways or browsers that are able to run a WoT Runtime and script management, providing a convenient way to extend WoT support to new types of endpoints and implement WoT applications such as TD Directory.

This specification describes an application programming interface (API) representing the WoT Interface that allows scripts to discover, operate Things and to expose locally defined Things characterized by WoT Interactions specified by a script.

The APIs defined in this document deliberately follow the Web of Things (WoT) Thing Description 1.1 specification closely. It is possible to implement more abstract APIs on top of them, or implementing directly the WoT network facing interface (i.e. the WoT Interface).

Editor's note

This specification is implemented at least by the Eclipse Thingweb project also known as node-wot, which is considered the reference open source implementation at the moment. Check its source code, including examples.

Status of This Document

This section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the W3C technical reports index at https://www.w3.org/TR/.

Implementers need to be aware that this specification is considered unstable. Vendors interested in implementing this specification before it eventually reaches the Candidate Recommendation phase should subscribe to the repository and take part in the discussions.

Editor's note: The W3C WoT WG is asking for feedback

Please contribute to this draft using the GitHub Issues page of the WoT Scripting API repository. For feedback on security and privacy considerations, please use the WoT Security and Privacy Issues.

This document was published by the Web of Things Working Group as a Group Note using the Note track.

This Group Note is endorsed by the Web of Things Working Group, but is not endorsed by W3C itself nor its Members.

This is a draft document and may be updated, replaced or obsoleted by other documents at any time. It is inappropriate to cite this document as other than work in progress.

The W3C Patent Policy does not carry any licensing requirements or commitments on this document.

This document is governed by the 12 June 2023 W3C Process Document.

1. Introduction

WoT provides layered interoperability based on how Things are used: "consumed" and "exposed", as defined in the Web of Things (WoT) Architecture 1.1 terminology.

By consuming a TD, a client Thing creates a local runtime resource model that allows accessing the Properties, Actions and Events exposed by the server Thing on a remote device.

Exposing a Thing requires: This specification describes how to expose and consume Things by a script. Also, it defines a generic API for Thing discovery.
Note

Typically scripts are meant to be used on bridges or gateways that expose and control simpler devices as WoT Things and have means to handle (e.g. install, uninstall, update etc.) and run scripts.

Note

This specification does not make assumptions on how the WoT Runtime handles and runs scripts, including single or multiple tenancy, script deployment and lifecycle management. The API already supports the generic mechanisms that make it possible to implement script management, for instance by exposing a manager Thing whose Actions (action handlers) implement script lifecycle management operations.

2. Use Case Scenarios

This section is non-normative.

The business use cases listed in the [WOT-USE-CASES] document may be implemented using this API, based on the scripting use case scenarios described here.

2.1 Consuming a Thing

2.2 Exposing a Thing

2.3 Discovery

3. Conformance

As well as sections marked as non-normative, all authoring guidelines, diagrams, examples, and notes in this specification are non-normative. Everything else in this specification is normative.

The key words MAY, MUST, and SHOULD in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

Editor's note

This specification used to be a Working Draft which was expected to become a W3C Recommendation. However, it is now a WG Note which contains informative statements only. Therefore we need to consider how to deal with the description within this Conformance section.

This specification describes the conformance criteria for the following classes of user agent (UA).

Due to requirements of small embedded implementations, splitting WoT client and server interfaces was needed. Then, discovery is a distributed application, but typical scenarios have been covered by a generic discovery API in this specification. This resulted in using 3 conformance classes for a UA that implements this API, one for client, one for server, and one for discovery. An application that uses this API can introspect for the presence of the consume(), produce() and discover() methods on the WoT API object in order to determine which conformance class the UA implements.

WoT Consumer UA

Implementations of this conformance class MUST implement the ConsumedThing interface and the consume() method on the WoT API object.

WoT Producer UA

Implementations of this conformance class MUST implement ExposedThing interface and the produce() method on the WoT API object.

WoT Discovery UA

Implementations of this conformance class MUST implement the ThingDiscoveryProcess interface, the discover() method, the exploreDirectory() method, and the requestThingDescription() method on the WoT API object.

These conformance classes MAY be implemented in a single UA.

This specification can be used for implementing the WoT Scripting API in multiple programming languages. The interface definitions are specified in [WEBIDL].

The UA may be implemented in the browser, or in a separate runtime environment, such as Node.js or in small embedded runtimes.

Implementations that use ECMAScript executed in a browser to implement the APIs defined in this document MUST implement them in a manner consistent with the ECMAScript Bindings defined in the Web IDL specification [WEBIDL].

Implementations that use TypeScript or ECMAScript in a runtime to implement the APIs defined in this document MUST implement them in a manner consistent with the TypeScript Bindings defined in the TypeScript specification [TYPESCRIPT].

4. Terminology and conventions

The generic WoT terminology is defined in [WOT-ARCHITECTURE]: Thing, Thing Description (in short TD), Partial TD, Web of Things (in short WoT), WoT Interface, Protocol Bindings, WoT Runtime, Consuming a Thing Description, TD Directory, Property, Action, Event, DataSchema, Form, SecurityScheme, NoSecurityScheme etc.

WoT Interaction is a synonym for Interaction Affordance. An Interaction Affordance (or shortly, affordance) is the term used in [WOT-TD] when referring to Thing capabilities, as explained in TD issue 282. However, this term is not well understood outside the TD semantic context. Hence for the sake of readability, this document will use the previous term WoT interaction or, simply, interaction instead.

WoT network interface synonym for WoT Interface.

JSON Schema is defined in these specifications.

Promise, Error, JSON, JSON.stringify, JSON.parse, internal method and internal slot are defined in [ECMASCRIPT].

5. The ThingDescription type

WebIDLtypedef object ThingDescription;

Represents a Thing Description (TD) as defined in [WOT-TD]. It is expected to be a parsed JSON object that is validated using JSON Schema validation.

5.1 Fetching a Thing Description

Fetching a TD given a URL should be done with an external method, such as the Fetch API or a HTTP client library, which offer already standardized options on specifying fetch details.

Example 1: Fetching a Thing Description
try {
  let res = await fetch('https://tds.mythings.biz/sensor11');
  // ... additional checks possible on res.headers
  let td = await res.json();
  let thing = await WOT.consume(td);
  console.log("Thing name: " + thing.getThingDescription().title);
} catch (err) {
  console.log("Fetching TD failed", err.message);
}

5.2 Expanding a Thing Description

Note that the Web of Things (WoT) Thing Description 1.1 specification allows using a shortened Thing Description by the means of defaults and requiring clients to expand them with default values specified in the Web of Things (WoT) Thing Description 1.1 specification for the properties that are not explicitly defined in a given TD.

To expand a TD given td, run the following steps:
  1. For each item in the TD default values table from [WOT-TD], if the term is not defined in td, add the term definition with the default value specified in [WOT-TD].

5.3 Validating a Thing Description

The [WOT-TD] specification defines how a TD should be validated. Therefore, this API expects the ThingDescription objects be validated before used as parameters. This specification defines a basic TD validation as follows.

To validate a TD given td, run the following steps:
  1. If JSON Schema validation fails on td, throw a "TypeError" and stop.
Editor's note: Handling default values

Additional steps may be added to fill the default values of mandatory fields.

6. The WOT namespace

Defines the WoT API object as a singleton and contains the API methods, grouped by conformance classes.

WebIDL[SecureContext, Exposed=(Window,Worker)]
namespace WOT {
  // methods defined in UA conformance classes
};

6.1 The consume() method

WebIDLpartial namespace WOT {
  Promise<ConsumedThing> consume(ThingDescription td);
};
Belongs to the WoT Consumer conformance class. Expects an td argument and returns a Promise that resolves with a ConsumedThing object that represents a client interface to operate with the Thing. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. Let thing be a new ConsumedThing object constructed from td.
  4. Set up the WoT Interactions based on introspecting td as explained in [WOT-TD] and [WOT-PROTOCOL-BINDINGS]. Make a request to the underlying platform to initialize the Protocol Bindings.
    Editor's note

    Implementations encapsulate the complexity of how to use the Protocol Bindings for implementing WoT interactions. In the future elements of that could be standardized.

  5. Resolve promise with thing.
Editor's note

Note the difference between constructing ConsumedThing and using the consume() method: the latter also initializes the protocol bindings, whereas a simple constructed object will not have WoT Interactions initialized until they are invoked.

6.2 The produce() method

WebIDLtypedef object ExposedThingInit;

partial namespace WOT {
  Promise<ExposedThing> produce(ExposedThingInit init);
};
Belongs to the WoT Producer conformance class. Expects a init argument and returns a Promise that resolves with an ExposedThing object that extends ConsumedThing with a server interface, i.e. the ability to define request handlers. The init object is an instance of the ExposedThingInit type. Specifically, an ExposedThingInit value is a dictionary used for the initialization of an ExposedThing and it represents a Partial TD as described in the [WOT-ARCHITECTURE]. As such, it has the same structure of a Thing Description but it may omit some information. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. Let thing be a new ExposedThing object constructed with init.
  4. Resolve promise with thing.

6.2.1 Expand an ExposedThingInit

To expand an ExposedThingInit given init and obtain a valid td as a result, run the following steps:
  1. Run validate an ExposedThingInit on init. If that fails, throw SyntaxError and stop.
  2. Let td be the result of running clone given init.
  3. For each scheme in td.["securityDefinitions"], make a request to the underlying platform to check if it is supported by at least one Protocol Binding. If not, then remove scheme from td.
  4. If td.["security"] does not exist in td.["securityDefinitions"], then remove security from td.
  5. For each affordance in td.properties, td.actions and td.events, run the following sub-steps:
    1. For each form in affordance.forms:
      1. If form.contentType is not recognized by the runtime as valid remove contentType from form.
      2. If form.href has an unknown schema, remove href from form.
      3. If form.href is absolute and its authority it is not recognized by the runtime as valid, remove href from form.
      4. If form.href is already in use by other ExposedThings, remove href from form.
  6. Search for missing required properties in td accordingly to TD JSON Schema.
    Editor's note

    The editors find this step vague. It will be improved or removed in the next iteration.

  7. For each missing property run these sub-steps:
    1. If missing is title generate a runtime unique name and assign to title.
    2. If missing is @context assign the latest supported Thing Description context URI.
    3. If missing is instance assign the string 1.0.0.
    4. If missing is forms generate a list of Forms using the available Protocol Bindings and content types encoders. Then assign the obtained list to forms.
    5. If missing is security assign the label of the first supported SecurityScheme in securityDefinitions field. If no SecurityScheme is found generate a NoSecurityScheme called nosec and assign the string nosec to security.
      Issue 1

      The discussion about how to properly generate a value for security is still open. See issue #299

    6. If missing is href define formStub as the partial Form that does not have href. Generate a valid url using the first Protocol Binding that satisfy the requirements of formStub. Assign url to href. If not Protocol Binding can be found remove formStub from td.
    7. Add missing to td with value as value
  8. Run validate a TD on td. If that fails re-throw the error and stop
  9. Return td

6.2.2 Validating an ExposedThingInit

To validate an ExposedThingInit given init, run the following steps:
  1. Parse TD JSON Schema and load it in object called exposedThingInitSchema
  2. let optional be a list containing the following strings: title, @context, instance, forms, security, and href.
  3. For each property and sub-property key in exposedThingInitSchema equals to required execute the following steps:
    1. if key value is an Array then remove all its elements equal to the elements in optional
    2. if key value is a string then if value is equal to one of the elements in optional remove key from exposedThingInitSchema
  4. Return the result of validating an object with JSON Schema given init and exposedThingInitSchema.
    Editor's note

    The validating an object with JSON Schema steps are still under discussion. Currently this specification reference to the validation process of JSONSchema. Please follow this document when validating init with exposedThingInitSchema. Notice that the working group is evaluating an alternative formal approach.

6.3 The discover() method

WebIDLpartial namespace WOT {
  Promise<ThingDiscoveryProcess> discover(optional ThingFilter filter = {});
};
Belongs to the WoT Discovery conformance class. Starts the discovery process that will provide ThingDescription objects for Thing Descriptions that match an optional filter argument of type ThingFilter. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. If discovery is not supported by the implementation, reject promise with NotSupportedError and stop.
  4. Let discovery be a new ThingDiscoveryProcess object.
  5. Set discovery.[[filter]] to filter.
  6. Set discovery.[[url]] to undefined.
  7. If filters in general are not supported by the implementation and filter is not undefined or null, reject promise with NotSupportedError and stop.
  8. If discovery cannot be started by the underlying platform, reject promise with OperationError and stop.
  9. Request the underlying platform to start the discovery process by any means supported and provisioned in the WoT Runtime for which the script has access to, passing discovery to it.
  10. Resolve promise with discovery.

6.4 The exploreDirectory() method

WebIDLpartial namespace WOT {
  Promise<ThingDiscoveryProcess> exploreDirectory(USVString url,
      optional ThingFilter filter = {});
};
Belongs to the WoT Discovery conformance class. Starts the discovery process that given a TD Directory URL, will provide ThingDescription objects for Thing Descriptions that match an optional filter argument of type ThingFilter. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. If directory discovery is not supported by the implementation, reject promise with NotSupportedError and stop.
  4. Let discovery be a new ThingDiscoveryProcess object.
  5. Set discovery.[[url]] to url.
  6. Set discovery.[[filter]] to filter.
  7. Request the underlying platform to start the directory discovery process.
    Note

    This is a placeholder for more details in the discovery algorithm. Implementations should follow the procedures described in the [WOT-DISCOVERY] and [WOT-PROTOCOL-BINDINGS] specifications. Some normative steps are indicated below.

    1. If url is not a TD Directory or if the underlying implementation cannot support the Protocol Binding indicated by url, reject promise with NotSupportedError and terminate these steps.
    2. If filters in general are not supported by the implementation and filter is not undefined or null, reject promise with NotSupportedError and stop.
    3. Run the discovery process given discovery.
      Note

      From this point on, errors are recorded only on error, but don't affect promise any longer.

  8. Resolve promise with discovery.

6.5 The requestThingDescription() method

WebIDLpartial namespace WOT {
  Promise<ThingDescription> requestThingDescription(USVString url);
};
Belongs to the WoT Discovery conformance class. Requests a Thing Description from the given URL. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. If fetching a Thing Description is not supported by the implementation, reject promise with NotSupportedError and stop.
  4. Let td be the result of making a request to the underlying platform to retrieve the Thing Description using the Protocol Binding specified by url. If retrieving td fails, reject promise with NotFoundError and stop.
  5. Resolve promise with td.

7. Handling interaction data

As specified in the Web of Things (WoT) Thing Description 1.1 specification, WoT interactions extend DataSchema and include a number of possible Forms, out of which one is selected for the interaction. The Form contains a contentType to describe the data. For certain content types, a DataSchema is defined, based on JSON Schema, making possible to represent these contents as JavaScript types and eventually set range constraints on the data.

7.1 The InteractionInput type

WebIDLtypedef any DataSchemaValue;
typedef (ReadableStream or DataSchemaValue) InteractionInput;

Belongs to the WoT Consumer conformance class and represents the WoT Interaction data provided by application scripts to the UA.

DataSchemaValue is an ECMAScript value that is accepted for DataSchema defined in [WoT-TD]. The possible values MUST be of type null, boolean, number, string, array, or object.

ReadableStream is meant to be used for WoT Interactions that don't have a DataSchema in the Thing Description, only a Form's contentType that can be represented by a stream.

In practice, any ECMAScript value may be used for WoT Interactions that have a DataSchema defined in the Thing Description, or which can be mapped by implementations to the Form's contentType defined in the Thing Description.

The algorithms in this document specify how exactly input data is used in WoT Interactions.

7.2 The InteractionOutput interface

Belongs to the WoT Consumer conformance class. An InteractionOutput object is always created by the implementations and exposes the data returned from WoT Interactions to application scripts.

This interface exposes a convenience function which should cover the vast majority of IoT use cases: the value() function. Its implementation will inspect the data, parse it if adheres to a DataSchema, or otherwise fail early, leaving the underlying stream undisturbed so that application scripts could attempt reading the stream themselves, or handling the data as ArrayBuffer.

WebIDL[SecureContext, Exposed=(Window,Worker)]
interface InteractionOutput {
  readonly attribute ReadableStream? data;
  readonly attribute boolean dataUsed;
  readonly attribute Form? form;
  readonly attribute DataSchema? schema;
  Promise<ArrayBuffer> arrayBuffer();
  Promise<DataSchemaValue> value();
};

The data property represents the raw payload in WoT Interactions as a ReadableStream, initially null.

The dataUsed property tells whether the data stream has been disturbed. Initially false.

The form attribute represents the Form selected from the Thing Description for this WoT Interaction, initially null.

The schema attribute represents the DataSchema (defined in [WoT-TD]) of the payload as a JSON object, initially null.

The [[value]] internal slot represents the parsed value of the WoT Interaction, initially undefined (note that null is a valid value).

7.2.1 The value() function

Parses the data returned by the WoT Interaction and returns a value with the type described by the interaction DataSchema if that exists, or by the contentType of the interaction Form. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If this.[[value]] is not undefined, resolve promise with that value and stop.
  3. If this.data is not a ReadableStream or if dataUsed is true, or if form is not an object or if schema or its type are null or undefined, then reject promise with NotReadableError and stop.
  4. If form.contentType is not application/json and if a mapping is not available in the Protocol Bindings from form.contentType to [JSON-SCHEMA], reject promise with NotSupportedError and stop.
  5. Let reader be the result of getting a reader from data. If that threw an exception, reject promise with that exception and stop.
  6. Let bytes be the result of reading all bytes from data with reader.
  7. Set dataUsed to true.
  8. If form.contentType is not application/json and if a mapping is available in the Protocol Bindings from form.contentType to [JSON-SCHEMA], transform bytes with that mapping.
  9. Let json be the result of running parse JSON from bytes on bytes. If that throws, reject promise with that exception and stop.
  10. Set [[value]] to the result of running check data schema on json and schema. If that throws, reject promise with that exception and stop.
  11. Resolve promise with [[value]].

7.2.2 The arrayBuffer() function

When invoked, MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If data is not ReadableStream or if dataUsed is true, reject promise with NotReadableError and stop.
  3. Let reader be the result of getting a reader from data. If that threw an exception, reject promise with that exception and stop.
  4. Let bytes be the result of reading all bytes from data with reader.
  5. Set dataUsed to true.
  6. Let arrayBuffer be a new ArrayBuffer whose contents are bytes. If that throws, reject promise with that exception and stop.
  7. Resolve promise with arrayBuffer.

7.2.3 The check data schema algorithm

To run the check data schema steps on payload and schema,
  1. Let type be schema.type.
  2. If type is "null" and if payload is not null, throw TypeError and stop, otherwise return null.
  3. If type is "boolean" and payload is a falsy value or its byte length is 0, return false, otherwise return true.
  4. If type is "integer" or "number",
    1. If payload is not a number, throw TypeError and stop.
    2. If form.minimum is defined and payload is smaller, or if form.maximum is defined and payload is bigger, throw a RangeError and stop.
  5. If type is "string", return payload.
  6. If type is "array", run these sub-steps:
    1. If payload is not an array, throw TypeError and stop.
    2. If form.minItems is defined and payload.length is less than that, or if form.maxItems is defined and payload.length is more than that, throw RangeError and stop.
    3. Let payload be an array of items obtained by running the check data schema steps on each element item of payload and schema.items. If this throws at any stage, re-throw that exception and stop.
  7. If type is "object", run these sub-steps:
    1. If payload or schema.properties is not an object, throw TypeError and stop.
    2. For each key in payload:
      1. Let prop be payload[key].
      2. Let propSchema be interaction.properties[key].
      3. Let prop be the result of running the check data schema steps on prop and propSchema. If this throws, re-throw that exception and stop.
    3. Let required be schema.required if that is an array or an empty array otherwise.
    4. For each key in required, if key is not present in payload, throw SyntaxError and stop.
  8. Return payload.

7.2.4 The create interaction request algorithm

For a given ConsumedThing object thing, in order to create interaction request given a source, form and schema, run these steps:
  1. Let idata be a new an InteractionOutput object.
  2. Set idata.form to form, set idata.schema to schema, set |idata.data to null and set idata.[[value]] to undefined.
  3. If source is a ReadableStream object, let idata.data be source, return idata and stop.
  4. If schema and its type are defined and not null, run these sub-steps:
    1. If type is "null" and source is not "null", throw TypeError and stop.
    2. If type is "boolean" and source is a falsy value, set idata.[[value]] to false, otherwise set it to true.
    3. If type is "integer" or "number" and source is not a number, or if form.minimum is defined and source is smaller, or if form.maximum is defined and source is bigger, throw RangeError and stop.
    4. If type is "string" and source is not a string, let idata.[[value]] be the result of running serialize JSON to bytes given source. If that is failure, throw SyntaxError and stop.
    5. If type is "array", run these sub-steps:
      1. If source is not an array, throw a TypeError and stop.
      2. Let length be the length of source.
      3. If form.minItems is defined and length is less than that, or if form.maxItems is defined and length is more than that, throw RangeError and stop.
      4. For each item in source, let itemschema be schema.items and let item be the result of running the create interaction request steps given item, form and itemschema. If this throws, re-throw that exception and stop.
      5. Set data.[[value]] to source.
    6. If type is "object", run these sub-steps:
      1. If source is not an object, throw TypeError and stop.
      2. If schema.properties is not an object, throw TypeError and stop.
      3. For each key in source,
        1. Let value be source[key].
        2. Let propschema be properties.interactions[key].
        3. Let value be the result of running the create interaction request steps on value, form and propschema. If this throws, re-throw that exception and stop.
      4. If schema.required is an array, for each item in required check if item is a property name in source. If an item is not found in source, throw SyntaxError and stop.
      5. Set data.[[value]] to source.
  5. Set idata.data to a new ReadableStream created from idata.[[value]] internal slot as its underlying source.
  6. Return idata.

7.2.5 The parse interaction response algorithm

For a given ConsumedThing object thing, in order to parse interaction response given response, form and schema, run these steps:
  1. Let result be a new InteractionOutput object.
  2. Let result.schema be schema.
  3. Let result.form be form.
  4. Let result.data be a new ReadableStream with the payload data of response as its underlying source.
  5. Let result.dataUsed be false.
  6. Return result.

As illustrated in the next pictures, the InteractionOutput interface is used every time implementations provide data to scripts, while InteractionInput is used when the scripts pass data to the implementation.

Figure 1 Data structures used when reading data

When a ConsumedThing reads data, it receives it from the implementation as an InteractionOutput object.

An ExposedThing read handler provides the read data to the implementation as InteractionInput.

Figure 2 Data structures used when writing data

When a ConsumedThing writes data, it provides it to the implementation as InteractionInput.

An ExposedThing write handler receives data from to implementation as an InteractionOutput object.

Figure 3 Data structures used when invoking an Action

When a ConsumedThing invokes an Action, it provides the parameters as InteractionInput and receives the output of the Action as an InteractionOutput object.

An ExposedThing action handler receives arguments from the implementation as an InteractionOutput object and provides Action output as InteractionInput to the implementation.

7.4 Error handling

The algorithms in this API define the errors to be reported to application scripts.

The errors reported to the other communication end are mapped and encapsulated by the Protocol Bindings.

Figure 4 Error handling in WoT interactions
Editor's note

This topic is still being discussed in Issue #200. A standardized error mapping would be needed in order to ensure consistency in mapping script errors to protocol errors and vice versa. In particular, when algorithms say "error received from the Protocol Bindings", that will be factored out as an explicit error mapping algorithm. Currently, that is encapsulated by implementations.

8. The ConsumedThing interface

Represents a client API to operate a Thing. Belongs to the WoT Consumer conformance class.

WebIDL[SecureContext, Exposed=(Window,Worker)]
interface ConsumedThing {
  constructor(ThingDescription td);
  Promise<InteractionOutput> readProperty(DOMString propertyName,
                              optional InteractionOptions options = {});
  Promise<PropertyReadMap> readAllProperties(
                              optional InteractionOptions options = {});
  Promise<PropertyReadMap> readMultipleProperties(
                              sequence<DOMString> propertyNames,
                              optional InteractionOptions options = {});
  Promise<undefined> writeProperty(DOMString propertyName,
                              InteractionInput value,
                              optional InteractionOptions options = {});
  Promise<undefined> writeMultipleProperties(
                              PropertyWriteMap valueMap,
                              optional InteractionOptions options = {});
  /*Promise<undefined> writeAllProperties(
                              PropertyWriteMap valueMap,
                              optional InteractionOptions options = {});*/
  Promise<InteractionOutput> invokeAction(DOMString actionName,
                              optional InteractionInput params = {},
                              optional InteractionOptions options = {});
  Promise<Subscription> observeProperty(DOMString name,
                              InteractionListener listener,
                              optional ErrorListener onerror,
                              optional InteractionOptions options = {});
  Promise<Subscription> subscribeEvent(DOMString name,
                              InteractionListener listener,
                              optional ErrorListener onerror,
                              optional InteractionOptions options = {});
  ThingDescription getThingDescription();
};

dictionary InteractionOptions {
  unsigned long formIndex;
  object uriVariables;
  any data;
};

[SecureContext, Exposed=(Window,Worker)]
interface Subscription {
  readonly attribute boolean active;
  Promise<undefined> stop(optional InteractionOptions options = {});
};

[SecureContext, Exposed=(Window,Worker)]
interface PropertyReadMap {
  readonly maplike<DOMString, InteractionOutput>;
};

[SecureContext, Exposed=(Window,Worker)]
interface PropertyWriteMap {
  readonly maplike<DOMString, InteractionInput>;
};

callback InteractionListener = undefined(InteractionOutput data);
callback ErrorListener = undefined(Error error);
Editor's note: Where is the writeAllProperties method?

The writeAllProperties() method is still under discussion. Meanwhile, use the writeMultipleProperties() method instead.

8.1 Internal slots for ConsumedThing

A ConsumedThing object has the following internal slots:

Internal Slot Initial value Description (non-normative)
[[td]] null The Thing Description of the ConsumedThing.
[[activeSubscriptions]] {} An ordered map keyed on a string name representing the Event and value is a Subscription object.
[[activeObservations]] {} An ordered map keyed on a string name representing a Property and value is a Subscription object.

8.2 Constructing ConsumedThing

After fetching a Thing Description as a JSON object, one can create a ConsumedThing object.

To create ConsumedThing with the ThingDescription td, run the following steps:
  1. Run the validate a TD steps on td. If that fails, throw SyntaxError and stop.
  2. Run the expand a TD steps on td. If that fails, re-throw the error and stop.
  3. Let thing be a new ConsumedThing object.
  4. Set the internal slot [[td]] of thing to td.
  5. Return thing.

8.3 The getThingDescription() method

Returns the [[td]] of the ConsumedThing object that represents the Thing Description of the ConsumedThing. Applications may consult the Thing metadata stored in [[td]] in order to introspect its capabilities before interacting with it.

8.4 The readProperty() method

Reads a Property value. Takes as arguments propertyName and optionally options. It returns a Promise that resolves with a Property value represented as an InteractionOutput object or rejects on error. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. Let interaction be [[td]].properties.propertyName.
  4. If interaction is undefined, reject promise with a NotFoundError and stop.
  5. If option.formIndex is defined, let form be the Form associated with formIndex in interaction.forms array, otherwise let form be a Form in interaction.forms whose op is readproperty, selected by the implementation.
  6. If form is failure, reject promise with a SyntaxError and stop.
  7. Make a request to the underlying platform (via the Protocol Bindings) to retrieve the value of the propertyName Property using form and the optional URI templates given in options.uriVariables.
  8. If the request fails, reject promise with the error received from the Protocol Bindings and stop.
  9. Let response be the response received to the request.
  10. Let data be the result of running parse interaction response on response, form and interaction. If that fails, reject promise with a SyntaxError and stop.
  11. Resolve promise with data.

8.5 The readMultipleProperties() method

Reads multiple Property values with one request. Takes as arguments propertyNames and optionally options. It returns a Promise that resolves with a PropertyReadMap object that maps keys from propertyNames to values returned by this algorithm. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. If option.formIndex is defined, let form be the Form associated with formIndex in the [[td]].forms array, otherwise let form be the Form in [[td]].forms array whose op is readmultipleproperties, as selected by the implementation.
  4. If form is failure, reject promise with a SyntaxError and stop.
  5. Let result be an object and for each string name in propertyNames add a property with key name and the value null.
  6. Make a request to the underlying platform (via the Protocol Bindings) to retrieve the Property values given by propertyNames with form and optional URI templates given in options' uriVariables.
  7. If this cannot be done with a single request with the Protocol Bindings, reject promise with a NotSupportedError and stop.
  8. Process the response and for each key in result, run the following sub-steps:
    1. Let value be result[key].
    2. Let schema be this.[[td]].properties[key].
    3. Let property be the result of running parse interaction response on value, form and schema.
  9. If the above step throws at any point, reject promise with that exception and stop.
  10. Resolve promise with result.

8.6 The readAllProperties() method

Reads all properties of the Thing with one request. Takes options as optional argument. It returns a Promise that resolves with a PropertyReadMap object that maps keys from Property names to values returned by this algorithm. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. Let forms be subscription.[[interaction]].forms.
  4. If forms is undefined, reject promise with a SyntaxError and stop.
  5. If option.formIndex is not undefined and is less than forms.length, set subscription.[[form]] to forms.[formIndex].
  6. Otherwise, set subscription.[[form]] to a Form in forms whose op is "readallproperties", as selected by the implementation.
  7. If subscription.[[form]] is failure, reject promise with a SyntaxError and stop.
  8. Make a request to the underlying platform using the Protocol Bindings to retrieve all the Property definitions from the TD given form and optional URI templates in options.uriVariables.
  9. If this cannot be done with a single request with the Protocol Bindings of the Thing, then reject promise with a NotSupportedError and stop.
  10. If the request fails, reject promise with the error received from the Protocol Bindings and stop.
  11. Process the reply and let result be an object with the keys and values obtained in the reply.
  12. Process the response and for each key in result, run the following sub-steps:
    1. Let value be result[key].
    2. Let schema be this.[[td]].properties[key].
    3. Let property be the result of running parse interaction response on value, form and schema.
  13. Resolve promise with result.

8.7 The writeProperty() method

Writes a single Property. Takes as arguments propertyName, value and optionally options. It returns a Promise that resolves on success and rejects on failure. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. Let interaction be this.[[td]].properties[propertyName].
  4. If interaction is undefined, reject promise with a NotFoundError and stop.
  5. If option.formIndex is not undefined, let form be the Form associated with formIndex in the interaction.forms array, otherwise let form be a Form in interaction.forms whose op is writeproperty, as selected by the implementation.
  6. If form is failure, reject promise with a SyntaxError and stop.
  7. Let data be the result of running the create interaction request steps given value, form and interaction. If that throws, reject promise with that exception and stop.
  8. Make a request to the underlying platform (via the Protocol Bindings) to write the Property given by propertyName using data and the optional URI templates given in options' uriVariables.
  9. If the request fails, reject promise with the error received from the Protocol Bindings and stop.
  10. Otherwise resolve promise.
Editor's note

As discussed in Issue #193, the design decision is that write interactions only return success or error, not the written value (optionally). TDs should capture the schema of the Property values, including precision and alternative formats. When a return value is expected from the interaction, an Action should be used instead of a Property.

8.8 The writeMultipleProperties() method

Writes a multiple Property values with one request. Takes as arguments properties - as an object with keys being Property names and values as Property values - and optionally options. It returns a Promise that resolves on success and rejects on failure. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. If option.formIndex is defined, let form be the Form associated with formIndex in the [[td]].forms array, otherwise let form be a Form in [[td]].forms array whose op is writemultipleproperties, as selected by the implementation.
  4. If form is failure, reject promise with a SyntaxError and stop.
  5. Let propertyNames be an array of string with as elements the keys of the properties object.
  6. For each name in propertyNames, let property be this.[[td]].properties[name].
  7. If property is null or undefined or is not writeable reject promise with NotSupportedError and stop.
  8. Let result be an object and for each string name in propertyNames add a property with key name and let its value be null.
  9. Let schemas be an object and for each name in propertyNames add a property with key name and let its value be this.[[td]].properties[name].
  10. For each key key in properties, run the create interaction request steps given properties[key], form and the value for schema[key]. If that throws for any name, reject promise with that exception and stop.
  11. Make a single request to the underlying platform (via the Protocol Bindings) to write each Property provided in properties with optional URI templates given in options' uriVariables.
  12. If this cannot be done with a single request with the Protocol Bindings of the Thing, then reject promise with a NotSupportedError and stop.
  13. If the request fails, return the error received from the Protocol Bindings and stop.
  14. Otherwise resolve promise.

8.9 The observeProperty() method

Makes a request for Property value change notifications. Takes as arguments propertyName, listener and optionally onerror and options. It returns a Promise that resolves on success and rejects on failure.
Editor's note

This algorithm allows for only one active Subscription per Property. If a new Subscription is made while an existing Subscription is active the runtime will throw an NotAllowedError.

The method MUST run the following steps:
  1. Let thing be the reference of this ConsumedThing object.
  2. Return a Promise promise and execute the next steps in parallel.
  3. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  4. If listener is not a Function, reject promise with a TypeError and stop.
  5. If onerror is not null and is not a Function, reject promise with a TypeError and stop.
  6. If thing.[[activeObservations]][propertyName] [=map/exists], reject promise with a NotAllowedError and stop.
  7. Let subscription be a new Subscription object with its internal slots set as follows:
  8. Make a request to the underlying platform to observe the Property identified by propertyName with form and optional URI templates given in options' uriVariables.
  9. If the request fails, reject promise with the error received from the Protocol Bindings and stop.
  10. Set thing.[[activeObservations]][|propertyName] to subscription and resolve promise.
  11. Whenever the underlying platform detects a notification for this subscription keyed on propertyName with a new Property value value, run the following sub-steps:
  12. Whenever the underlying platform detects an error for this subscription, run the following sub-steps:
    • If the error is irrecoverable and stops the subscription, set subscription.active to false and suppress further notifications.
    • Let error be a new NetworkError and set its message to reflect the underlying error condition.
    • If onerror is a Function, invoke it given error.

8.10 The invokeAction() method

Makes a request for invoking an Action and return the result. Takes as arguments actionName, optionally params and optionally options. It returns a Promise that resolves with the result of the Action represented as an InteractionOutput object, or rejects with an error. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. Let interaction be this.[[td]].actions[actionName].
  4. If interaction is not an object, reject promise with a NotFoundError and stop.
  5. Let forms be subscription.[[interaction]].forms.
  6. If forms is undefined, reject promise with a SyntaxError and stop.
  7. If option.formIndex is not undefined and is less than forms.length, set subscription.[[form]] to forms.[formIndex].
  8. Otherwise, set subscription.[[form]] to a Form in forms whose op is "invokeaction", as selected by the implementation.
  9. If subscription.[[form]] is failure, reject promise with a SyntaxError and stop.
  10. Let args be the result of running the create interaction request steps on params, form and interaction. If that throws, reject promise with that exception and stop.
  11. Make a request to the underlying platform (via the Protocol Bindings) to invoke the Action identified by actionName given args and options.uriVariables.
  12. If the request fails locally or returns an error over the network, reject promise with the error received from the Protocol Bindings and stop.
  13. Let value be the reply returned in the reply.
  14. Let result be the result of running parse interaction response with value, form and interaction. If that throws, reject promise with that exception and stop.
  15. Resolve promise with result.

8.11 The subscribeEvent() method

Makes a request for subscribing to Event notifications. Takes as arguments eventName, listener and optionally onerror and options. It returns a Promise to signal success or failure.
Editor's note

This algorithm allows for only one active Subscription per Event. If a new Subscription is made while an existing Subscription is active the runtime will throw an NotAllowedError.

The method MUST run the following steps:
  1. Let thing be the reference of this ConsumedThing object.
  2. Return a Promise promise and execute the next steps in parallel.
  3. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  4. If listener is not a Function, reject promise with a TypeError and stop.
  5. If onerror is not null and is not a Function, reject promise with a TypeError and stop.
  6. If thing.[[activeSubscriptions]][eventName] does not exist, reject promise with a NotAllowedError and stop.
  7. Let subscription be a new Subscription object with its internal slots set as follows:
  8. Make a request to the underlying platform via the Protocol Bindings to subscribe to the Event identified by eventName with [[form]], optional URI templates given in options.uriVariables and optional subscription data given in options.data.
  9. If the request fails, reject promise with the error received from the Protocol Bindings and stop.
  10. Set eventName to thing.[[activeSubscriptions]][eventName] to subscription.
  11. Resolve promise.
  12. Whenever the underlying platform detects a notification for the Event subscription keyed on eventName, run the following sub-steps:
    1. Invoke listener given the result of running parse interaction response on the data provided with the Event, subscription.[[form]] and subscription.[[interaction]].
  13. Whenever the underlying platform detects an error for Event subscription keyed on eventName, run the following sub-steps:
    • If the error is irrecoverable and stops the subscription, set subscription.active to false and suppress further notifications.
    • Let error be a new NetworkError and set its message to reflect the underlying error condition.
    • If onerror is a Function, invoke it given error.

8.12 The InteractionOptions dictionary

Holds the interaction options that need to be exposed for application scripts according to the Thing Description.

The formIndex property, if defined, represents an application hint for which Form definition, identified by this index, of the TD to use for the given WoT interaction. Implementations SHOULD use the Form with this index for making the interaction, but MAY override this value if the index is not found or not valid. If not defined, implementations SHOULD attempt to use the Form definitions in order of appearance as listed in the TD for the given Wot Interaction.

The uriVariables property if defined, represents the URI template variables to be used with the WoT Interaction that are represented as parsed JSON objects defined in [WOT-TD].

Editor's note

The support for URI variables comes from the need, exposed by the Web of Things (WoT) Thing Description 1.1 specification, to be able to describe existing RESTful endpoints that use them. However, it should be possible to write a Thing Description that would use Actions for representing this kind of interactions and model the URI variables as action parameters. In that case, implementations can serialize the parameters as URI variables, and therefore, the options parameter could be dismissed.

The data property if defined, represents additional opaque data that needs to be passed to the interaction.

8.13 The PropertyReadMap type

Represents a map of Property names to an InteractionOutput object that represents the value the Property can take. It is used as a property bag for interactions that involve multiple Properties at once.

8.14 The PropertyWriteMap type

Represents a map of Property names to an InteractionInput that represents the value the Property can take. It is used as a property bag for interactions that involve multiple Properties at once.

8.15 The InteractionListener callback

User provided callback that is given an argument of type InteractionOutput and is used for observing Property changes and handling Event notifications. Since subscribing to Events are WoT interactions and might take options or even data, they are not modelled with software events.

8.16 The ErrorListener callback

User provided callback that is given an argument of type Error and is used for conveying critical and non-critical errors from the Protocol Bindings to applications.

8.17 The Subscription interface

Represents a subscription to Property change and Event interactions.

The active boolean property denotes if the subscription is active, i.e. it is not stopped because of an error or because of invocation of the stop() method.

8.17.1 Internal slots for Subscription

A Subscription object has the following internal slots:
Internal Slot Initial value Description (non-normative)
[[type]] null Indicates what WoT Interaction the Subscription refers to. The value can be either "property" or "event" or null.
[[name]] null The Property or Event name.
[[interaction]] null The Thing Description fragment that describes the WoT interaction.
[[form]] null The Form associated with the subscription.
[[thing]] null The ConsumedThing associated with the subscription.

8.17.2 The stop() method

Stops delivering notifications for the subscription. It takes an optional parameter options and returns a Promise. When invoked, the method MUST execute the following steps:

  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. If options' formIndex is defined, let unsubscribeForm be the Form associated with formIndex in [[interaction]]'s forms array.
  4. Otherwise let unsubscribeForm be the result of running the find a matching unsubscribe form algorithm given [[form]].
  5. If unsubscribeForm is failure, reject promise with a SyntaxError and stop.
  6. If [[type]] is "property", make a request to the underlying platform via the Protocol Bindings to stop observing the Property identified by [[name]] with unsubscribeForm and optional URI templates given in options' uriVariables.
  7. Otherwise, if [[type]] is "event", make a request to the underlying platform via the Protocol Bindings to unsubscribe from the Event identified by [[name]] with unsubscribeForm, with optional URI templates given in options' uriVariables and optional unsubscribe data given in options.data.
  8. If the request fails, reject promise with the error received from the Protocol Bindings and stop.
  9. Otherwise:
  10. If the underlying platform receives further notifications for this subscription, implementations SHOULD silently suppress them.

8.17.3 Finding an unsubscribe Form

Note

This algorithm is under development and is non-normative. Implementations MAY choose another algorithm to find a matching unsubscribe Form to a given subscribe Form.

To find a matching unsubscribe form given subscribeForm in the context of a Subscription object, run the following steps:
  1. Let results be an empty array.
  2. For each form in [[interaction]].forms,
    1. Add an internal slot [[matchLevel]] on form and set its value to 0.
    2. If form.op is "unobserveproperty" if [[type]] is "property" or if form.op is "unsubscribeevent" if [[type]] is"event",
      1. Set the internal slot [[matchLevel]] on form to 1 and add form to results.
      2. If form.href and [[subscribeForm]].href are same origin-domain, increment form.[[matchLevel]].
      3. If form.contentType is equal to [[subscribeForm]]'s contentType and form.[[matchLevel]] is greater than 2, increment form.[[matchLevel]].
  3. If results is empty, return null and terminate these steps.
  4. Return the first form in results that has the highest [[matchLevel]] value.

8.18 ConsumedThing Examples

The next example illustrates how to fetch a TD by URL, create a ConsumedThing, read metadata (title), read property value, subscribe to property change, subscribe to a WoT event, unsubscribe.

Example 2: Thing Client API example with data value
try {
  let res = await fetch("https://tds.mythings.org/sensor11");
  let td = res.json();
  let thing = new ConsumedThing(td);
  console.log("Thing " + thing.getThingDescription().title + " consumed.");
} catch (e) {
  console.log("TD fetch error: " + e.message);
};

try {
  // subscribe to property change for “temperature”
  await thing.observeProperty("temperature", async (data) => {
    try {
      console.log("Temperature changed to: " + await data.value());
    } catch (error) {
      console.error("Cannot read the observed property temperature");
      console.error(error);
    }
  });
  // subscribe to the “ready” event defined in the TD
  await thing.subscribeEvent("ready", async (eventData) => {
    try {
      console.log("Ready; index: " + await eventData.value());
      // run the “startMeasurement” action defined by TD
      await thing.invokeAction("startMeasurement", { units: "Celsius" });
      console.log("Measurement started.");
    } catch (error) {
      console.error("Cannot read the ready event or startMeasurement failed");
      console.error(error)
    }
  });
} catch (e) {
  console.log("Error starting measurement.");
}

setTimeout(async () => {
  try {
    const temperatureData = await thing.readProperty("temperature")
    const temperature = await temperatureData.value();
    console.log("Temperature: " + temperature);

    await thing.unsubscribe("ready");
    console.log("Unsubscribed from the ‘ready’ event.");
  } catch (error) {
    console.log("Error in the cleanup function");
  }
}, 10000);

The following shows an advance usage of InteractionOutput to read a property without a DataSchema.

Example 3: Thing Client API example with arrayBuffer
/*
* takePicture affordance form:
* "form": {
*   "op": "invokeaction",
*   "href" : "http://camera.example.com:5683/takePicture",
*   "response": {
*     "contentType": "image/jpeg",
*     "contentCoding": "gzip"
*   }
*}
* See https://www.w3.org/TR/wot-thing-description/#example-23
*/
let response;
let image;
try {
  response = await thing.invokeAction(“takePicture”));
  image = await response.value() // throws NotReadableError --> schema not defined
} catch(ex) {
  image = await response.arrayBuffer();
  // image: ArrayBuffer [0x1 0x2 0x3 0x5 0x15 0x23 ...]
}

Finally, the next two examples shows the usage of a ReadableStream from an InteractionOutput.

Example 4: Thing Client API example with readable stream (e.g., video stream)
/*{
"video": {
  "description" : "the video stream of this camera",
  "forms": [
    {
      "op": "readproperty",
      "href": "http://camera.example.com/live",
      "subprotocol": "hls"
      "contentType": "video/mp4"
    }
  ]
}}*/

const video = await thing.readProperty("video")
const reader = video.data.getReader()
reader.read().then(function processVideo({ done, value }) {
  if (done) {
    console.log("live video stoped");
    return;
  }
  const decoded = decode(value)
  UI.show(decoded)
  // Read some more, and call this function again
  return reader.read().then(processText);
});

Here consider that the JSON object is too big to be read wholly in the memory. Therefore, we use streaming processing to get the total number of the events recorded by the remote Web Thing.

Example 5: Thing Client API example with readable stream (e.g., counting json objects)
/*
* "eventHistory":
* {
*   "description" : "A long list of the events recorded by this thing",
*   "type": "array",
*   "forms": [
*     {
*       "op": "readproperty",
*       "href": "http://recorder.example.com/eventHistory",
*     }
*   ]
* }
*/

// Example of streaming processing: counting json objects
let objectCounter = 0
const parser = new Parser() //User library for json streaming parsing (i.e. https://github.com/uhop/stream-json/wiki/Parser)

parser.on('data', data => data.name === 'startObject' && ++objectCounter);
parser.on('end', () => console.log(`Found ${objectCounter} objects.`));

const response = await thing.readProperty(“eventHistory”)
await response.data.pipeTo(parser);

// Found N objects

9. The ExposedThing interface

The ExposedThing interface is the server API to operate the Thing that allows defining request handlers, Property, Action, and Event interactions.

WebIDL[SecureContext, Exposed=(Window,Worker)]
interface ExposedThing {
  ExposedThing setPropertyReadHandler(DOMString name,
          PropertyReadHandler handler);
  ExposedThing setPropertyWriteHandler(DOMString name,
          PropertyWriteHandler handler);
  ExposedThing setPropertyObserveHandler(DOMString name,
          PropertyReadHandler handler);
  ExposedThing setPropertyUnobserveHandler(DOMString name,
          PropertyReadHandler handler);
  Promise<undefined> emitPropertyChange(DOMString name,
          optional InteractionInput data);

  ExposedThing setActionHandler(DOMString name, ActionHandler action);

  ExposedThing setEventSubscribeHandler(DOMString name,
          EventSubscriptionHandler handler);
  ExposedThing setEventUnsubscribeHandler(DOMString name,
          EventSubscriptionHandler handler);
  Promise<undefined> emitEvent(DOMString name,
          optional InteractionInput data);

  Promise<undefined> expose();
  Promise<undefined> destroy();

  ThingDescription getThingDescription();
};

callback PropertyReadHandler = Promise<InteractionInput>(
        optional InteractionOptions options = {});

callback PropertyWriteHandler = Promise<undefined>(
        InteractionOutput value,
        optional InteractionOptions options = {});

callback ActionHandler = Promise<InteractionInput>(
        InteractionOutput params,
        optional InteractionOptions options = {});

callback EventSubscriptionHandler = Promise<undefined>(
        optional InteractionOptions options = {});

9.1 Internal slots for ExposedThing

An ExposedThing object has the following internal slots:

Internal Slot Initial value Description (non-normative)
[[td]] null The Thing Description of the ExposedThing.
[[readHandlers]] {} A Map with property names as keys and PropertyReadHandlers as values
[[writeHandlers]] {} A Map with property names as keys and PropertyWriteHandlers as values
[[observeHandlers]] {} A Map with property names as keys and PropertyReadHandlers as values
[[unobserveHandlers]] {} A Map with property names as keys and Functions as values
[[actionHandlers]] {} A Map with action names as keys and ActionHandlers as values
[[subscribeHandlers]] {} A Map with event names as keys and EventSubscriptionHandlers as values
[[unsubscribeHandlers]] {} A Map with event names as keys and EventSubscriptionHandlers as values
[[propertyObservers]] {} A Map with property names as keys and an Array of listeners as values
[[eventListeners]] {} A Map with event names as keys and Array of listeners as values

9.2 Constructing ExposedThing

The ExposedThing interface extends ConsumedThing. It is constructed from a full or partial ThingDescription object.

Note

Note that an existing ThingDescription object can be optionally modified (for instance by adding or removing elements on its properties, actions and events internal properties) and the resulting object can used for constructing an ExposedThing object. This is the current way of adding and removing Property, Action and Event definitions, as illustrated in the examples.

Note

Before invoking expose(), the ExposedThing object does not serve any requests. This allows first constructing ExposedThing and then initialize its Properties and service handlers before starting serving requests.

To construct an ExposedThing with the ExposedThingInit init, run the following steps:
  1. If invoking this method is not allowed for the current scripting context for security reasons, throw a SecurityError and stop.
  2. Run the expand an ExposedThingInit steps on init. if that fails re-throw the error and stop. Otherwise store the obtained td
  3. Run the expand a TD steps on td. If that fails, re-throw the error and stop.
  4. Let thing be a new ExposedThing object.
  5. Set the [[td]] of thing to td.
  6. Return thing.

9.3 The getThingDescription() method

Returns the [[td]] of the ExposedThing object that represents the Thing Description of the Thing. Applications may consult the Thing metadata stored in [[td]] in order to introspect its capabilities before interacting with it.

9.4 The PropertyReadHandler callback

A function that is called when an external request for reading a Property is received and defines what to do with such requests. It returns a Promise and resolves with an ReadableStream object or an ECMAScript value conforming to DataSchema, or rejects with an error.

9.5 The setPropertyReadHandler() method

Takes as arguments name and handler. Sets the service handler that defines what to do when a request is received for reading the specified Property matched by name. Throws on error. Returns a reference to this object for supporting chaining.

Editor's note

Note that there is no need to register handlers for handling requests for reading multiple or all Properties. The request and reply are transmitted in a single network request, but the ExposedThing may implement them using multiple calls to the single read handler.

The handler callback function should implement reading a Property and SHOULD be called by implementations when a request for reading a Property is received from the underlying platform.

There MUST be at most one handler for any given Property, so newly added handlers MUST replace the previous handlers. If no handler is initialized for any given Property, implementations SHOULD implement a default property read handler based on the Thing Description provided in the [[td]] internal slot.

When the method is invoked given name and handler, implementations MUST run the following steps:
  1. If invoking this method is not allowed for the current scripting context for security reasons, throw a SecurityError and stop.
  2. If [[td]].properties[name] does not exist, throw NotFoundError and stop.
  3. Set this.[[readHandlers]][name] to handler.

9.6 Handling requests for reading a Property

When a network request for reading Property name is received by the implementation with options, run the following steps:
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. Let value be the result of running the read server property steps with name and options:
    1. Let interaction be [[td]].properties.name.
    2. If a Property with name does not exist, throw NotFoundError and stop.
    3. Let handler be null.
    4. If there is a user provided PropertyReadHandler in [[readHandlers]] internal slot for interaction, let handler be that.
    5. Otherwise, if there is a default read handler provided by the implementation, let handler be that.
    6. If handler is null, throw NotSupportedError and stop.
    7. Let value be the result of invoking handler given options. If that fails, throw the error and stop.
    8. Return value.
      Note

      The value returned here SHOULD either conform to DataSchema or it SHOULD be an ReadableStream object created by the handler.

  4. If the previous step has thrown an error, send the error back with the reply created by following the Protocol Bindings and stop.
  5. Serialize and add the returned value to the reply created by following the Protocol Bindings.

9.7 Handling requests for reading multiple Properties

When a network request for reading multiple Properties given in an object propertyNames is received with options, run the following read multiple properties steps on propertyNames and options:
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. For each property with key name defined in propertyNames,
    1. Let value be the result of running the read server property steps on name and options. If that throws, send back the error in the reply created by following the Protocol Bindings and stop.
    2. Set propertyNames.name to value.
  4. Reply to the request by sending a single reply created from propertyNames according to the Protocol Bindings.

9.8 Handling requests for reading all Properties

When a network request for reading all Properties is received with options, run the following steps:
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. Let properties be an object created with all properties defined in the Thing with values set to null.
  4. Run the read multiple properties steps on properties and options.

9.9 The setPropertyObserveHandler() method

Takes as arguments name and handler. Sets the service handler that defines what to do when a request is received for observing the specified Property matched by name. Throws on error. Returns a reference to this object for supporting chaining.

The handler callback function should implement reading a Property and resolve with an InteractionOutput object or reject with an error.

There MUST be at most one handler for any given Property, so newly added handlers MUST replace the previous handlers. If no handler is initialized for any given Property, implementations SHOULD implement a default property read handler based on the Thing Description.

When the method is invoked given name and handler, implementations MUST run the following steps:
  1. If invoking this method is not allowed for the current scripting context for security reasons, throw a SecurityError and stop.
  2. If this.[[td]].properties[name] does not exist, throw NotFoundError and stop.
  3. Set this[[observeHandlers]][name] to handler.

9.10 Handling Property observe requests

When a network request for observing a Property name is received by the implementation with options, run the following steps:
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. If this.[[td]].properties[name] does not exist, send back a NotFoundError in the reply and stop.
  4. Internally save the request sender information together with options and this.[[propertyObservers]][name], in order to be able to notify about Property value changes.
Note

Every time the value of property changes, emitPropertyChange() needs to be explicitly called by the application script.

9.11 The setPropertyUnobserveHandler() method

Takes as arguments name and handler. Sets the service handler that defines what to do when a request is received for unobserving the specified Property matched by name. Throws on error. Returns a reference to this object for supporting chaining.

The handler callback function should implement what to do when an unobserve request is received by the implementation.

There MUST be at most one handler for any given Property, so newly added handlers MUST replace the previous handlers. If no handler is initialized for any given Property, implementations SHOULD implement a default handler based on the Thing Description.

When the method is invoked given name and handler, implementations MUST run the following steps:
  1. If invoking this method is not allowed for the current scripting context for security reasons, throw a SecurityError and stop.
  2. If this.[[td]].properties[name] does not exist, throw NotFoundError and stop.
  3. Set this.[[unobserveHandlers]][name] to handler.

9.12 Handling Property unobserve requests

When a network request for unobserving a Property name with options is received by the implementation, run the following steps:
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. If this.[[td]].properties[name] does not exist, send back a NotFoundError in the reply and stop.
  4. Let handler be this.[[unobserveHandlers]][name];
  5. If handler is a Function, invoke that given options, then send back a reply following the Protocol Bindings and stop.
  6. Otherwise, if this.[[propertyObservers]][name] exists, remove it from this.[[propertyObservers]], send back a reply as defined in the Protocol Bindings and stop.
  7. Otherwise, send back a NotFoundError in the reply as defined in the Protocol Bindings and stop.

9.13 The emitPropertyChange() method

Takes the name argument, denoting a Property name, and optionally the data argument. Triggers emitting a notification to all observers of the specified Property with the data provided by the data argument, or if not provided, it is obtained by the observe handler or the by read handler associated with that Property. The method MUST run the following steps:
  1. Let promise be a new Promise.
  2. Return promise and execute the next steps in parallel.
  3. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  4. Let name be the first argument.
  5. Let property be this.[[td]].properties[name].
  6. If property is undefined, reject promise with NotFoundError and stop.
  7. Let data be the second argument.
  8. If data is undefined, run the following sub-steps:
    1. Let handler be null.
    2. If name does not exist in [[readHandlers]], reject promise and stop.
    3. Let handler be [[readHandlers]][name].
    4. If handler is null or undefined, reject promise and stop.
    5. Let handled be the result of invoking handler given null.
    6. If handled is rejected, then reject promise and stop.
    7. Otherwise if handled resolved with value, let data be value.
  9. For each observer in [[propertyObservers]][name], run the following sub-steps:
    1. Let options be the interaction options saved with observer.
    2. Request the underlying platform to create a reply from data and options according to the Protocol Bindings.
      Editor's note

      This clause needs expanding and/or refer to an algorithm in [WOT-PROTOCOL-BINDINGS].

    3. Send reply to observer.
  10. Resolve promise.

9.14 The PropertyWriteHandler callback

A function that is called when an external request for writing a Property is received and defines what to do with such requests. Takes as argument value and returns a Promise, resolved when the value of the Property - identified by the name provided when setting the handler has been updated -, or rejects with an error if the property is not found or the value cannot be updated.

Editor's note

Note that the code in this callback function can read the property before updating it in order to find out the old value, if needed. Therefore the old value is not provided to this function.

Note

The value is provided by implementations as an InteractionOutput object in order to be able to represent values that are not described by a DataSchema, such as streams.

9.15 The setPropertyWriteHandler() method

Takes as arguments name and handler. Sets the service handler that defines what to do when a request is received for writing the Property matched by name given when setting the handler. Throws on error. Returns a reference to this object for supporting chaining.

Note

Note that even for readonly Properties it is possible to specify a write handler, as explained in Issue 199. In this case, the write handler may define in an application-specific way to fail the request.

There MUST be at most one write handler for any given Property, so newly added handlers MUST replace the previous handlers. If no write handler is initialized for any given Property, implementations SHOULD implement default property update if the Property is writeable and notifying observers on change if the Property is observable, based on the Thing Description.

When the method is invoked given name and handler, implementations MUST run the following steps:
  1. If invoking this method is not allowed for the current scripting context for security reasons, throw a SecurityError and stop.
  2. If this.[[td]].properties[name] does not exist, throw NotFoundError and stop.
  3. Set this.[[writeHandlers]][name] to handler.

9.16 Handling requests for writing a Property

When a network request for writing a Property name with a new value value and options is received, implementations MUST run the following update property steps, given name, value, options and mode set to "single":
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. Let interaction be this.[[td]].properties[name].
  4. If interaction is undefined, return a NotFoundError in the reply and stop.
  5. Let handler be this.[[writeHandlers]][name].
  6. If handler is undefined and if there is a default write handler provided by the implementation, let handler be that.
  7. If handler is undefined, send back a NotSupportedError with the reply and stop.
  8. Let promise be the result of invoking handler given name and options. If it fails, return the error in the reply and stop.
  9. If mode is "single", reply to the request reporting success, following the Protocol Bindings and stop.

9.17 Handling requests for writing multiple Properties

When a network request for writing multiple Properties given in an object propertyNames is received with options, run the following steps:
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. For each property with key name and value value defined in propertyNames, run the update property steps with name, value, options and mode set to "multiple". If that fails, reply to the request with that error and stop.
  4. Reply to the request by sending a single reply according to the Protocol Bindings.

9.18 The ActionHandler callback

A function that is called when an external request for invoking an Action is received and defines what to do with such requests. It is invoked given params and optionally with an options object. It returns a Promise that rejects with an error or resolves with the value returned by the Action as InteractionInput.

Note

Application scripts MAY return a ReadableStream object from an ActionHandler. Implementations will then use the stream for constructing the Action's response.

9.19 The setActionHandler() method

Takes as arguments name and action. Sets the handler function that defines what to do when a request is received to invoke the Action matched by name. Throws on error. Returns a reference to this object for supporting chaining.

The action callback function will implement an Action and SHOULD be called by implementations when a request for invoking the Action is received from the underlying platform.

There MUST be at most one handler for any given Action, so newly added handlers MUST replace the previous handlers.

When the method is invoked given name and action, run the following steps:
  1. If invoking this method is not allowed for the current scripting context for security reasons, throw a SecurityError and stop.
  2. Let interaction be this.[[td]].actions[name].
  3. If interaction is undefined, throw a NotFoundError and stop.
  4. Set this.[[actionHandlers]][name] to action.

9.20 Handling Action requests

When a network request for invoking the Action identified by name is received given inputs and optionally options, run the following steps:
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. Let interaction be this.[[td]].properties[name].
  4. If interaction is undefined, return a NotFoundError in the reply and stop.
  5. Let handler be this.[[actionHandlers]][name].
  6. If handler is undefined, return a NotSupportedError with the reply created by following the Protocol Bindings and stop.
  7. Let promise be the result of invoking handler given name, inputs and options.
  8. If promise rejects, send the error with the reply and stop.
  9. When promise resolves with data, use data to create and send the reply according to the Protocol Bindings.

9.21 The EventSubscriptionHandler callback

A function that is called when an external request for subscribing to an Event is received and defines what to do with such requests. It is invoked given an options object provided by the implementation and coming from subscribers. It returns a Promise that rejects with an error or resolves when the subscription is accepted.

9.22 The setEventSubscribeHandler() method

Takes as arguments name and handler. Sets the handler function that defines what to do when a subscription request is received for the specified Event matched by name. Throws on error. Returns a reference to this object for supporting chaining.

The handler callback function SHOULD implement what to do when an subscribe request is received, for instance necessary initializations. Note that the handler for emitting Events is set separately.

There MUST be at most one event subscribe handler for any given Event, so newly added handlers MUST replace the previous handlers.

When the method is invoked given name and handler, run the following steps:
  1. If invoking this method is not allowed for the current scripting context for security reasons, throw a SecurityError and stop.
  2. Let interaction be this.[[td]].events[name].
  3. If interaction is undefined, throw a NotFoundError and stop.
  4. Set this.[[subscribeHandlers]][name] to handler.
  5. Return this.

9.23 Handling Event subscribe requests

When an Event subscription request for name is received by the underlying platform with optional options, run the following steps:
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. Let interaction be this.[[td]].events[name].
  4. If interaction is undefined, send back a NotFoundError and stop.
  5. If this.[[subscribeHandlers]][name] is a Function, invoke it given options and stop.
  6. Otherwise implement the default subscriber mechanism:
    1. Let subscriber be a tuple formed of options (from which uriVariables and data may be used) and the subscriber information needed to create an Event notification response.
    2. Set this.[[eventListeners]][name] to subscriber.

9.24 The setEventUnsubscribeHandler() method

Takes as arguments name and handler. Sets the handler function that defines what to do when the specified Event matched by name is unsubscribed from. Throws on error. Returns a reference to this object for supporting chaining.

The handler callback function SHOULD implement what to do when an unsubscribe request is received.

There MUST be at most one handler for any given Event, so newly added handlers MUST replace the previous handlers.

When the method is invoked given name and handler, run the following steps:
  1. If invoking this method is not allowed for the current scripting context for security reasons, throw a SecurityError and stop.
  2. Let interaction be this.[[td]].events[name].
  3. If interaction is undefined, throw a NotFoundError and stop.
  4. Set this.[[unsubscribeHandlers]][name] to handler.
  5. Return this.

9.25 Handling Event unsubscribe requests

When an Event unsubscribe request for name is received by the underlying platform optionally with options, run the following steps:
  1. If this operation is not supported, send back a NotSupportedError according to the Protocol Bindings and stop.
  2. If this operation is not allowed, send back a NotAllowedError according to the Protocol Bindings and stop.
  3. Let interaction be this.[[td]].events[name].
  4. If interaction is undefined, send back a NotFoundError and stop.
  5. If this.[[unsubscribeHandlers]][name] exists and is a Function, invoke it given options and stop.
  6. Otherwise name [=map/exists] in this.[[eventListeners]], remove name.
  7. Return this.

9.26 Handling Events

When an Event with name name is emitted with data by the emitEvent() method, run the following steps:
  1. Let listeners be [[eventListeners]].name.
  2. For each subscriber in listeners, run the following sub-steps:
    1. Create an Event notification response according to the Protocol Bindings from data and subscriber, including its options.
    2. If data is undefined, assume that the notification response will contain an empty data payload as specified by Protocol Bindings.
    3. If the underlying protocol stack permits conveying event errors and if an error condition has been detected by the UA, create response as an error notification according to the Protocol Bindings, using data, subscriber and its options.
      Note

      The error reporting is protocol specific and it is encapsulated by implementations. On the client end, the error listener passed with the subscription will be invoked if the client UA detects the error.

    4. Send response to the subscriber identified by subscriber.

9.27 The emitEvent() method

Takes as arguments name denoting an Event name and optionally data. Triggers emitting the Event with the optional data. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. Let interaction be [[td]].events.name.
  4. If an Event with the name name is not found, reject promise with NotFoundError and stop.
  5. Make a request to the underlying platform to emit an Event with optional data. Call the handling events steps.

9.28 The expose() method

Start serving external requests for the Thing, so that WoT Interactions using Properties, Actions and Events will be possible. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. Run the expand a TD steps on the [[td]].
  4. Run the validate a TD on [[td]]. If that fails, reject promise with a TypeError and stop.
  5. For each key in [[td]].properties initialize this.[[propertyObservers]].key to an empty Array in order to store observe request data needed to notify the observers on value changes.
  6. For each key in this.[[td]].events initialize this.[[eventListeners]].key to an empty Array in order to store subscribe request data needed to notify the subscribers on event emission.
  7. Set up the WoT Interactions based on introspecting [[td]] as explained in [WOT-TD] and [WOT-PROTOCOL-BINDINGS]. Make a request to the underlying platform to initialize the Protocol Bindings and then start serving external requests for WoT Interactions (read, write and observe Properties, invoke Actions and manage Event subscriptions), based on the Protocol Bindings. Implementations MAY reject this step for any reason (e.g. if they want to enforce further checks and constraints on interaction forms).
  8. If there was an error during the request, reject promise with an Error object error with error.message set to the error code seen by the Protocol Bindings and stop.
  9. Otherwise resolve promise and stop.

9.29 The destroy() method

Stop serving external requests for the Thing and destroy the object. Note that eventual unregistering should be done before invoking this method. The method MUST run the following steps:
  1. Return a Promise promise and execute the next steps in parallel.
  2. If invoking this method is not allowed for the current scripting context for security reasons, reject promise with a SecurityError and stop.
  3. Make a request to the underlying platform to stop serving external requests for WoT Interactions, based on the Protocol Bindings.
  4. If there was an error during the request, reject promise with an Error object error with its message set to the error code seen by the Protocol Bindings and stop.
  5. Otherwise resolve promise and stop.

9.30 ExposedThing Examples

The next example illustrates how to create an ExposedThing based on a partial TD object constructed beforehand.

Example 6: Create ExposedThing with a simple Property
try {
  let temperaturePropertyDefinition = {
    type: "number",
    minimum: -50,
    maximum: 10000
  };
  let tdFragment = {
    properties: {
      temperature: temperaturePropertyDefinition
    },
    actions: {
      reset: {
        description: "Reset the temperature sensor",
        input: {
          temperature: temperatureValueDefinition
        },
        output: null,
        forms: []
      },
    },
    events: {
      onchange: temperatureValueDefinition
    }
  };
  let thing1 = await WOT.produce(tdFragment);
  // initialize Properties
  await thing1.writeProperty("temperature", 0);
  // add service handlers
  thing1.setPropertyReadHandler("temperature", () => {
     return readLocalTemperatureSensor();  // Promise
  });
  // start serving requests
  await thing1.expose();
} catch (err) {
   console.log("Error creating ExposedThing: " + err);
}

The next example illustrates how to add or modify a Property definition on an existing ExposedThing: take its td property, add or modify it, then create another ExposedThing with that.

Example 7: Add an object Property
try {
  // create a deep copy of thing1's TD
  let instance = JSON.parse(JSON.stringify(thing1.td));
  const statusValueDefinition = {
    type: "object",
    properties: {
      brightness: {
        type: "number",
        minimum: 0.0,
        maximum: 100.0,
        required: true
      },
      rgb: {
        type: "array",
        "minItems": 3,
        "maxItems": 3,
        items : {
            "type" : "number",
            "minimum": 0,
            "maximum": 255
        }
      }
  };
  instance["name"] = "mySensor";
  instance.properties["brightness"] = {
    type: "number",
    minimum: 0.0,
    maximum: 100.0,
    required: true,
  };
  instance.properties["status"] = statusValueDefinition;
  instance.actions["getStatus"] = {
    description: "Get status object",
    input: null,
    output: {
      status : statusValueDefinition;
    },
    forms: [...]
  };
  instance.events["onstatuschange"] = statusValueDefinition;
  instance.forms = [...];  // update
  var thing2 = new ExposedThing(instance);
  // TODO: add service handlers
  await thing2.expose();
  });
} catch (err) {
   console.log("Error creating ExposedThing: " + err);
}

The following will cover a set of examples for the generation of a Thing Description from an ExposedThingInit using expand an ExposedThingInit steps. As hypothesis the runtime supports HTTP and COAP protocol bindings and it is hosted at 192.168.0.1.

The next example shows how to exploit a ExposedThingInit to create a simple Thing Description with one Property with the default values.

Editor's note

TODO: add more examples where the ExposedThingInit contains suggested values that are replaced by the algorithm.

10. The ThingDiscoveryProcess interface

Discovery is a distributed application that requires provisioning and support from participating network nodes (clients, servers, directory services). This API models the client side of typical discovery schemes supported by various IoT deployments.

The ThingDiscoveryProcess object provides the properties and methods controlling the discovery process and returning the results.

WebIDL[SecureContext, Exposed=(Window,Worker)]
interface ThingDiscoveryProcess {
  constructor(optional ThingFilter filter = {});
  readonly attribute boolean done;
  readonly attribute Error? error;
  undefined stop();
  async iterable<ThingDescription>;
};

The ThingDiscoveryProcess object has the following internal slots.

Internal Slot Initial value Description (non-normative)
[[filter]] undefined The ThingFilter object used in discovery.
[[url]] undefined A URL representing the TD Directory in a discovery.

The done property is true if the discovery has been stopped or completed with no more results to report.

The error property represents the last error that occurred during the discovery process. Typically used for critical errors that stop discovery.

The ThingDiscoveryProcess object implements the async iterator concept.

10.1 Constructing ThingDiscoveryProcess

To create ThingDiscoveryProcess with a filter, run the following steps:
  1. If filter is not an object or null, throw a TypeError and stop.
  2. Let discovery be a new ThingDiscoveryProcess object.
  3. Set discovery.[[filter]] to filter.
  4. Set discovery.done to false.
  5. Set discovery.error to null.
  6. Return discovery.

10.2 The ThingFilter dictionary

Represents an object containing the constraints for discovering Things as key-value pairs.

WebIDLdictionary ThingFilter {
  object? fragment;
  
};

The fragment property represents a template object used for matching property by property against discovered Things.

Editor's note

The query property was temporarily removed from ThingFilter, until it is standardized in the WoT Discovery task force. It represented a query string accepted by the implementation, for instance a SPARQL or JSON query. Support was to be implemented locally in the WoT Runtime or remotely as a service in a TD Directory.

Editor's note

The url property was removed. It used to represent the target entity serving the discovery request, for instance the URL of a TD Directory, or the URL of a directly targeted Thing, but these are implemented by dedicated methods now.

10.3 The discovery process algorithm

Describes what to do during a discovery process that has already started. The algorithm, given discovery, runs the following steps:
  1. Whenever a new link to a Thing Description is discovered and provided by the underlying platform, run the following sub-steps:
    1. Retrieve td as a JSON object, using the Protocol Binding used by the underlying discovery process (as specified by link). In the case of an HTTP(S) Binding, this process could use the Fetch API for the td's retrieval.
    2. If that fails, set the discovery.error property to SyntaxError, discard td and continue the discovery process.
  2. Whenever a Thing Description td is discovered and provided by the underlying platform or by the previous step, run the following sub-steps:
    Note

    At this point implementations MAY control the flow of the discovery process (depending on memory constraints, for instance queue the results, or temporarily stop discovery if the queue is getting too large, or resume discovery when the queue is emptied sufficiently). These steps are run for each discovered/fetched td.

    1. Let fragment be discovery.[[filter]].fragment.
    2. If fragment is an object, then for each key defined in it:
      1. Check if that key exists in json and json[key is equal to fragment.key.
      2. If this is fails in any checks, discard td and continue the discovery process.
    3. Yield td using asyncIterator.
      Editor's note

      Improve this step using proper asyncIterator terminology.

  3. Whenever an error occurs during the discovery process, run the following sub-steps:
    Note

    The last error is retained. Implementations MAY choose to stop the discovery process if they consider it should be reported.

    1. Let error be a new Error object. Set error.name to "DiscoveryError".
    2. If there was an error code or message provided by the Protocol Bindings, set error.message to that value as string.
    3. Set discovery.error to error.
    4. If the error is irrecoverable and discovery has been stopped by the underlying platform, or if the implementation decided to stop the discovery process and report the error, set discovery.done to true and terminate these steps.

10.4 The stop() method

Stops or suppresses the discovery process. It might not be supported by all discovery methods and endpoints, however, any further discovery results or errors will be discarded and the discovery is marked as done. The method MUST run the following steps:
  1. If invoking this method is not allowed for the current scripting context for security reasons, throw a SecurityError and stop.
  2. Request the underlying platform to stop the discovery process. If this returns an error, or if it is not possible, for instance when discovery is based on open ended multicast requests, the implementation SHOULD discard subsequent discovered items.
  3. Set the done property to true.

10.5 Discovery Examples

The following example finds ThingDescription objects of Things that are exposed by local hardware, regardless how many instances of WoT Runtime it is running Using the asyncIterator provided by the Discovery object, we can iterate asynchronously over the results and perform operations with the obtained ThingDescription objects.

Example 9: Fetch the Thing Description of a Thing
let url = "https://mythings.com/thing1";
let td = await WOT.requestThingDescription(url);
console.log("Found Thing Description for " + td.title);

The next example finds ThingDescription objects of Things listed in a TD Directory service. We set a timeout for safety.

Example 10: Discover Things via directory
let discovery = await WOT.exploreDirectory("http://directory.wotservice.org");
setTimeout( () => {
    discovery.stop();
    console.log("Discovery stopped after timeout.");
  },
  3000);
for await (const td of discovery) {
  console.log("Found Thing Description for " + td.title);
  let thing = new ConsumedThing(td);
  console.log("Thing name: " + thing.getThingDescription().title);
};
if (discovery.error) {
  console.log("Discovery stopped because of an error: " + error.message);
}

The next example is for a generic discovery, by any means provisioned to the WOT runtime, including local Things, if any is available.

Example 11: Discover Things in a network
let discovery = await WOT.discover();
setTimeout( () => {
    discovery.stop();
    console.log("Stopped open-ended discovery");
  },
  10000);
for await (const td of discovery) {
  console.log("Found Thing Description for " + td.title);
};
if (discovery.error) {
  console.log("Discovery stopped because of an error: " + error.message);
}

11. Security and Privacy

A detailed discussion of security and privacy considerations for the Web of Things, including a threat model that can be adapted to various circumstances, is presented in the informative document [WOT-SECURITY]. This section discusses only security and privacy risks and possible mitigations directly relevant to the scripts and WoT Scripting API.

A suggested set of best practices to improve security for WoT devices and services has been documented in [WOT-SECURITY]. That document may be updated as security measures evolve. Following these practices does not guarantee security, but it might help avoid commonly known vulnerabilities.

The WoT security risks and possible mitigations are concerning the following groups:

11.1 Scripting Runtime Security and Privacy Risks

This section is normative and contains specific risks relevant for the WoT Scripting Runtime.

11.1.1 Corrupted Input Security and Privacy Risk

A typical way to compromise any process is to send it a corrupted input via one of the exposed interfaces. This can be done to a script instance using WoT interface it exposes.

Mitigation:
Implementors of this API SHOULD perform validation on all script inputs. In addition to input validation, fuzzing should be used to verify that the input processing is done correctly. There are many tools and techniques in existence to do such validation. More details can be found in [WOT-SECURITY].

11.1.2 Physical Device Direct Access Security and Privacy Risk

In case a script is compromised or misbehaving, the underlying physical device (and potentially surrounded environment) can be damaged if a script can use directly exposed native device interfaces. If such interfaces lack safety checks on their inputs, they might bring the underlying physical device (or environment) to an unsafe state (i.e. device overheats and explodes).

Mitigation:
The WoT Scripting Runtime SHOULD avoid directly exposing the native device interfaces to the script developers. Instead, a WoT Scripting Runtime should provide a hardware abstraction layer for accessing the native device interfaces. Such hardware abstraction layer should refuse to execute commands that might put the device (or environment) to an unsafe state. Additionally, in order to reduce the damage to a physical WoT device in cases a script gets compromised, it is important to minimize the number of interfaces that are exposed or accessible to a particular script based on its functionality.

11.1.3 Provisioning and Update Security Risk

If the WoT Scripting Runtime supports post-manufacturing provisioning or updates of scripts, WoT Scripting Runtime or any related data (including security credentials), it can be a major attack vector. An attacker can try to modify any above described element during the update or provisioning process or simply provision attacker's code and data directly.

Mitigation:
Post-manufacturing provisioning or update of scripts, WoT Scripting Runtime or any related data should be done in a secure fashion. A set of recommendations for secure update and post-manufacturing provisioning can be found in [WOT-SECURITY].

11.1.4 Security Credentials Storage Security and Privacy Risk

Typically the WoT Scripting Runtime needs to store the security credentials that are provisioned to a WoT device to operate in WoT network. If an attacker can compromise the confidentiality or integrity of these credentials, then it can obtain access to the WoT assets, impersonate WoT things or devices or create Denial-Of-Service (DoS) attacks.

Mitigation:
The WoT Scripting Runtime should securely store the provisioned security credentials, guaranteeing their integrity and confidentiality. In case there are more than one tenant on a single WoT-enabled device, a WoT Scripting Runtime should guarantee isolation of each tenant provisioned security credentials. Additionally, in order to minimize a risk that provisioned security credentials get compromised, the WoT Scripting Runtime should not expose any API for scripts to query the provisioned security credentials.

11.2 Script Security and Privacy Risks

This section is non-normative.

This section describes specific risks relevant for script developers.

11.2.1 Corrupted Script Input Security and Privacy Risk

A script instance may receive data formats defined by the TD, or data formats defined by the applications. While the WoT Scripting Runtime SHOULD perform validation on all input fields defined by the TD, scripts may be still exploited by input data.

Mitigation:
Script developers should perform validation on all application defined script inputs. In addition to input validation, fuzzing could be used to verify that the input processing is done correctly. There are many tools and techniques in existence to do such validation. More details can be found in [WOT-SECURITY].

11.2.2 Denial Of Service Security Risk

If a script performs a heavy functional processing on received requests before the request is authenticated, it presents a great risk for Denial-Of-Service (DOS) attacks.

Mitigation:
Scripts should avoid heavy functional processing without prior successful authentication of requestor. The set of recommended authentication mechanisms can be found in [WOT-SECURITY].

A. API design rationale

API rationale usually belongs to a separate document, but in the WoT case the complexity of the context justifies including basic rationale here.

A.1 Approaches to WoT application development

The WoT Interest Group and Working Group have explored different approaches to application development for WoT that have been all implemented and tested.

A.1.1 No Scripting API

It is possible to develop WoT applications that only use the WoT network interface, typically exposed by a WoT gateway that presents a RESTful API towards clients and implements IoT protocol plugins that communicate with supported IoT deployments. One such implementation is the Mozilla WebThings platform.

A.1.2 Simple Scripting API

WoT Things show good synergy with software objects, so a Thing can be represented as a software object, with Properties represented as object properties, Actions as methods, and Events as events. In addition, metadata is stored in special properties. Consuming and exposing is done with factory methods that produce a software object that directly represents a remote Thing and its interactions. One such implementation is the Arena Web Hub project.

In the next example, a Thing that represents interactions with a lock would look like the following: the status property and the open() method are directly exposed on the object.

Example 12: Open a lock with a simple API
let lock = await WoT.consume(‘https://td.my.com/lock-00123’);
console.log(lock.status);
lock.open('withThisKey');

A.1.3 This API, aligned with the Web of Things (WoT) Thing Description 1.1 specification

Since the direct mapping of Things to software objects have had some challenges, this specification takes another approach that exposes software objects to represent the Thing metadata as data property and the WoT interactions as methods. One implementation is node-wot in the Eclipse ThingWeb project, which is the current reference implementation of the API specified in this document.

The same example now would look like the following: the status property and the open() method are represented indirectly.

Example 13: Open a lock
let res = await fetch(‘https://td.my.com/lock-00123’);
let td = await res.json();
let lock = new ConsumedThing(td);
console.log(lock.readProperty(‘status’));
lock.invokeAction(‘open’, 'withThisKey');

In conclusion, the WoT WG decided to explore the third option that closely follows the Web of Things (WoT) Thing Description 1.1 specification. Based on this, a simple API can also be implemented. Since Scripting is an optional module in WoT, this leaves room for applications that only use the WoT network interface. Therefore all three approaches above are supported by the Web of Things (WoT) Thing Description 1.1 specification.

Moreover, the WoT network interface can be implemented in many languages and runtimes. Consider this API an example for what needs to be taken into consideration when designing a Scripting API for WoT.

A.2 Fetching and validating a TD

The fetch(url) method has been part of this API in earlier versions. However, now fetching a TD given a URL should be done with an external method, such as the Fetch API or a HTTP client library, which offer already standardized options on specifying fetch details. The reason is that while simple fetch operations (covering most use cases) could be done in this API, when various fetch options were needed, there was no point in duplicating existing work to re-expose those options in this API.

Since fetching a TD has been scoped out, and TD validation is defined externally in the Web of Things (WoT) Thing Description 1.1 specification, that is scoped out, too. This specification expects a TD as parsed JSON object that has been validated according to the Web of Things (WoT) Thing Description 1.1 specification.

A.3 Factory vs constructors

The factory methods for consuming and exposing Things are asynchronous and fully validate the input TD. In addition, one can also construct ConsumedThing and ExposedThing by providing a parsed and validated TD. Platform initialization is then done when needed during the WoT interactions.

A.4 Observers

Earlier drafts used the Observer construct, but since it has not become standard, a new design was needed that was light enough for embedded implementations. Therefore observing Property changes and handling WoT Events is done with callback registrations.

A.5 Using Events

This API ended up not using software events at all, for the following reasons:
  • Subscription to WoT Events may be different from handling software events (subscription might need parameters, might involve security tokens etc).
  • Most implementations are for Node.js and browser implementations will likely be libraries (because possible dependency management issues in native implementations), using Events has been challenging.
  • Observing Property changes and handling WoT Events is done with the solution above.

A.6 Polymorphic functions

The reason to use function names like readProperty(), readMultipleProperties() etc. instead of a generic polymorphic read() function is that the current names map exactly to the "op" vocabulary from the Form definition in the Web of Things (WoT) Thing Description 1.1 specification.

B. Changes

The following is a list of major changes to the document. Major versions of this specification are the following:

For a complete list of changes, see the github change log. You can also view the recently closed issues.

C. Full Web IDL

WebIDLtypedef object ThingDescription;

[SecureContext, Exposed=(Window,Worker)]
namespace WOT {
  // methods defined in UA conformance classes
};

partial namespace WOT {
  Promise<ConsumedThing> consume(ThingDescription td);
};

typedef object ExposedThingInit;

partial namespace WOT {
  Promise<ExposedThing> produce(ExposedThingInit init);
};

partial namespace WOT {
  Promise<ThingDiscoveryProcess> discover(optional ThingFilter filter = {});
};

partial namespace WOT {
  Promise<ThingDiscoveryProcess> exploreDirectory(USVString url,
      optional ThingFilter filter = {});
};

partial namespace WOT {
  Promise<ThingDescription> requestThingDescription(USVString url);
};

typedef any DataSchemaValue;
typedef (ReadableStream or DataSchemaValue) InteractionInput;

[SecureContext, Exposed=(Window,Worker)]
interface InteractionOutput {
  readonly attribute ReadableStream? data;
  readonly attribute boolean dataUsed;
  readonly attribute Form? form;
  readonly attribute DataSchema? schema;
  Promise<ArrayBuffer> arrayBuffer();
  Promise<DataSchemaValue> value();
};

[SecureContext, Exposed=(Window,Worker)]
interface ConsumedThing {
  constructor(ThingDescription td);
  Promise<InteractionOutput> readProperty(DOMString propertyName,
                              optional InteractionOptions options = {});
  Promise<PropertyReadMap> readAllProperties(
                              optional InteractionOptions options = {});
  Promise<PropertyReadMap> readMultipleProperties(
                              sequence<DOMString> propertyNames,
                              optional InteractionOptions options = {});
  Promise<undefined> writeProperty(DOMString propertyName,
                              InteractionInput value,
                              optional InteractionOptions options = {});
  Promise<undefined> writeMultipleProperties(
                              PropertyWriteMap valueMap,
                              optional InteractionOptions options = {});
  /*Promise<undefined> writeAllProperties(
                              PropertyWriteMap valueMap,
                              optional InteractionOptions options = {});*/
  Promise<InteractionOutput> invokeAction(DOMString actionName,
                              optional InteractionInput params = {},
                              optional InteractionOptions options = {});
  Promise<Subscription> observeProperty(DOMString name,
                              InteractionListener listener,
                              optional ErrorListener onerror,
                              optional InteractionOptions options = {});
  Promise<Subscription> subscribeEvent(DOMString name,
                              InteractionListener listener,
                              optional ErrorListener onerror,
                              optional InteractionOptions options = {});
  ThingDescription getThingDescription();
};

dictionary InteractionOptions {
  unsigned long formIndex;
  object uriVariables;
  any data;
};

[SecureContext, Exposed=(Window,Worker)]
interface Subscription {
  readonly attribute boolean active;
  Promise<undefined> stop(optional InteractionOptions options = {});
};

[SecureContext, Exposed=(Window,Worker)]
interface PropertyReadMap {
  readonly maplike<DOMString, InteractionOutput>;
};

[SecureContext, Exposed=(Window,Worker)]
interface PropertyWriteMap {
  readonly maplike<DOMString, InteractionInput>;
};

callback InteractionListener = undefined(InteractionOutput data);
callback ErrorListener = undefined(Error error);

[SecureContext, Exposed=(Window,Worker)]
interface ExposedThing {
  ExposedThing setPropertyReadHandler(DOMString name,
          PropertyReadHandler handler);
  ExposedThing setPropertyWriteHandler(DOMString name,
          PropertyWriteHandler handler);
  ExposedThing setPropertyObserveHandler(DOMString name,
          PropertyReadHandler handler);
  ExposedThing setPropertyUnobserveHandler(DOMString name,
          PropertyReadHandler handler);
  Promise<undefined> emitPropertyChange(DOMString name,
          optional InteractionInput data);

  ExposedThing setActionHandler(DOMString name, ActionHandler action);

  ExposedThing setEventSubscribeHandler(DOMString name,
          EventSubscriptionHandler handler);
  ExposedThing setEventUnsubscribeHandler(DOMString name,
          EventSubscriptionHandler handler);
  Promise<undefined> emitEvent(DOMString name,
          optional InteractionInput data);

  Promise<undefined> expose();
  Promise<undefined> destroy();

  ThingDescription getThingDescription();
};

callback PropertyReadHandler = Promise<InteractionInput>(
        optional InteractionOptions options = {});

callback PropertyWriteHandler = Promise<undefined>(
        InteractionOutput value,
        optional InteractionOptions options = {});

callback ActionHandler = Promise<InteractionInput>(
        InteractionOutput params,
        optional InteractionOptions options = {});

callback EventSubscriptionHandler = Promise<undefined>(
        optional InteractionOptions options = {});

[SecureContext, Exposed=(Window,Worker)]
interface ThingDiscoveryProcess {
  constructor(optional ThingFilter filter = {});
  readonly attribute boolean done;
  readonly attribute Error? error;
  undefined stop();
  async iterable<ThingDescription>;
};

dictionary ThingFilter {
  object? fragment;
  
};

D. Acknowledgements

Special thanks to former editor Johannes Hund (until August 2017, when at Siemens AG) and Kazuaki Nimura (until December 2018) for developing this specification. Also, the editors would like to thank Dave Raggett, Matthias Kovatsch, Michael Koster, Elena Reshetova, Michael McCool as well as the other WoT WG members for their comments, contributions and guidance.

E. References

E.1 Normative references

[ECMASCRIPT]
ECMAScript Language Specification. Ecma International. URL: https://tc39.es/ecma262/multipage/
[fetch]
Fetch Standard. Anne van Kesteren. WHATWG. Living Standard. URL: https://fetch.spec.whatwg.org/
[html]
HTML Standard. Anne van Kesteren; Domenic Denicola; Ian Hickson; Philip Jägenstedt; Simon Pieters. WHATWG. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[infra]
Infra Standard. Anne van Kesteren; Domenic Denicola. WHATWG. Living Standard. URL: https://infra.spec.whatwg.org/
[JSON-SCHEMA]
JSON Schema: A Media Type for Describing JSON Documents. Austin Wright; Henry Andrews; Ben Hutton; Greg Dennis. Internet Engineering Task Force (IETF). 8 December 2020. Internet-Draft. URL: https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema
[RFC2119]
Key words for use in RFCs to Indicate Requirement Levels. S. Bradner. IETF. March 1997. Best Current Practice. URL: https://www.rfc-editor.org/rfc/rfc2119
[RFC8174]
Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words. B. Leiba. IETF. May 2017. Best Current Practice. URL: https://www.rfc-editor.org/rfc/rfc8174
[streams]
Streams Standard. Adam Rice; Domenic Denicola; Mattias Buelens; 吉野剛史 (Takeshi Yoshino). WHATWG. Living Standard. URL: https://streams.spec.whatwg.org/
[TYPESCRIPT]
TypeScript Language Specification. Microsoft. 1 October 2012. URL: https://www.typescriptlang.org/docs/handbook/intro.html
[url]
URL Standard. Anne van Kesteren. WHATWG. Living Standard. URL: https://url.spec.whatwg.org/
[WEBIDL]
Web IDL Standard. Edgar Chen; Timothy Gu. WHATWG. Living Standard. URL: https://webidl.spec.whatwg.org/
[WOT-ARCHITECTURE]
Web of Things (WoT) Architecture 1.1. W3C. 19 January 2023. URL: https://www.w3.org/TR/2023/CR-wot-architecture11-20230119/
[WOT-PROTOCOL-BINDINGS]
Web of Things (WoT) Binding Templates. W3C. 30 January 2020. URL: https://www.w3.org/TR/2020/NOTE-wot-binding-templates-20200130/
[WOT-SECURITY]
Web of Things (WoT) Security and Privacy Guidelines. W3C. 6 November 2019. URL: https://www.w3.org/TR/2019/NOTE-wot-security-20191106/
[WOT-TD]
Web of Things (WoT) Thing Description 1.1. W3C. 19 January 2023. URL: https://www.w3.org/TR/2023/CR-wot-thing-description11-20230119/

E.2 Informative references

[WOT-DISCOVERY]
Web of Things (WoT) Discovery. W3C. 19 January 2023. URL: https://www.w3.org/TR/2023/CR-wot-discovery-20230119/
[WOT-USE-CASES]
Web of Things (WoT): Use Cases and Requirements. W3C. 7 March 2022. URL: https://www.w3.org/TR/2022/NOTE-wot-usecases-20220307/