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 [[[wot-architecture11]]] 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 [[[wot-thing-description11]]] 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).
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.
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.
then instantiating a software stack that implements the WoT Interface specified by the TD in order to serve requests for accessing the exposed Properties, Actions and Events,
This specification describes how to expose and consume Things by a script. Also, it defines a generic API for Thing discovery.
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.
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.
Use Case Scenarios
The business use cases listed in the [[wot-usecases]] document may be
implemented using this API, based on the scripting use case scenarios
described here.
After evaluating dynamic modifications to Thing Descriptions
through several versions of this API, the editors concluded that the
simplest way to represent these use cases is to take an existing
TD, modify it (i.e. add or remove definitions) and then create
a new Thing based on the modified TD.
Emit a WoT Event, i.e. notify all subscribed listeners.
Optionally specify a timeout to the discovery process after which it is stopped/suppressed.
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.
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]].
Terminology and conventions
The generic WoT terminology is defined in [[!wot-architecture11]]: 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-thing-description11]]
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.
Requesting a TD given a URL should be done with the
requestThingDescription()
method.
Alternatively, external methods, such as the Fetch API or an HTTP client library, can be used.
Note that the [[[wot-thing-description11]]] specification allows using a shortened Thing Description
by the means of defaults and requiring clients to expand them with default values specified in the
[[[wot-thing-description11]]] specification for the properties that are not explicitly defined in a given
TD.
To expand a TD given |td:ThingDescription|, run the following steps:
For each item in the TD default values table from [[!wot-thing-description11]], if the term is not defined in |td|, add the term definition with the default value specified in [[!wot-thing-description11]].
Validating a Thing Description
The [[!wot-thing-description11]] 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:ThingDescription|, run the following steps:
If JSON Schema
validation fails on |td|, [= exception/throw =] a {{"TypeError"}} and stop.
Additional steps may be added to fill the default values of mandatory fields.
The WOT namespace
Defines the WoT API object as a singleton and contains the API
methods, grouped by conformance classes.
[SecureContext, Exposed=(Window,Worker)]
namespace WOT {
// methods defined in UA conformance classes
};
Belongs to the WoT Consumer conformance class. Expects an |td:ThingDescription| 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:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
Let |thing:ConsumedThing| be a new {{ConsumedThing}} object constructed from |td|.
Set up the WoT Interactions based on introspecting td as explained in [[!wot-thing-description11]] and [[!wot-binding-templates]]. Make a request to the underlying platform to initialize the Protocol Bindings.
Implementations encapsulate the complexity of how to use the
Protocol Bindings for implementing WoT interactions.
In the future elements of that could be standardized.
Belongs to the WoT Producer conformance class. Expects a |init:ExposedThingInit| 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:ExposedThingInit| 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-architecture11]]. As such, it has the same
structure of a Thing Description but it may omit some information.
The method MUST run the following steps:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
Let |thing:ExposedThing| be a new {{ExposedThing}} object constructed with |init|.
[=Resolve=] |promise| with |thing|.
Expand an ExposedThingInit
To expand an ExposedThingInit given |init:ExposedThingInit| and obtain a valid |td:ThingDescription| as a result, run the following steps:
Let |td| be the result of running [=map/clone=] given |init|.
For each |scheme:SecurityScheme| 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|.
If |td|.["security"] does not [=map/exist=] in |td|.["securityDefinitions"], then remove security from |td|.
For each |affordance| in |td|.properties, |td|.actions and |td|.events, run the following sub-steps:
For each |form:Form| in |affordance|.forms:
If |form|.|contentType:string| is not recognized by the runtime as valid remove |contentType:string| from |form|.
If |form|.|href:URL| has an unknown schema, remove |href| from |form|.
If |form|.|href:URL| is absolute and its authority it is not recognized by the runtime as valid, remove |href| from |form|.
If |form|.|href:URL| is already in use by other ExposedThings, remove |href| from |form|.
Search for missing required properties in |td| accordingly to
TD JSON
Schema.
The editors find this step vague. It will be improved or removed in the next iteration.
For each |missing| property run these sub-steps:
If |missing| is title generate a runtime unique name and assign to title.
If |missing| is @context assign the latest supported Thing Description context URI.
If |missing| is instance assign the string 1.0.0.
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.
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.
The discussion about how to properly generate a value for security is still open.
See issue #299
If |missing| is href define |formStub| as the partial Form that does not have href. Generate a valid |url: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|.
Add |missing| to |td| with |value| as value
Run validate a TD on |td|. If that fails re-[= exception/throw =] the error and stop
Return |td|
Validating an ExposedThingInit
To validate an ExposedThingInit given |init:ExposedThingInit|, run the following steps:
Parse TD JSON
Schema
and load it in object called |exposedThingInitSchema:object|
let |optional:Array| be a list containing the following strings: title, @context,
instance, forms, security, and href.
For each property and sub-property |key| in |exposedThingInitSchema| equals to required execute the following steps:
if |key| |value| is an Array then remove all its elements equal to the elements in |optional|
if |key| |value| is a string then if |value| is equal to one of the elements in |optional| remove |key| from |exposedThingInitSchema|
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.
Belongs to the WoT Discovery conformance class. Starts the discovery process that will provide {{ThingDescription}} objects for Thing Descriptions that match an optional |filter:ThingFilter| argument of type {{ThingFilter}}. The method MUST run the following steps:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
If discovery is not supported by the implementation, [=reject=] |promise|
with {{NotSupportedError}} and stop.
Let |discovery:ThingDiscoveryProcess| be a new {{ThingDiscoveryProcess}} object.
Set |discovery|.{{ThingDiscoveryProcess/[[filter]]}} to |filter:ThingFilter|.
Set |discovery|.{{ThingDiscoveryProcess/[[url]]}} to `undefined`.
If filters in general are not supported by the implementation and
|filter| is not `undefined` or `null`, [=reject=] |promise|
with {{NotSupportedError}} and stop.
If discovery cannot be started by the underlying platform,
[=reject=] |promise| with {{OperationError}} and stop.
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.
[=Resolve=] |promise| with |discovery|.
Note that the details of the discovery process depend on the
underlying implementation which needs to be preconfigured in order to
use, for example, the appropriate Introduction methods as defined in
the [[[wot-discovery]]] specification.
Since the discover() method outputs
Thing Descriptions and not URLs, it is expected to cover both the
Introduction and the Exploration phase.
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:ThingFilter| argument of type {{ThingFilter}}. The method MUST run the following steps:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
If directory discovery is not supported by the implementation, [=reject=] |promise|
with {{NotSupportedError}} and stop.
Let |discovery:ThingDiscoveryProcess| be a new {{ThingDiscoveryProcess}} object.
Set |discovery|.{{ThingDiscoveryProcess/[[url]]}} to |url:USVString|.
Set |discovery|.{{ThingDiscoveryProcess/[[filter]]}} to |filter:ThingFilter|.
Request the underlying platform to start the directory discovery process.
This is a placeholder for more details in the discovery algorithm.
Implementations should follow the procedures described in the
[[wot-discovery]] and [wot-binding-templates] specifications.
Some normative steps are indicated below.
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.
If filters in general are not supported by the implementation and
|filter| is not `undefined` or `null`, [=reject=] |promise|
with {{NotSupportedError}} and stop.
Belongs to the WoT Discovery conformance class.
Requests a Thing Description from the given URL.
The method MUST run the following steps:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting
context for security reasons, [=reject=] |promise| with a {{SecurityError}}
and stop.
If requesting a Thing Description is not supported by the
implementation, [=reject=] |promise| with {{NotSupportedError}} and
stop.
Let |td:ThingDescription| 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.
[=Resolve=] |promise| with |td|.
Handling interaction data
As specified in the [[[wot-thing-description11]]] 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.
The InteractionInput type
typedef 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.
{{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.
The algorithms in this document specify how exactly input data is used in
WoT Interactions.
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}}.
The schema attribute represents the DataSchema
(defined in [[wot-thing-description11]]) 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).
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:
Return a {{Promise}} |promise:Promise| and execute the next steps
[=in parallel=].
If |this|.[[\value]] is not `undefined`, [=resolve=] |promise| with that value and stop.
If |this|.|data| is not a {{ReadableStream}} or if
|dataUsed| is `true`, or if |form| is not an {{object}} or if |schema|
is `null` or `undefined`, then
[=reject=] |promise| with {{NotReadableError}} and stop.
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.
Let |reader| be the result of
getting a reader from |data|. If that threw an exception, [=reject=]
|promise| with that exception and stop.
Let |bytes| be the result of
reading all bytes from |data| with |reader|.
Set |dataUsed| to `true`.
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.
Let |json| be the result of running parse JSON from bytes on
|bytes|. If that throws, [=reject=] |promise| with that exception and
stop.
Set [[\value]] to the result of running check data schema
on |json| and |schema|. If that throws, [=reject=] |promise| with that
exception and stop.
Return a {{Promise}} |promise:Promise| and execute the next steps
[=in parallel=].
If |data| is not {{ReadableStream}} or if |dataUsed| is `true`,
[=reject=] |promise| with {{NotReadableError}} and stop.
Let |reader| be the result of
getting a reader from |data|. If that threw an exception, [=reject=]
|promise| with that exception and stop.
Let |bytes| be the result of
reading all bytes from |data| with |reader|.
Set |dataUsed| to `true`.
Let |arrayBuffer| be a new {{ArrayBuffer}} whose contents are |bytes|.
If that throws, [=reject=] |promise| with that exception and stop.
[=Resolve=] |promise| with |arrayBuffer|.
The check data schema algorithm
To run the check data schema steps on |payload| and |schema:object|,
If |schema| is `null` or `undefined`, return `undefined`.
If |schema|.|const| is not |undefined| and |schema|.|const| does not equal |payload|, throw {{TypeError}} and stop.
If |schema|.|enum| is not |undefined| and none of the elements in
|schema|.|enum| equal |payload|, throw {{TypeError}} and stop.
Let |oneOf| be |schema|.|oneOf|.
If |oneOf| is not |undefined|,
For each |subSchema| in |oneOf|, run the check data schema
steps on |payload| and |subSchema|.
If none or more than one of these runs do not throw, throw
{{TypeError}} and stop.
Let |type| be |schema|.|type|.
If |type| is `"null"` and if |payload| is not `null`,
throw {{TypeError}} and stop, otherwise return `null`.
If |type| is `"boolean"` and |payload| is a falsy value or its byte
length is 0, return `false`, otherwise return `true`.
If |type| is `"integer"` or `"number"`,
If |payload| is not a number, throw {{TypeError}} and stop.
If |form|.|minimum| is defined and |payload| is smaller, or if |form|.|maximum| is defined and |payload| is bigger, throw a {{RangeError}} and stop.
If |type| is `"string"`, return |payload|.
If |type| is `"array"`, run these sub-steps:
If |payload| is not an array, throw {{TypeError}} and stop.
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.
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.
If |type| is `"object"`, run these sub-steps:
If |payload| or |schema|.|properties| is not an {{object}},
throw {{TypeError}} and stop.
For each |key| in |payload|:
Let |prop| be |payload|[|key|].
Let |propSchema| be |interaction|.|properties|[|key|].
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.
Let |required| be |schema|.|required| if that is an array
or an empty array otherwise.
For each |key| in |required|, if |key| is not present in |payload|,
throw {{SyntaxError}} and stop.
Return |payload|.
The create interaction request algorithm
For a given ConsumedThing object |thing:ConsumedThing|, in order to
create interaction request given a |source: InteractionInput|, |form:Form| and
|schema:object|, run these steps:
Let |idata| be a new an {{InteractionOutput}} object.
Set |idata|.|form| to |form|, set |idata|.|schema| to |schema|, set |idata.|data| to `null` and set |idata|.{{InteractionOutput/[[value]]}} to `undefined`.
If |source| is a {{ReadableStream}} object, let |idata|.|data| be
|source|, return |idata| and stop.
If |schema| and its |type| are defined and not `null`, run these sub-steps:
If |type| is `"null"` and |source| is not `"null"`,
throw {{TypeError}} and stop.
If |type| is `"boolean"` and |source| is a falsy value, set
|idata|.{{InteractionOutput/[[value]]}} to `false`, otherwise set it to `true`.
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.
If |type| is `"string"` and |source| is not a string, let
|idata|.{{InteractionOutput/[[value]]}} be the result of running
serialize JSON to bytes given |source|.
If that is failure, throw {{SyntaxError}} and stop.
If |type| is `"array"`, run these sub-steps:
If |source| is not an array, throw a {{TypeError}} and stop.
Let |length| be the length of |source|.
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.
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.
Set |data|.{{InteractionOutput/[[value]]}} to |source|.
If |type| is `"object"`, run these sub-steps:
If |source| is not an object, throw {{TypeError}} and stop.
If |schema|.|properties| is not an object,
throw {{TypeError}} and stop.
For each |key| in |source|,
Let |value| be |source|[|key|].
Let |propschema| be |properties|.|interactions|[|key|].
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.
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.
Set |data|.{{InteractionOutput/[[value]]}} to |source|.
Set |idata|.|data| to a new {{ReadableStream}} created from
|idata|.{{InteractionOutput/[[value]]}} internal slot as its
underlying source.
Return |idata|.
The parse interaction response algorithm
For a given ConsumedThing object |thing:ConsumedThing|, in order to
parse interaction response given |response|, |form:Form| and
|schema:object|, run these steps:
Let |result| be a new {{InteractionOutput}} object.
Let |result|.|schema| be |schema|.
Let |result|.|form| be |form|.
Let |result|.|data| be a new {{ReadableStream}} with the payload data
of |response| as its
underlying source.
Let |result|.|dataUsed| be `false`.
Return |result|.
Using {{InteractionInput}} and {{InteractionOutput}}
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.
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}}.
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.
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.
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.
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.
The ConsumedThing interface
Represents a client API to operate a Thing. Belongs to the
WoT Consumer conformance class.
To create {{ConsumedThing}} with the {{ThingDescription}}
|td:ThingDescription|, run the following steps:
Run the validate a TD steps on |td|. If that fails,
[= exception/throw =] {{SyntaxError}} and stop.
Run the expand a TD steps on |td|. If that fails, re-[= exception/throw =] the error and stop.
Let |thing:ConsumedThing| be a new {{ConsumedThing}} object.
Set the internal slot {{ConsumedThing/[[td]]}} of |thing| to |td|.
Return |thing|.
The getThingDescription() method
Returns the {{ConsumedThing/[[td]]}} of the {{ConsumedThing}} object
that represents the Thing Description of the {{ConsumedThing}}.
Applications may consult the Thing metadata stored in {{ConsumedThing/[[td]]}} in
order to introspect its capabilities before interacting with it.
The readProperty() method
Reads a Property value. Takes as arguments |propertyName:string|
and optionally |options:InteractionOptions|.
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:
Return a {{Promise}} |promise:Promise| and execute the next steps
[=in parallel=].
If invoking this method is not allowed for the current scripting
context for security reasons, [=reject=] |promise| with a
{{SecurityError}} and stop.
Let |interaction| be {{ConsumedThing/[[td]]}}.|properties|.|propertyName|.
If |interaction| is `undefined`, [=reject=] |promise| with a {{NotFoundError}}
and stop.
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.
If |form| is failure, [=reject=] |promise| with a {{SyntaxError}} and
stop.
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|.
If the request fails, [=reject=] |promise| with the error received from the Protocol Bindings and stop.
Let |response| be the response received to the request.
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.
[=Resolve=] |promise| with |data|.
The readMultipleProperties() method
Reads multiple Property values with one request.
Takes as arguments |propertyNames: string sequence| and optionally
|options:InteractionOptions|.
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:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
If |option|.|formIndex| is defined, let |form| be the
Form associated with |formIndex| in the {{ConsumedThing/[[td]]}}.|forms| array, otherwise let |form| be the Form in {{ConsumedThing/[[td]]}}.|forms| array whose |op| is `readmultipleproperties`, as selected by the implementation.
If |form| is failure, [=reject=] |promise| with a {{SyntaxError}} and stop.
Let |result:object| be an object and for each string |name:string| in |propertyNames| add a property with key |name| and the value `null`.
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|.
If this cannot be done with a single request with the Protocol Bindings, [=reject=] |promise| with a {{NotSupportedError}} and stop.
Process the response and for each |key| in |result|, run the following
sub-steps:
Let |value| be |result|[|key|].
Let |schema| be |this|.{{ConsumedThing/[[td]]}}.|properties|[|key|].
If the above step throws at any point, [=reject=] |promise| with that
exception and stop.
[=Resolve=] |promise| with |result|.
The readAllProperties() method
Reads all properties of the Thing with one request. Takes |options:InteractionOptions| 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:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
Let |forms| be |subscription|.{{Subscription/[[interaction]]}}.|forms|.
If |forms| is `undefined`, [=reject=] |promise| with a
{{SyntaxError}} and stop.
If |option|.|formIndex| is not `undefined` and is less than |forms|.|length|, set |subscription|.{{Subscription/[[form]]}} to |forms|.[|formIndex|].
Otherwise, set |subscription|.{{Subscription/[[form]]}} to a
Form in |forms| whose |op| is `"readallproperties"`, as selected by the implementation.
If |subscription|.{{Subscription/[[form]]}} is failure, [=reject=] |promise| with a {{SyntaxError}} and stop.
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|.
If this cannot be done with a single request with the Protocol Bindings of the Thing, then [=reject=] |promise| with a {{NotSupportedError}} and stop.
If the request fails, [=reject=] |promise| with the error received from the Protocol Bindings and stop.
Process the reply and let |result:object| be an object with the keys and values obtained in the reply.
Process the response and for each |key| in |result|, run the following
sub-steps:
Let |value| be |result|[|key|].
Let |schema| be |this|.{{ConsumedThing/[[td]]}}.|properties|[|key|].
Writes a single Property. Takes as arguments |propertyName:string|,
|value:InteractionInput| and optionally |options:InteractionOptions|.
It returns a {{Promise}} that resolves on success and rejects on failure.
The method MUST run the following steps:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
Let |interaction| be |this|.{{ConsumedThing/[[td]]}}.|properties|[|propertyName|].
If |interaction| is `undefined`, [=reject=] |promise| with a {{NotFoundError}}
and stop.
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.
If |form| is failure, [=reject=] |promise| with a {{SyntaxError}} and stop.
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.
Make a request to the underlying platform (via the Protocol Bindings)
to write the Property given by |propertyName| using
|data:InteractionOutput| and the optional URI templates given in
|options|' |uriVariables|.
If the request fails, [=reject=] |promise| with the error received from the Protocol Bindings and stop.
Otherwise [=resolve=] |promise|.
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.
The writeMultipleProperties() method
Writes a multiple Property values with one request.
Takes as arguments |properties:object| - as an object with keys being
Property names and values as Property values - and optionally
|options:InteractionOptions|.
It returns a {{Promise}} that resolves on success and rejects on failure. The method MUST run the following steps:
Return a {{Promise}} |promise:Promise| and execute the next steps
[=in parallel=].
If invoking this method is not allowed for the current scripting
context for security reasons, [=reject=] |promise| with a
{{SecurityError}} and stop.
If |option|.|formIndex| is defined, let |form| be the
Form associated with |formIndex| in the {{ConsumedThing/[[td]]}}.|forms| array,
otherwise let |form| be a Form in {{ConsumedThing/[[td]]}}.|forms|
array whose |op| is `writemultipleproperties`, as selected by
the implementation.
If |form| is failure, [=reject=] |promise| with a {{SyntaxError}} and
stop.
Let |propertyNames| be an array of |string| with as elements the keys of the |properties| object.
For each |name:string| in |propertyNames|, let |property| be |this|.{{ConsumedThing/[[td]]}}.|properties|[|name|].
If |property| is `null` or `undefined` or is not `writeable` [=reject=] |promise| with {{NotSupportedError}} and stop.
Let |result:object| be an object and for each string |name:string| in |propertyNames| add a property with key |name| and let its value be
`null`.
Let |schemas:object| be an object and for each |name:string|
in |propertyNames| add a property with key |name| and let its value
be |this|.{{ConsumedThing/[[td]]}}.|properties|[|name|].
For each key |key:string| 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.
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|.
If this cannot be done with a single request with the
Protocol Bindings of the Thing, then [=reject=] |promise|
with a {{NotSupportedError}} and stop.
If the request fails, return the error received from the
Protocol Bindings and stop.
Otherwise [=resolve=] |promise|.
The observeProperty() method
Makes a request for Property value change notifications.
Takes as arguments |propertyName:string|, |listener:InteractionListener| and
optionally |onerror:ErrorListener| and |options:InteractionOptions|.
It returns a {{Promise}} that resolves on success and rejects on failure.
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:
Let |thing| be the reference of this {{ConsumedThing}} object.
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
If |listener| is not a {{Function}}, [=reject=] |promise|
with a {{TypeError}} and stop.
If |onerror| is not `null` and is not a {{Function}}, [=reject=] |promise|
with a {{TypeError}} and stop.
If |thing|.{{ConsumedThing/[[activeObservations]]}}[|propertyName|] [=map/exists=], [=reject=] |promise| with a {{NotAllowedError}} and stop.
Let |subscription| be a new {{Subscription}} object with its internal slots
set as follows:
Let |subscription|.{{Subscription/[[type]]}} be `"property"`.
Let |subscription|.{{Subscription/[[name]]}} be |propertyName|.
Let |subscription|.{{Subscription/[[interaction]]}} be {{ConsumedThing/[[td]]}}.|properties|[|propertyName|].
Let |subscription|.{{Subscription/[[thing]]}} be |thing|.
Let |forms| be |subscription|.{{Subscription/[[interaction]]}}.|forms|.
If |forms| is `undefined`, [=reject=] |promise| with a
{{SyntaxError}} and stop.
If |option|.|formIndex| is not `undefined` and is less than |forms|.|length|, set |subscription|.{{Subscription/[[form]]}} to |forms|.[|formIndex|].
Otherwise, set |subscription|.{{Subscription/[[form]]}} to a
Form in |forms| whose |op| is `"observeproperty"`, as selected by the implementation.
If |subscription|.{{Subscription/[[form]]}} is failure, [=reject=] |promise| with a
{{SyntaxError}} and stop.
If |subscription|.{{Subscription/[[interaction]]}} is `undefined`, [=reject=] |promise| with a {{NotFoundError}} and stop.
Make a request to the underlying platform to observe the
Property identified by |propertyName| with |form| and
optional URI templates given in |options|' |uriVariables|.
If the request fails, [=reject=] |promise| with the error received from
the Protocol Bindings and stop.
[=map/Set=] |thing|.{{ConsumedThing/[[activeObservations]]}}[|propertyName] to |subscription| and [=resolve=] |promise|.
Whenever the underlying platform detects a notification for this
|subscription| [=map/keyed=] on |propertyName| with a new Property value |value|,
run the following sub-steps:
Let |reply| be the result of running parse interaction response
with |value|, |subscription|.{{Subscription/[[form]]}} and |subscription|.{{Subscription/[[interaction]]}}.
If that throws, [=reject=] |promise| with that exception and stop.
Invoke |listener| given |reply|.
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|.
The invokeAction() method
Makes a request for invoking an Action and return the result.
Takes as arguments |actionName:string|, optionally
|params:InteractionInput| and optionally |options:InteractionOptions|.
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:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
Let |interaction| be |this|.{{ConsumedThing/[[td]]}}.|actions|[|actionName|].
If |interaction| is not an {{object}}, [=reject=] |promise| with a {{NotFoundError}}
and stop.
Let |forms| be |subscription|.{{Subscription/[[interaction]]}}.|forms|.
If |forms| is `undefined`, [=reject=] |promise| with a
{{SyntaxError}} and stop.
If |option|.|formIndex| is not `undefined` and is less than |forms|.|length|, set |subscription|.{{Subscription/[[form]]}} to |forms|.[|formIndex|].
Otherwise, set |subscription|.{{Subscription/[[form]]}} to a
Form in |forms| whose |op| is `"invokeaction"`, as selected by the implementation.
If |subscription|.{{Subscription/[[form]]}} is failure, [=reject=] |promise| with a
{{SyntaxError}} and stop.
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.
Make a request to the underlying platform (via the Protocol Bindings) to invoke the Action identified by |actionName| given |args| and |options|.|uriVariables|.
If the request fails locally or returns an error over the network, [=reject=] |promise| with the error received from the Protocol Bindings and stop.
Let |value| be the reply returned in the reply.
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.
[=Resolve=] |promise| with |result|.
The subscribeEvent() method
Makes a request for subscribing to Event notifications.
Takes as arguments |eventName:string|, |listener:WoTListener| and
optionally |onerror:ErrorListener| and |options:InteractionOptions|.
It returns a {{Promise}} to signal success or failure.
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:
Let |thing| be the reference of this {{ConsumedThing}} object.
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
If |listener| is not a {{Function}}, [=reject=] |promise|
with a {{TypeError}} and stop.
If |onerror| is not `null` and is not a {{Function}}, [=reject=] |promise|
with a {{TypeError}} and stop.
If |thing|.{{ConsumedThing/[[activeSubscriptions]]}}[|eventName|] [=map/exists=], [=reject=] |promise| with a {{NotAllowedError}} and stop.
Let |subscription| be a new {{Subscription}} object with its internal slots
set as follows:
Let |subscription|.{{Subscription/[[type]]}} be `"event"`.
Let |subscription|.{{Subscription/[[name]]}} be |eventName|.
Let |subscription|.{{Subscription/[[interaction]]}} be |thing|. {{ConsumedThing/[[td]]}}.|events|[|eventName|].
If |subscription|.{{Subscription/[[interaction]]}} is `undefined`, [=reject=] |promise| with a {{NotFoundError}} and stop.
Let |subscription|.{{Subscription/[[thing]]}} be |thing|.
If |options|.|formIndex| [=map/exists=], then let |subscription|.{{Subscription/[[form]]}} be |thing|.{{Subscription/[[interaction]]}}.|forms|[|formIndex|].
Otherwise, let |subscription|.{{Subscription/[[form]]}} be an [=implementation-defined=]
Form from the |subscription|.{{Subscription/[[interaction]]}}.|forms| array whose |op| is `"subscribeevent"`.
If |subscription|.{{Subscription/[[form]]}} does not [=map/exist=],
[=reject=] |promise| with a {{SyntaxError}} and stop.
Make a request to the underlying platform via the Protocol Bindings
to subscribe to the Event identified by |eventName:string| with
{{Subscription/[[form]]}}, optional URI templates given in |options|.|uriVariables|
and optional subscription data given in |options|.|data|.
If the request fails, [=reject=] |promise| with the error received from
the Protocol Bindings and stop.
[=map/Set=] |eventName| to |thing|.{{ConsumedThing/[[activeSubscriptions]]}}[|eventName|] to |subscription|.
[=Resolve=] |promise|.
Whenever the underlying platform detects a notification for the
Event |subscription| [=map/keyed=] on |eventName|, run the following sub-steps:
Invoke |listener| given the result of running
parse interaction response on the data provided with the
Event, |subscription|.{{Subscription/[[form]]}} and |subscription|.{{Subscription/[[interaction]]}}.
Whenever the underlying platform detects an error for
Event |subscription| [=map/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|.
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-thing-description11]].
The support for URI variables comes from the need, exposed by the [[[wot-thing-description11]]] 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.
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.
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.
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.
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.
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.
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`.
Stops delivering notifications for the subscription. It takes an
optional parameter |options:InteractionOptions| and returns a {{Promise}}.
When invoked, the method MUST execute the following steps:
Return a {{Promise}} |promise:Promise| and execute the next steps
[=in parallel=].
If invoking this method is not allowed for the current scripting
context for security reasons, [=reject=] |promise| with a
{{SecurityError}} and stop.
If |options|' |formIndex| is defined, let |unsubscribeForm| be the
Form associated with |formIndex| in {{Subscription/[[interaction]]}}'s
|forms| array.
Otherwise let |unsubscribeForm| be the result of running the
find a matching unsubscribe form algorithm given {{Subscription/[[form]]}}.
If |unsubscribeForm| is failure, [=reject=] |promise| with a {{SyntaxError}} and
stop.
If {{Subscription/[[type]]}} is `"property"`, make a request to the underlying
platform via the Protocol Bindings to stop observing the
Property identified by {{Subscription/[[name]]}} with |unsubscribeForm| and optional
URI templates given in |options|' |uriVariables|.
Otherwise, if {{Subscription/[[type]]}} is `"event"`, make a request to the underlying
platform via the Protocol Bindings to unsubscribe from the
Event identified by {{Subscription/[[name]]}} with |unsubscribeForm|, with optional URI
templates given in |options|' |uriVariables| and optional
unsubscribe data given in |options|.|data|.
If the request fails, [=reject=] |promise| with the error received from
the Protocol Bindings and stop.
if {{Subscription/[[type]]}} is `"event"`, remove {{Subscription/[[name]]}} from {{Subscription/[[thing]]}}.{{ConsumedThing/[[activeSubscriptions]]}} .
if {{Subscription/[[type]]}} is `"property"`, remove {{Subscription/[[name]]}} from {{Subscription/[[thing]]}}.{{ConsumedThing/[[activeObservations]]}} .
[=Resolve=] |promise|.
If the underlying platform receives further notifications for this
subscription, implementations SHOULD silently suppress them.
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:
Let |results| be an empty array.
For each |form| in {{Subscription/[[interaction]]}}.|forms|,
Add an internal slot [[\matchLevel]] on |form| and set
its value to `0`.
If |form|.|op| is `"unobserveproperty"` if {{Subscription/[[type]]}} is
`"property"` or if |form|.|op| is `"unsubscribeevent"`
if {{Subscription/[[type]]}} is`"event"`,
Set the internal slot [[\matchLevel]] on |form| to 1
and add |form| to |results|.
If |form|.|href| and [[\subscribeForm]].|href| are
same origin-domain, increment |form|.[[\matchLevel]].
If |form|.|contentType| is equal to [[\subscribeForm]]'s
|contentType| and |form|.[[\matchLevel]] is greater than 2,
increment |form|.[[\matchLevel]].
If |results| is empty, return `null` and terminate these steps.
Return the first |form| in |results| that has the highest
[[\matchLevel]] value.
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.
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}}.
Finally, the next two examples shows the usage of a {{ReadableStream}} from an {{InteractionOutput}}.
/*{
"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 stopped");
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.
/*
* "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
The ExposedThing interface
The {{ExposedThing}} interface is the server API to operate the Thing that allows defining request handlers, Property, Action, and Event interactions.
A {{Map}} with property names as keys and {{PropertyReadHandler}}s as values
[[\writeHandlers]]
`{}`
A {{Map}} with property names as keys and {{PropertyWriteHandler}}s as values
[[\observeHandlers]]
`{}`
A {{Map}} with property names as keys and {{PropertyReadHandler}}s as values
[[\unobserveHandlers]]
`{}`
A {{Map}} with property names as keys and {{Function}}s as values
[[\actionHandlers]]
`{}`
A {{Map}} with action names as keys and {{ActionHandler}}s as values
[[\subscribeHandlers]]
`{}`
A {{Map}} with event names as keys and {{EventSubscriptionHandler}}s as values
[[\unsubscribeHandlers]]
`{}`
A {{Map}} with event names as keys and {{EventSubscriptionHandler}}s 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
Creating {{ExposedThing}}
The {{ExposedThing}} interface
is created from a full or partial {{ThingDescription}} object.
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 creating an
{{ExposedThing}} object. This is the current way of adding and
removing Property, Action and Event definitions, as illustrated in the examples.
Before invoking expose(), the {{ExposedThing}} object does not serve any requests. This allows first creating {{ExposedThing}} and then initialize its Properties and service handlers before starting serving requests.
To create an {{ExposedThing}} with the {{ExposedThingInit}}
|init:ExposedThingInit|, run the following steps:
If invoking this method is not allowed for the current scripting context for security reasons, [= exception/throw =] a {{SecurityError}} and stop.
Run the expand an ExposedThingInit steps on |init|. if that fails re-[= exception/throw =] the error and stop. Otherwise store the obtained |td:ThingDescription|
Run the expand a TD steps on |td|. If that fails, re-[= exception/throw =] the error and stop.
Let |thing:ExposedThing| be a new {{ExposedThing}} object.
Set the {{ExposedThing/[[td]]}} of |thing| to |td|.
Return |thing|.
The getThingDescription() method
Returns the {{ExposedThing/[[td]]}} of the {{ExposedThing}} object
that represents the Thing Description of the Thing.
Applications may consult the Thing metadata stored in {{ExposedThing/[[td]]}} in
order to introspect its capabilities before interacting with it.
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.
The setPropertyReadHandler() method
Takes as arguments |name:string| and |handler:PropertyReadHandler|.
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.
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 {{ExposedThing/[[td]]}} internal slot.
When the method is invoked given |name:string| and
|handler:PropertyReadHandler|, implementations MUST run the
following steps:
If invoking this method is not allowed for the current scripting context for security reasons, [= exception/throw =] a {{SecurityError}} and stop.
If {{ExposedThing/[[td]]}}.|properties|[|name|] does not [=map/exist=],
[= exception/throw =] {{NotFoundError}} and stop.
Set |this|.{{ExposedThing/[[readHandlers]]}}[|name|] to |handler|.
Handling requests for reading a Property
When a network request for reading Property |name:string|
is received by the implementation with |options:InteractionOptions|,
run the following steps:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
Let |value| be the result of running the read server property
steps with |name:string| and |options:InteractionOptions|:
Let |interaction| be {{ExposedThing/[[td]]}}.|properties|.|name|.
If a Property with |name| does not exist, throw {{NotFoundError}} and stop.
Let |handler:function| be `null`.
If there is a user provided {{PropertyReadHandler}} in {{ExposedThing/[[readHandlers]]}} internal slot
for |interaction|, let |handler| be that.
Otherwise, if there is a default read handler provided by the implementation, let |handler| be that.
If |handler| is `null`, throw {{NotSupportedError}}
and stop.
Let |value| be the result of invoking |handler| given |options|.
If that fails, throw the error and stop.
Return |value|.
The |value| returned here SHOULD either conform to DataSchema
or it SHOULD be an {{ReadableStream}} object created by the
|handler|.
If the previous step has thrown an error, send the error back with
the reply created by following the Protocol Bindings and stop.
Serialize and add the returned |value| to the reply created by
following the Protocol Bindings.
Handling requests for reading multiple Properties
When a network request for reading multiple Properties given in
an object |propertyNames| is received with |options:InteractionOptions|,
run the following read multiple properties steps on |propertyNames| and |options|:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
For each property with key |name| defined in |propertyNames|,
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.
Set |propertyNames|.|name| to |value|.
Reply to the request by sending a single reply created from |propertyNames| according to the Protocol Bindings.
Handling requests for reading all Properties
When a network request for reading all Properties is received
with |options:InteractionOptions|, run the following steps:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
Let |properties| be an object created with all properties defined in
the Thing with values set to `null`.
Takes as arguments |name:string| and |handler:PropertyReadHandler|.
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:string| and
|handler:PropertyReadHandler|, implementations MUST run the
following steps:
If invoking this method is not allowed for the current scripting context for security reasons, [= exception/throw =] a {{SecurityError}} and stop.
If |this|.{{ExposedThing/[[td]]}}.|properties|[|name|] does not [=map/exist=],
[= exception/throw =] {{NotFoundError}} and stop.
Set |this|{{ExposedThing/[[observeHandlers]]}}[|name|] to |handler|.
When a network request for observing a Property |name:string| is
received by the implementation with |options:InteractionOptions|,
run the following steps:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
If |this|.{{ExposedThing/[[td]]}}.|properties|[|name|] does not
[=map/exist=], send back a {{NotFoundError}} in the
reply and stop.
Internally save the request sender information together with |options| and
|this|.{{ExposedThing/[[propertyObservers]]}}[|name|], in order to be able
to notify about Property value changes.
Every time the value of |property| changes,
`emitPropertyChange()`
needs to be explicitly called by the application script.
The setPropertyUnobserveHandler() method
Takes as arguments |name:string| and |handler:PropertyReadHandler|.
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:string| and
|handler:PropertyReadHandler|, implementations MUST run the
following steps:
If invoking this method is not allowed for the current scripting context for security reasons, [= exception/throw =] a
{{SecurityError}} and stop.
If |this|.{{ExposedThing/[[td]]}}.|properties|[|name|] does not [=map/exist=],
[= exception/throw =] {{NotFoundError}} and stop.
Set |this|.{{ExposedThing/[[unobserveHandlers]]}}[|name|] to |handler|.
When a network request for unobserving a Property |name:string|
with |options:InteractionOptions| is received by the implementation,
run the following steps:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
If |this|.{{ExposedThing/[[td]]}}.|properties|[|name|] does not [=map/exist=],
send back a {{NotFoundError}} in the reply and stop.
Let |handler| be |this|.{{ExposedThing/[[unobserveHandlers]]}}[|name|];
If |handler| is a {{Function}}, invoke that given |options|, then send back a
reply following the Protocol Bindings and stop.
Otherwise, if |this|.{{ExposedThing/[[propertyObservers]]}}[|name|] [=map/exists=],
remove it from |this|.{{ExposedThing/[[propertyObservers]]}}, send back a reply as defined in the Protocol Bindings and stop.
Otherwise, send back a {{NotFoundError}} in the reply as defined in
the Protocol Bindings and stop.
The emitPropertyChange() method
Takes the |name:string| argument, denoting a Property name, and
optionally the |data:InteractionInput| 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:
Let |promise:Promise| be a new {{Promise}}.
Return |promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
Let |name| be the first argument.
Let |property| be |this|.{{ExposedThing/[[td]]}}.|properties|[|name|].
If |property| is `undefined`,
[=reject=] |promise| with {{NotFoundError}} and stop.
Let |data| be the second argument.
If |data| is `undefined`, run the following sub-steps:
Let |handler:function| be `null.`
If |name| does not [=map/exist=] in {{ExposedThing/[[readHandlers]]}},
[=reject=] |promise| and stop.
Let |handler| be {{ExposedThing/[[readHandlers]]}}[|name|].
If |handler| is `null` or `undefined`, [=reject=] |promise| and stop.
Let |handled| be the result of invoking |handler| given `null`.
If |handled| is [=rejected=], then [=reject=] |promise| and stop.
Otherwise if |handled| [=resolved=] with |value|, let |data| be |value|.
For each |observer| in {{ExposedThing/[[propertyObservers]]}}[|name|],
run the following sub-steps:
Let |options| be the interaction options saved with |observer|.
Request the underlying platform to create a |reply| from |data| and |options| according to the Protocol Bindings.
This clause needs expanding and/or refer to an algorithm in [[wot-binding-templates]].
Send |reply| to |observer|.
[=Resolve=] |promise|.
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:InteractionOutput| 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.
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.
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.
The setPropertyWriteHandler() method
Takes as arguments |name:string| and |handler:PropertyWriteHandler|.
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 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:string| and
|handler:PropertyWriteHandler|, implementations MUST run the
following steps:
If invoking this method is not allowed for the current scripting context for security reasons, [= exception/throw =] a {{SecurityError}} and stop.
If |this|.{{ExposedThing/[[td]]}}.|properties|[|name|] does not [=map/exist=],
[= exception/throw =] {{NotFoundError}} and stop.
Set |this|.{{ExposedThing/[[writeHandlers]]}}[|name|] to |handler|.
Handling requests for writing a Property
When a network request for writing a Property |name:string|
with a new value |value:InteractionOutput| and |options:InteractionOptions|
is received, implementations MUST run the following
update property steps, given |name|, |value|, |options|
and |mode| set to `"single"`:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
Let |interaction| be |this|.{{ExposedThing/[[td]]}}.|properties|[|name|].
If |interaction| is `undefined`, return a {{NotFoundError}} in the
reply and stop.
Let |handler:function| be |this|.{{ExposedThing/[[writeHandlers]]}}[|name|].
If |handler| is `undefined` and if there is a default write handler provided by the implementation, let |handler| be that.
If |handler| is `undefined`, send back a {{NotSupportedError}}
with the reply and stop.
Let |promise| be the result of invoking |handler| given |name| and
|options|. If it fails, return the error in the reply and stop.
If |mode| is `"single"`, reply to the request reporting success, following the Protocol Bindings and stop.
Handling requests for writing multiple Properties
When a network request for writing multiple Properties given in an object |propertyNames| is received with |options:InteractionOptions|,
run the following steps:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
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.
Reply to the request by sending a single reply according to the
Protocol Bindings.
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:InteractionOutput| and optionally
with an |options:InteractionOptions| object.
It returns a {{Promise}} that rejects with an error or resolves with
the value returned by the Action as {{InteractionInput}}.
Application scripts MAY return a {{ReadableStream}} object from an
{{ActionHandler}}. Implementations will then use the stream for
constructing the Action's response.
The setActionHandler() method
Takes as arguments |name:string| and |action:ActionHandler|.
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:string| and |action:ActionHandler|,
run the following steps:
If invoking this method is not allowed for the current scripting context for security reasons, [= exception/throw =] a {{SecurityError}} and stop.
Let |interaction| be |this|.{{ExposedThing/[[td]]}}.|actions|[|name|].
If |interaction| is `undefined`,
[= exception/throw =] a {{NotFoundError}} and stop.
Set |this|.{{ExposedThing/[[actionHandlers]]}}[|name|] to |action|.
When a network request for invoking the Action identified by |name:string| is received given |inputs| and optionally |options:InteractionOptions|, run the following steps:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
Let |interaction| be |this|.{{ExposedThing/[[td]]}}.|properties|[|name|].
If |interaction| is `undefined`, return a {{NotFoundError}} in the reply and stop.
Let |handler:function| be |this|.{{ExposedThing/[[actionHandlers]]}}[|name|].
If |handler| is `undefined`, return a {{NotSupportedError}} with the reply
created by following the Protocol Bindings and stop.
Let |promise| be the result of invoking |handler| given |name|, |inputs| and |options|.
If |promise| rejects, send the error with the reply and stop.
When |promise| resolves with |data:InteractionInput|, use |data| to
create and send the reply according to the Protocol Bindings.
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:InteractionOptions| 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.
The setEventSubscribeHandler() method
Takes as arguments |name:string| and |handler:EventSubscriptionHandler|.
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:string| and
|handler:EventSubscriptionHandler|, run the following steps:
If invoking this method is not allowed for the current scripting context for security reasons, [= exception/throw =] a {{SecurityError}} and stop.
Let |interaction| be |this|.{{ExposedThing/[[td]]}}.|events|[|name|].
If |interaction| is `undefined`,
[= exception/throw =] a {{NotFoundError}} and stop.
Set |this|.{{ExposedThing/[[subscribeHandlers]]}}[|name|] to |handler|.
When an Event subscription request for |name| is received by the
underlying platform with optional |options:InteractionOptions|,
run the following steps:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
Let |interaction| be |this|.{{ExposedThing/[[td]]}}.|events|[|name|].
If |interaction| is `undefined`, send back a
{{NotFoundError}} and stop.
If |this|.{{ExposedThing/[[subscribeHandlers]]}}[|name|] is a {{Function}},
invoke it given |options| and stop.
Otherwise implement the default subscriber mechanism:
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.
Set |this|.{{ExposedThing/[[eventListeners]]}}[|name|] to |subscriber|.
The setEventUnsubscribeHandler() method
Takes as arguments |name:string| and |handler:EventSubscriptionHandler|.
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:string| and
|handler:EventSubscriptionHandler|, run the following steps:
If invoking this method is not allowed for the current scripting context for security reasons, [= exception/throw =] a {{SecurityError}} and stop.
Let |interaction| be |this|.{{ExposedThing/[[td]]}}.|events|[|name|].
If |interaction| is `undefined`,
[= exception/throw =] a {{NotFoundError}} and stop.
Set |this|.{{ExposedThing/[[unsubscribeHandlers]]}}[|name|] to |handler|.
When an Event unsubscribe request for |name| is received by the
underlying platform optionally with |options:InteractionOptions|,
run the following steps:
If this operation is not supported, send back a {{NotSupportedError}}
according to the Protocol Bindings and stop.
If this operation is not allowed, send back a {{NotAllowedError}}
according to the Protocol Bindings and stop.
Let |interaction| be |this|.{{ExposedThing/[[td]]}}.|events|[|name|].
If |interaction| is `undefined`, send back a
{{NotFoundError}} and stop.
If |this|.{{ExposedThing/[[unsubscribeHandlers]]}}[|name|]
[=map/exists=] and is a {{Function}}, invoke it given |options| and stop.
Otherwise |name| [=map/exists] in |this|.{{ExposedThing/[[eventListeners]]}},
[=map/remove=] |name|.
When an Event with name |name| is emitted with
|data:InteractionInput| by the
emitEvent() method, run the
following steps:
Let |listeners| be {{ExposedThing/[[eventListeners]]}}.|name|.
For each |subscriber| in |listeners|, run the following sub-steps:
Create an Event notification |response| according to the
Protocol Bindings from |data| and |subscriber|, including
its |options|.
If |data| is `undefined`, assume that the notification |response|
will contain an empty data payload as specified by Protocol Bindings.
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|.
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.
Send |response| to the subscriber identified by |subscriber|.
The emitEvent() method
Takes as arguments |name:string| denoting an Event name and
optionally |data:InteractionInput|.
Triggers emitting the Event with the optional data.
The method MUST run the following steps:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
Let |interaction| be {{ExposedThing/[[td]]}}.|events|.|name|.
If an Event with the name |name| is not found,
[=reject=] |promise| with {{NotFoundError}} and stop.
Make a request to the underlying platform to emit an Event
with optional |data|. Call the handling events
steps.
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
Run the expand a TD steps on the {{ExposedThing/[[td]]}}.
Run the validate a TD on {{ExposedThing/[[td]]}}. If that fails,
[=reject=] |promise| with a {{TypeError}} and stop.
For each |key| in {{ExposedThing/[[td]]}}.|properties| initialize
|this|.{{ExposedThing/[[propertyObservers]]}}.|key| to an empty {{Array}}
in order to store observe request data needed to notify the observers on value changes.
For each |key| in |this|.{{ExposedThing/[[td]]}}.|events| initialize
|this|.{{ExposedThing/[[eventListeners]]}}.|key| to an empty {{Array}}
in order to store subscribe request data needed to notify the subscribers on event emission.
Set up the WoT Interactions based on introspecting {{ExposedThing/[[td]]}}
as explained in [[!wot-thing-description11]] and [[!wot-binding-templates]].
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).
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.
Otherwise [=resolve=] |promise| and stop.
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:
Return a {{Promise}} |promise:Promise| and execute the next steps [=in parallel=].
If invoking this method is not allowed for the current scripting context for security reasons, [=reject=] |promise| with a {{SecurityError}} and stop.
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.
Otherwise [=resolve=] |promise| and stop.
ExposedThing Examples
The next example illustrates how to create an {{ExposedThing}} based on a partial TD object constructed beforehand.
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.
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.
TODO: add more examples where the ExposedThingInit contains suggested values that are replaced by the algorithm.
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.
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 when used within the `exploreDirectory()` method.
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.
Constructing {{ThingDiscoveryProcess}}
To create {{ThingDiscoveryProcess}} with a |filter:ThingFilter|, run the following steps:
If |filter| is not an object or `null`, [= exception/throw =] a {{TypeError}} and stop.
Let |discovery:ThingDiscoveryProcess| be a new {{ThingDiscoveryProcess}} object.
Set |discovery|.{{ThingDiscoveryProcess/[[filter]]}} to |filter|.
Set |discovery|.{{ThingDiscoveryProcess/done}} to `false`.
Set |discovery|.{{ThingDiscoveryProcess/error}} to `null`.
Return |discovery|.
The ThingFilter dictionary
Represents an object containing the constraints for discovering Things as key-value pairs.
dictionary ThingFilter {
object? fragment;
};
The fragment property represents a template object used for matching property by property against discovered Things.
The |query:DOMString| 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.
The |url:USVString| 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.
The discovery process algorithm
Describes what to do during a discovery process that has already started.
The algorithm, given |discovery:ThingDiscoveryProcess|, runs the following steps:
Whenever a new |link| to a Thing Description
is discovered and provided by the underlying platform, run the following
sub-steps:
Retrieve |td:ThingDescription| 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.
If that fails, set the |discovery|.{{ThingDiscoveryProcess/error}}
property to {{SyntaxError}}, discard |td| and continue the discovery process.
Whenever a Thing Description |td| is discovered and provided
by the underlying platform or by the previous step, run the following
sub-steps:
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|.
Let |fragment| be |discovery|.{{ThingDiscoveryProcess/[[filter]]}}.{{ThingFilter/fragment}}.
If |fragment| is an {{object}}, then for each |key| defined in it:
Check if that |key| [=map/exists=] in |json| and |json|[|key| is equal to |fragment|.|key|.
If this is fails in any checks, discard |td| and continue the discovery process.
Yield |td| using {{Symbol/asyncIterator}}.
Improve this step using proper asyncIterator terminology.
Whenever an error occurs during the discovery process, run the following
sub-steps:
The last error is retained. Implementations MAY choose to stop the
discovery process if they consider it should be reported.
Let |error| be a new {{Error}} object.
Set |error|.|name| to `"DiscoveryError"`.
If there was an error code or message provided by the
Protocol Bindings, set |error|.|message| to that value as string.
Set |discovery|.{{ThingDiscoveryProcess/error}} to |error|.
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|.{{ThingDiscoveryProcess/done}} to `true` and
terminate these steps.
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:
If invoking this method is not allowed for the current scripting context for security reasons, [= exception/throw =] a {{SecurityError}} and stop.
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.
Set the {{ThingDiscoveryProcess/done}} property to `true`.
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 {{Symbol/asyncIterator}} provided by the Discovery object, we can iterate asynchronously over the results and perform operations with the obtained {{ThingDescription}} objects.
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.
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.
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);
}
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:
Implementors of WoT Runtimes that do not implement a Scripting Runtime.
The [[!wot-architecture11]] document provides generic security guidelines
for this group.
Implementors of the WoT Scripting API in a WoT Scripting Runtime. This is the main scope and is covered in the
Scripting Runtime Security and Privacy Risks sub-section that
contains normative text regarding security.
WoT script developers, covered in the
Script Security and Privacy Risks sub-section that contains
informative recommendations concerning security.
Scripting Runtime Security and Privacy Risks
This section is normative and contains specific risks relevant for the WoT Scripting Runtime.
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]].
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.
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]].
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.
Script Security and Privacy Risks
This section describes specific risks relevant for script developers.
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]].
Denial of Service (DoS) Security Risk
If a script performs 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]].
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.
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.
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.
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.
let lock = await WoT.consume('https://td.my.com/lock-00123');
console.log(lock.status);
lock.open('withThisKey');
This API, aligned with the [[[wot-thing-description11]]] 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.
In conclusion, the WoT WG decided to explore the third option that
closely follows the [[[wot-thing-description11]]] 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 [[[wot-thing-description11]]] 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.
Requesting and validating a TD
The current version of this specification defines a new
`requestThingDescription` method that simplifies the process of
retrieving and validating a Thing Description.
However, it only covers simple use cases that do not require additional
parameters or special HTTP headers for the retrieval.
More sophisticated use cases need to be covered by external methods, such
as the Fetch API or an
HTTP client library, which offer already standardized options on
specifying fetch details.
In these cases, the user is required to perform validation manually as
described by the [[[wot-thing-description11]]] specification.
In the future, `requestThingDescription()` might be extended with an
`options` argument, including frequently used `fetch` options.
Please
open an issue
to request support for options.
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.
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.
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 [[[wot-thing-description11]]] specification.
Changes
The following is a list of major changes to the document. Major versions of this specification are the following:
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.