Web of Things(WoT)脚本 API

W3C 小组说明

关于本文档的更多详细信息
此版本:
https://www.w3.org/TR/2023/NOTE-wot-scripting-api-20231003/
最新发布版本:
https://www.w3.org/TR/wot-scripting-api/
最新编辑草案:
https://w3c.github.io/wot-scripting-api/
历史:
https://www.w3.org/standards/history/wot-scripting-api/
提交 历史
编辑:
Zoltan Kis (Intel)
Daniel Peintner (Siemens AG)
Cristiano Aguzzi (受邀专家)
Johannes Hund (前编辑,当时任职于 Siemens AG)
Kazuaki Nimura (前编辑,任职于 Fujitsu Ltd.)
反馈:
GitHub w3c/wot-scripting-api拉取 请求 新问题未解决 问题
public-wot-wg@w3.org,主题行请写为 [wot-scripting-api] … 消息主题 …归档
仓库
在 GitHub 上
提交 bug
贡献者
GitHub 上的贡献者

摘要

万维物联网由实体()组成,这些实体可以在机器可解释的物 描述(TD)中描述其能力,并通过 WoT 接口公开这些能力,也就是建模为 属性(用于读取 和写入值)、动作(用于 执行带有或不带返回值的远程过程)和 事件(用于 发出 通知信号)的网络交互。

主要的 万维 物联网(WoT)概念在 Web of Things(WoT)架构 1.1 规范中描述。

脚本编写是 WoT 中一个可选的构建块,它通常用于能够运行 WoT 运行时 脚本管理的网关或浏览器中,提供一种便捷方式来将 WoT 支持扩展到新类型的端点,并实现诸如 TD 目录之类的 WoT 应用。

本规范描述一个应用程序编程 接口(API),该接口表示 WoT 接口,允许 脚本发现、操作,并公开由脚本指定的 ,这些本地定义的物 以 WoT 交互 为特征。

本文档中定义的 API 有意紧密遵循 Web of Things(WoT)物描述 1.1 规范。可以在它们之上实现更 抽象的 API,或直接实现面向 WoT 网络的接口(即 WoT 接口)。

编辑注

本规范至少已由 Eclipse Thingweb 项目实现,该项目也称为 node-wot, 目前被视为参考开源实现。请查看其源 代码,包括 示例

本文档状态

本节描述本文档在 发布时的状态。当前 W3C 出版物列表以及 本技术报告的最新修订版,可在 W3C 技术报告 索引 https://www.w3.org/TR/ 中找到。

实现者需要注意,本规范 被认为是不稳定的。有意在本规范最终达到候选 推荐阶段之前实现本 规范的厂商,应订阅该仓库并 参与讨论。

编辑注:W3C WoT WG 正在征求反馈

请使用 GitHub Issues 页面为本草案做出贡献,该页面属于 WoT 脚本 API 仓库。关于安全和隐私 考量的反馈,请使用 WoT 安全与 隐私 Issues。

本文档由 Web of Things 工作 组作为小组说明发布,使用 说明 轨道

本小组说明得到 Web of Things 工作 组认可,但未得到 W3C 本身或其 成员认可。

这是一份草案文档,可能随时被更新、替换或 废弃。不宜 将本文档作为非进行中工作的内容引用。

W3C 专利政策并 不对本文档施加任何许可要求或承诺。

本文档受 2023 年 6 月 12 日 W3C 流程 文档约束。

1. 引言

WoT 基于的使用方式提供分层互操作性:即“被使用”和 “被公开”,如 Web of Things(WoT)架构 1.1 术语中所定义。

通过使用 TD,客户端 会创建一个本地运行时资源模型,该模型允许访问远程设备上服务器 所公开的 属性动作事件

公开一个 需要: 本规范描述如何通过脚本公开和使用 。此外, 它还定义了一个用于发现的通用 API。

通常,脚本旨在用于桥接器 或网关,这些桥接器或网关将较简单的设备公开和控制为 WoT ,并且 具有处理(例如安装、卸载、更新等)和运行 脚本的手段。

本规范不对 WoT 运行时如何处理和运行脚本做出假设,包括单租户或 多租户、脚本部署和生命周期管理。 该 API 已经支持通用机制,使得 实现脚本管理成为可能,例如通过 公开一个管理器, 其动作 (动作处理器)实现脚本生命周期管理 操作。

2. 用例场景

本节为非规范性内容。

[WOT-USE-CASES] 文档中列出的业务用例可以使用此 API 实现,基础是此处描述的 脚本用例场景。

2.1 使用物

2.2 公开物

2.3 发现

3. 一致性

除标记为非规范性的章节外,本规范中的所有编写 指南、图表、示例和注释均为非规范性内容。除此之外,本规范中的所有内容都是 规范性的。

本文档中的关键词 MAYMUSTSHOULD 应按 BCP 14 [RFC2119] [RFC8174] 中的描述解释,但仅当它们以全大写形式出现时, 如此处所示。

编辑注

本规范曾是一个预期成为 W3C 推荐标准的工作草案。 然而,它现在是一个 WG 说明,其中仅包含资料性 陈述。因此,我们需要考虑如何处理 本一致性章节中的描述。

本规范描述以下类别的用户代理UA)的一致性标准。

由于小型嵌入式实现的要求, 需要拆分 WoT 客户端和服务器接口。随后, 发现是一个分布式应用,但典型场景 已通过本规范中的通用发现 API 覆盖。 这导致实现此 API 的 UA 使用 3 个一致性类别:一个用于 客户端,一个用于服务器,一个用于发现。使用此 API 的应用 可以内省 WoT API 对象上是否存在 consume()produce()discover() 方法,以便 确定该 UA 实现的是哪个一致性类别。

WoT Consumer UA

此一致性类别的实现 MUST 实现 ConsumedThing 接口,以及 WoT API 对象上的 consume() 方法。

WoT Producer UA

此一致性类别的实现 MUST 实现 ExposedThing 接口,以及 WoT API 对象上的 produce() 方法。

WoT Discovery UA

此一致性类别的实现 MUST 实现 ThingDiscoveryProcess 接口,以及 WoT API 对象上的 discover() 方法、 exploreDirectory() 方法和 requestThingDescription() 方法。

这些一致性类别 MAY 可以在单个 UA 中实现。

本规范可用于以多种编程语言实现 WoT 脚本 API。接口 定义在 [WEBIDL] 中指定。

UA 可以在 浏览器中实现,也可以在单独的运行时环境中实现,例如 Node.js,或在 小型嵌入式 运行时中实现。

使用在浏览器中执行的 ECMAScript 来 实现本文档中定义的 API 的实现,MUST 以与 Web IDL 规范中定义的 ECMAScript 绑定 [WEBIDL] 一致的方式实现它们。

使用运行时中的 TypeScript 或 ECMAScript 来 实现本文档中定义的 API 的实现, MUST 以与 TypeScript 规范中定义的 TypeScript 绑定 [TYPESCRIPT] 一致的方式实现它们。

4. 术语和约定

通用 WoT 术语定义于 [WOT-ARCHITECTURE]: ThingThing Description(简称 TD)、Partial TDWeb of Things(简称 WoT)、WoT InterfaceProtocol BindingsWoT RuntimeConsuming a Thing DescriptionTD DirectoryPropertyActionEvent DataSchema Form SecurityScheme NoSecurityScheme 等。

WoT Interaction Interaction Affordance 的同义词。Interaction Affordance(或简称 affordance)是 [WOT-TD] 在指称 能力时使用的术语,如 TD issue 282 中所解释。然而,该术语在 TD 语义 上下文之外并不易理解。因此,为提高可读性,本文档将 改用先前的术语 WoT interaction,或简称 interaction

WoT network interfaceWoT Interface 的同义词。

JSON Schema 定义于 这些 规范中。

PromiseErrorJSONJSON.stringifyJSON.parse internal method internal slot 定义于 [ECMASCRIPT]。

5. ThingDescription 类型

WebIDLtypedef object ThingDescription;

表示 [WOT-TD] 中 定义的一个物描述TD)。 它预期是一个 解析后的 JSON 对象,并使用 JSON Schema 验证进行验证。

5.1 获取物 描述

给定 URL 获取 TD 应通过外部方法完成,例如 Fetch API 或 HTTP 客户端库,这些方法已经提供了用于指定获取细节的标准化选项。

示例 1:获取 物描述
try {
  let res = await fetch('https://tds.mythings.biz/sensor11');
  // ... 可以对 res.headers 进行额外检查
  let td = await res.json();
  let thing = await WOT.consume(td);
  console.log("物名称:" + thing.getThingDescription().title);
} catch (err) {
  console.log("获取 TD 失败", err.message);
}

5.2 展开物 描述

请注意, Web of Things(WoT)物描述 1.1 规范允许借助 默认值使用简写的物 描述,并要求客户端使用 Web of Things(WoT)物描述 1.1 规范中为给定 TD 中未显式 定义的属性指定的默认值来展开它们。

要在给定 td 的情况下展开 TD, 运行以下步骤:
  1. 对于 [WOT-TD] 中 TD 默认值表中的每一项, 如果该术语未在 td 中定义,则使用 [WOT-TD] 中指定的默认值添加该术语 定义。

5.3 验证物 描述

[WOT-TD] 规范定义了应如何验证 TD。因此, 此 API 期望 ThingDescription 对象在作为参数使用之前已被验证。本 规范定义如下基本的 TD 验证。

要在给定 td 的情况下验证 TD,运行 以下步骤:
  1. 如果 JSON Schema 验证td 上失败,则抛出一个 “TypeError” 并停止。
编辑注: 处理默认值

可以添加其他步骤来填充 必填字段的默认值。

6. WOT 命名空间

WoT API 对象 定义为单例,并包含按 一致性类别分组的 API 方法。

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

6.1 consume() 方法

WebIDLpartial namespace WOT {
  Promise<ConsumedThing> consume(ThingDescription td);
};
属于 WoT Consumer 一致性 类别。期望一个 td 实参,并返回一个 Promise,该 Promise 会以一个 ConsumedThing 对象兑现,该对象表示用于操作 的客户端接口。 该方法 MUST 运行以下 步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise,并停止。
  3. thing 为一个 从 td 构造的新 ConsumedThing 对象。
  4. 基于对 td 的内省,按照 [WOT-TD] 和 [WOT-PROTOCOL-BINDINGS] 中的说明设置 WoT 交互。 向底层平台发出请求,以初始化 协议 绑定
    编辑注

    实现会封装如何使用 协议 绑定来实现 WoT 交互的 复杂性。 未来,其中一些元素可以被标准化。

  5. thing 兑现 promise
编辑注

请注意构造 ConsumedThing 与使用 consume() 方法之间的区别:后者 还会初始化协议绑定,而简单 构造的对象在其被调用之前,不会初始化 WoT 交互

6.2 produce() 方法

WebIDLtypedef object ExposedThingInit;

partial namespace WOT {
  Promise<ExposedThing> produce(ExposedThingInit init);
};
属于 WoT Producer 一致性 类别。期望一个 init 实参,并返回一个 Promise,该 Promise 会以一个 ExposedThing 对象兑现,该对象通过服务器接口扩展 ConsumedThing, 即定义请求 处理器的能力。init 对象是 ExposedThingInit 类型的一个实例。具体而言,一个 ExposedThingInit 值是用于初始化 ExposedThing 的字典,并且它表示一个 [WOT-ARCHITECTURE] 中描述的 Partial TD。 因此,它具有与物描述相同的结构, 但可以省略一些信息。该方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise,并停止。
  3. thing 为一个 使用 init 构造的新 ExposedThing 对象。
  4. thing 兑现 promise

6.2.1 展开 ExposedThingInit

要在给定 init 并以有效的 td 作为结果的情况下展开 ExposedThingInit,运行 以下步骤:
  1. init 上运行 validate an ExposedThingInit。如果失败,则 抛出 SyntaxError 并停止。
  2. td 为给定 init 运行 clone 的结果。
  3. 对于 td.["securityDefinitions"] 中的每个 scheme, 向底层平台发出请求,检查它是否 至少受一个协议绑定支持。 如果不支持,则从 td 中移除 scheme
  4. 如果 td.["security"] 在 td.["securityDefinitions"] 中 不存在, 则从 td 中移除 security
  5. 对于 td.properties、td.actions 和 td.events 中的每个 affordance,运行以下 子步骤:
    1. 对于 affordance.forms 中的每个 form
      1. 如果运行时未将 form.contentType 识别为有效,则从 form 中移除 contentType
      2. 如果 form.href 具有未知 scheme,则从 form 中移除 href
      3. 如果 form.href 是绝对的,并且运行时未将其 authority 识别为有效,则从 form 中移除 href
      4. 如果 form.href 已被其他 ExposedThings 使用, 则从 form 中移除 href
  6. 按照 TD JSON Schematd 中搜索缺失的必需属性。
    编辑注

    编辑认为此步骤含糊。它将在 下一次迭代中得到改进或移除。

  7. 对于每个 missing 属性,运行这些 子步骤:
    1. 如果 missingtitle, 生成一个运行时唯一名称并赋给 title
    2. 如果 missing@context, 分配最新受支持的 Thing Description 上下文 URI。
    3. 如果 missinginstance, 分配字符串 1.0.0
    4. 如果 missingforms, 使用可用的 协议 绑定和内容类型编码器生成一个 Forms 列表。然后 将所得列表赋给 forms
    5. 如果 missingsecurity, 分配 securityDefinitions 字段中第一个受支持的 SecurityScheme 的标签。如果未找到 SecurityScheme, 则生成一个名为 nosecNoSecurityScheme, 并将字符串 nosec 赋给 security
      Issue 1

      关于如何适当地 为 security 生成值的讨论 仍处于开放状态。参见 issue #299

    6. 如果 missinghref,则将 formStub 定义为不具有 href 的部分 Form。使用第一个 满足 formStub 要求的 协议 绑定生成一个有效的 url。将 url 赋给 href。如果找不到 协议 绑定,则从 td 中移除 formStub
    7. missing 添加到 td,并以 value 作为 值
  8. td 上运行 validate a TD。如果 失败,则重新抛出 该错误并停止
  9. 返回 td

6.2.2 验证 ExposedThingInit

要在给定 init 的情况下验证 ExposedThingInit,运行 以下步骤:
  1. 解析 TD JSON Schema,并将其加载到名为 exposedThingInitSchema 的对象中
  2. optional 为一个列表, 其中包含以下字符串:title@contextinstanceformssecurityhref
  3. 对于 exposedThingInitSchema 中等于 required 的每个属性和子属性 key,执行以下 步骤:
    1. 如果 keyvalue 是一个 Array,则移除其中所有等于 optional 中元素的元素
    2. 如果 keyvalue 是一个 string,则如果 value 等于 optional 中的某个元素,则从 exposedThingInitSchema 中移除 key
  4. 返回给定 initexposedThingInitSchema 运行 validating an object with JSON Schema 的结果。
    编辑注

    validating an object with JSON Schema 步骤仍在讨论中。目前,本 规范引用 JSONSchema 的验证过程。 在用 exposedThingInitSchema 验证 init 时,请遵循此 文档。请注意, 工作组正在评估另一种形式化 方法。

6.3 discover() 方法

WebIDLpartial namespace WOT {
  Promise<ThingDiscoveryProcess> discover(optional ThingFilter filter = {});
};
属于 WoT Discovery 一致性 类别。启动发现过程,该过程将提供 与类型为 ThingFilter 的可选 filter 实参匹配的物描述ThingDescription 对象。 该方法 MUST 运行以下 步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise,并停止。
  3. 如果实现不支持发现, 则用 NotSupportedError 拒绝 promise,并停止。
  4. discovery 为一个新的 ThingDiscoveryProcess 对象。
  5. discovery.[[filter]] 设置为 filter
  6. discovery.[[url]] 设置为 undefined
  7. 如果实现通常不支持过滤器,并且 filter 不是 undefinednull,则用 NotSupportedError 拒绝 promise,并停止。
  8. 如果底层平台无法启动发现, 则用 OperationError 拒绝 promise,并停止。
  9. 请求底层平台通过 WoT 运行时 中任何受支持并已预配、且脚本有权访问的方式来启动 发现过程, 并将 discovery 传递给它。
  10. discovery 兑现 promise

6.4 exploreDirectory() 方法

WebIDLpartial namespace WOT {
  Promise<ThingDiscoveryProcess> exploreDirectory(USVString url,
      optional ThingFilter filter = {});
};
属于 WoT Discovery 一致性 类别。启动发现过程,该过程在给定 TD 目录 URL 时, 将提供 与类型为 ThingFilter 的可选 filter 实参匹配的物描述ThingDescription 对象。 该方法 MUST 运行以下 步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise,并停止。
  3. 如果实现不支持目录发现, 则用 NotSupportedError 拒绝 promise,并停止。
  4. discovery 为一个新的 ThingDiscoveryProcess 对象。
  5. discovery.[[url]] 设置为 url
  6. discovery.[[filter]] 设置为 filter
  7. 请求底层平台启动 目录发现过程。

    这是发现算法中 更多细节的占位符。实现应 遵循 [WOT-DISCOVERY] 和 [WOT-PROTOCOL-BINDINGS] 规范中描述的过程。下面 指出了一些规范性步骤。

    1. 如果 url 不是一个 TD 目录,或底层 实现无法支持 url 指示的 协议 绑定,则用 NotSupportedError 拒绝 promise,并终止 这些步骤。
    2. 如果实现通常不支持过滤器,并且 filter 不是 undefinednull, 则用 NotSupportedError 拒绝 promise,并停止。
    3. 给定 discovery,运行 发现 过程

      从此时起,错误 只记录在 error 上,但不再影响 promise

  8. discovery 兑现 promise

6.5 requestThingDescription() 方法

WebIDLpartial namespace WOT {
  Promise<ThingDescription> requestThingDescription(USVString url);
};
属于 WoT Discovery 一致性 类别。从给定 URL 请求一个物描述。 该方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise,并停止。
  3. 如果实现不支持获取物 描述, 则用 NotSupportedError 拒绝 promise,并停止。
  4. td 为向底层平台发出请求,使用 url 指定的 协议绑定 检索 物描述 的结果。如果检索 td 失败,则用 NotFoundError 拒绝 promise,并停止。
  5. td 兑现 promise

7. 处理交互数据

Web of Things(WoT)物描述 1.1 规范所指定,WoT 交互扩展 DataSchema,并包含 若干可能的表单,其中一个会被 选作该交互使用。 表单包含一个 contentType,用于描述 数据。对于某些内容类型,会定义一个基于 JSON SchemaDataSchema,从而 可以将这些内容表示为 JavaScript 类型,并 最终在数据上设置范围约束。

7.1 InteractionInput 类型

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

属于 WoT Consumer 一致性 类别,并表示由应用脚本提供给 UA 的 WoT 交互数据。

DataSchemaValue 是一个 ECMAScript 值,可被 [WoT-TD] 中定义的 DataSchema接受。 可能的值 MUST 属于 null boolean number stringarrayobject 类型。

ReadableStream 旨在用于那些在物 描述中没有 DataSchema,而只有 可以由流表示的FormcontentTypeWoT 交互

实践中,任何 ECMAScript 值都可用于那些在 物描述中定义了 DataSchemaWoT 交互,或用于那些可由实现映射到 物 描述中定义的 FormcontentType 的交互。

本文档中的算法指定了输入 数据在 WoT 交互中到底如何使用。

7.2 InteractionOutput 接口

属于 WoT Consumer 一致性 类别。InteractionOutput 对象始终由实现创建,并将从 WoT 交互返回的数据公开给 应用脚本。

此接口公开一个便利函数,该函数应 覆盖绝大多数 IoT 用例:value() 函数。其 实现会检查数据;如果数据符合 DataSchema,则解析它;否则会尽早失败, 让底层流保持未受扰动,以便 应用脚本可以尝试自行读取该流, 或将数据作为 ArrayBuffer 处理。

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

data 属性表示 WoT 交互中的原始 载荷,其形式为 ReadableStream, 初始为 null

dataUsed 属性说明 数据流是否已被 扰动。初始为 false

form 属性 表示从物描述中为 此 WoT 交互选择的表单, 初始为 null

schema 属性表示载荷的 DataSchema(定义于 [WoT-TD]), 其形式为一个 JSON 对象,初始为 null

[[value]] 内部槽表示 WoT 交互的已解析 值,初始为 undefined(注意 null 是有效值)。

7.2.1 value() 函数

解析由 WoT 交互返回的数据,并返回一个具有交互 DataSchema(如果存在)所描述类型的值, 或具有交互 表单contentType 所描述类型的值。该 方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果 this.[[value]] 不是 undefined,则以该值 兑现 promise 并停止。
  3. 如果 this.data 不是 ReadableStream,或 dataUsedtrue,或 form 不是 object, 或 schema 或其 typenullundefined,则用 NotReadableError 拒绝 promise 并停止。
  4. 如果 form.contentType 不是 application/json,并且 协议绑定中没有可用的映射 可将 form.contentType 映射到 [JSON-SCHEMA], 则用 NotSupportedError 拒绝 promise 并停止。
  5. reader 为从 data 获取 reader的结果。如果这 抛出异常,则用该 异常拒绝 promise 并停止。
  6. bytes 为使用 readerdata 读取所有字节的结果。
  7. dataUsed 设置为 true
  8. 如果 form.contentType 不是 application/json,并且 协议 绑定中存在可用映射,可将 form.contentType 映射到 [JSON-SCHEMA], 则使用该映射转换 bytes
  9. json 为在 bytes 上运行 parse JSON from bytes 的结果。如果这 抛出异常,则用该 异常拒绝 promise 并停止。
  10. [[value]] 设置为 在 jsonschema 上运行 check data schema 的结果。如果这抛出异常,则用该 异常拒绝 promise 并停止。
  11. [[value]] 兑现 promise

7.2.2 arrayBuffer() 函数

调用时,MUST 运行 以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果 data 不是 ReadableStream,或 dataUsedtrue,则用 NotReadableError 拒绝 promise 并停止。
  3. reader 为从 data 获取 reader的结果。如果这 抛出异常,则用该 异常拒绝 promise 并停止。
  4. bytes 为使用 readerdata 读取所有字节的结果。
  5. dataUsed 设置为 true
  6. arrayBuffer 为一个新的 ArrayBuffer,其内容为 bytes。如果这抛出异常,则用该 异常拒绝 promise 并停止。
  7. arrayBuffer 兑现 promise

7.2.3 check data schema 算法

要在 payloadschema 上运行 check data schema 步骤,
  1. typeschema.type
  2. 如果 type"null",并且 payload 不是 null,则抛出 TypeError 并停止;否则返回 null
  3. 如果 type"boolean",且 payload 是 falsy 值或其字节长度为 0,则返回 false;否则返回 true
  4. 如果 type"integer""number"
    1. 如果 payload 不是数字,则抛出 TypeError 并停止。
    2. 如果 form.minimum 已定义 且 payload 更小,或 form.maximum 已定义且 payload 更大,则抛出 RangeError 并停止。
  5. 如果 type"string",则返回 payload
  6. 如果 type"array",则运行这些 子步骤:
    1. 如果 payload 不是数组,则抛出 TypeError 并停止。
    2. 如果 form.minItems 已定义 且 payload.length 小于 该值,或 form.maxItems 已定义且 payload.length 大于 该值,则抛出 RangeError 并停止。
    3. payload 为一个由以下方式获得的项数组: 对 payload 的每个元素 itemschema.items 运行 check data schema 步骤。如果这在任何阶段抛出异常,则重新抛出该异常并 停止。
  7. 如果 type"object",则运行 这些子步骤:
    1. 如果 payload schema.properties 不是 object, 则抛出 TypeError 并停止。
    2. 对于 payload 中的每个 key
      1. proppayload[key]。
      2. propSchemainteraction.properties[key]。
      3. prop 为在 proppropSchema 上运行 check data schema 步骤的结果。如果这抛出异常,则重新抛出 该异常并停止。
    3. 如果 schema.required 是数组,则令 required 为该数组;否则令其为 空数组。
    4. 对于 required 中的每个 key, 如果 key 未出现在 payload 中,则抛出 SyntaxError 并停止。
  8. 返回 payload

7.2.4 create interaction request 算法

对于给定的 ConsumedThing 对象 thing,为 在给定 sourceformschema 的情况下 创建 交互请求,运行这些步骤:
  1. idata 为一个新的 InteractionOutput 对象。
  2. idata.form 设置为 form,将 idata.schema 设置为 schema,将 |idata.data 设置为 null,并将 idata.[[value]] 设置为 undefined
  3. 如果 source 是 一个 ReadableStream 对象,则令 idata.datasource,返回 idata 并停止。
  4. 如果 schema 及其 type 已定义且不为 null,则运行 这些子步骤:
    1. 如果 type"null",并且 source 不是 "null",则抛出 TypeError 并停止。
    2. 如果 type"boolean",并且 source 是 falsy 值,则将 idata.[[value]] 设置为 false;否则将其设置为 true
    3. 如果 type"integer""number",且 source 不是 数字,或 form.minimum 已定义且 source 更小,或 form.maximum 已定义且 source 更大,则抛出 RangeError 并停止。
    4. 如果 type"string",且 source 不是 字符串,则令 idata.[[value]] 为 给定 source 运行 serialize JSON to bytes 的结果。如果该结果为 failure,则抛出 SyntaxError 并停止。
    5. 如果 type"array",则运行 这些子步骤:
      1. 如果 source 不是数组, 则抛出一个 TypeError 并停止。
      2. lengthsource 的长度。
      3. 如果 form.minItems 已定义 且 length 小于该值,或 form.maxItems 已定义 且 length 大于该值,则抛出 RangeError 并停止。
      4. 对于 source 中的每个 item,令 itemschema schema.items,并令 item 为给定 itemformitemschema 运行 create interaction request 步骤的结果。如果 这抛出异常,则重新抛出该异常并停止。
      5. data.[[value]] 设置为 source
    6. 如果 type"object",则运行 这些子步骤:
      1. 如果 source 不是对象, 则抛出 TypeError 并停止。
      2. 如果 schema.properties 不是对象,则抛出 TypeError 并停止。
      3. 对于 source 中的每个 key
        1. valuesource[key]。
        2. propschemaproperties.interactions[key]。
        3. value 为在 valueformpropschema 上运行 create interaction request 步骤的结果。 如果这抛出异常,则重新抛出该异常并 停止。
      4. 如果 schema.required 是 数组,则对于 required 中的每个 item,检查 item 是否是 source 中的属性名。如果在 source 中未找到某个 item,则抛出 SyntaxError 并停止。
      5. data.[[value]] 设置为 source
  5. idata.data 设置为一个新的 ReadableStream,该流由 idata.[[value]] 内部槽创建,并将其作为该流的底层 源
  6. 返回 idata

7.2.5 parse interaction response 算法

对于给定的 ConsumedThing 对象 thing,为 在给定 responseformschema 的情况下 解析 交互响应,运行这些步骤:
  1. result 为一个新的 InteractionOutput 对象。
  2. result.schemaschema
  3. result.formform
  4. result.data 为一个新的 ReadableStream,其中 response 的载荷数据作为其底层 源
  5. result.dataUsedfalse
  6. 返回 result

如下图所示,每当实现向 脚本提供数据时,都会使用 InteractionOutput 接口;而当脚本向实现传递数据时, 使用 InteractionInput

1 读取 数据时使用的数据结构

ConsumedThing 读取数据时,它会从实现接收一个 InteractionOutput 对象。

ExposedThing 读取处理器InteractionInput 的形式向实现提供读取数据。

2 写入 数据时使用的数据结构

ConsumedThing 写入数据时,它会以 InteractionInput 的形式将数据提供给实现。

ExposedThing 写入 处理器会从实现接收一个 InteractionOutput 对象作为数据。

3 调用 动作时使用的数据结构

ConsumedThing 调用一个动作时, 它会以 InteractionInput 的形式提供参数,并以 InteractionOutput 对象的形式接收该动作的输出。

ExposedThing 动作处理器InteractionOutput 对象的形式从实现接收实参,并以 InteractionInput 的形式向实现提供动作输出。

7.4 错误处理

此 API 中的算法定义了要 报告给应用脚本的错误。

报告给另一通信端的错误由 协议 绑定进行映射和封装。

4 WoT 交互中的错误 处理
编辑注

此主题仍在 Issue #200 中讨论。为了确保将脚本错误映射到 协议错误以及反向映射的一致性,需要一个标准化的错误映射。 尤其是,当 算法提到“从协议绑定收到的错误”时, 这将被分解为一个显式的错误映射 算法。目前,它由 实现封装。

8. ConsumedThing 接口

表示用于操作一个的客户端 API。属于 WoT Consumer 一致性 类别。

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

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

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

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

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

callback InteractionListener = undefined(InteractionOutput data);
callback ErrorListener = undefined(Error error);
编辑注:writeAllProperties 方法在哪里?

writeAllProperties() 方法 仍在讨论中。与此同时,请改用 writeMultipleProperties() 方法。

8.1 ConsumedThing 的内部槽

ConsumedThing 对象具有以下 内部槽

内部槽 初始值 描述(非规范性
[[td]] null ConsumedThing物 描述
[[activeSubscriptions]] {} 一个有序 映射,其为 表示事件字符串 名称,而 是 一个Subscription 对象。
[[activeObservations]] {} 一个有序 映射,其为 表示某个属性字符串 名称,而 是 一个Subscription 对象。

8.2 构造 ConsumedThing

在以 JSON 对象形式获取 一个物描述之后,可以创建一个 ConsumedThing 对象。

要使用 ThingDescription td 创建 ConsumedThing, 运行以下步骤:
  1. td 上运行 validate a TD 步骤。如果 失败,则抛出 SyntaxError 并停止。
  2. td 上运行 expand a TD 步骤。如果 失败,则重新抛出 该错误并停止。
  3. thing 为一个新的 ConsumedThing 对象。
  4. thing 内部槽 [[td]] 设置为 td
  5. 返回 thing

8.3 getThingDescription() 方法

返回 ConsumedThing 对象的 [[td]],该对象表示 ConsumedThing物描述。 应用可以查询存储在 [[td]] 中的元数据,以便在与其交互之前 内省其能力。

8.4 readProperty() 方法

读取一个属性值。接受 propertyName 以及可选的 options 作为实参。它返回一个 Promise,该 Promise 会以一个 InteractionOutput 对象形式表示的 属性值兑现, 或在错误时拒绝。该方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. interaction[[td]].properties.propertyName
  4. 如果 interactionundefined, 则用 NotFoundError 拒绝 promise 并停止。
  5. 如果 option.formIndex 已定义, 则令 forminteraction.forms 数组中与 formIndex 关联的表单;否则, 令 forminteraction.forms 中一个 opreadproperty表单,由 实现选择。
  6. 如果 form 是 failure,则用 SyntaxError 拒绝 promise 并停止。
  7. 向底层平台发出请求(通过 协议绑定), 使用 form 以及 options.uriVariables 中给定的可选 URI 模板, 检索 propertyName 属性的值。
  8. 如果请求失败,则用从协议 绑定收到的错误 拒绝 promise 并停止。
  9. response 为该请求收到的响应。
  10. data 为在 responseforminteraction 上运行 parse interaction response 的结果。如果这 失败,则用 SyntaxError 拒绝 promise 并停止。
  11. data 兑现 promise

8.5 readMultipleProperties() 方法

用一个请求读取多个属性值。接受 propertyNames 以及可选的 options 作为实参。它 返回一个 Promise,该 Promise 会以一个 PropertyReadMap 对象兑现,该对象将 propertyNames 中的键映射到 由此算法返回的值。该方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. 如果 option.formIndex 已定义, 则令 form[[td]].forms 数组中与 formIndex 关联的表单;否则, 令 form[[td]].forms 数组中 opreadmultipleproperties表单, 由实现选择。
  4. 如果 form 是 failure,则用 SyntaxError 拒绝 promise 并停止。
  5. result 为一个对象, 并且对于 propertyNames 中的每个字符串 name, 添加一个键为 name 且值为 null 的属性。
  6. 向底层平台发出请求(通过 协议绑定), 使用 form 以及 options uriVariables 中给定的可选 URI 模板,检索 propertyNames 给定的 属性值。
  7. 如果无法用 协议绑定通过单个请求完成此操作, 则用 NotSupportedError 拒绝 promise 并停止。
  8. 处理响应,并且对于 result 中的每个 key,运行以下 子步骤:
    1. valueresult[key]。
    2. schemathis.[[td]].properties[key]。
    3. property 为在 valueformschema 上运行 parse interaction response 的结果。
  9. 如果上述步骤在任何时刻抛出异常, 则用该 异常拒绝 promise 并停止。
  10. result 兑现 promise

8.6 readAllProperties() 方法

用一个请求读取的所有属性。接受 options 作为 可选实参。它返回一个 Promise,该 Promise 会以一个 PropertyReadMap 对象兑现,该对象将属性名称中的键映射到 由此算法返回的值。该方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. formssubscription.[[interaction]].forms
  4. 如果 formsundefined, 则用 SyntaxError 拒绝 promise 并停止。
  5. 如果 option.formIndex 不是 undefined 且小于 forms.length,则将 subscription.[[form]] 设置为 forms.[formIndex]。
  6. 否则,将 subscription.[[form]] 设置为 forms 中一个 op"readallproperties"表单, 由实现选择。
  7. 如果 subscription.[[form]] 是 failure,则用 SyntaxError 拒绝 promise 并停止。
  8. 使用 协议绑定向底层平台发出请求, 在给定 form 以及 options.uriVariables 中可选 URI 模板的情况下, 从 TD 检索所有属性定义。
  9. 如果无法使用该协议绑定通过单个请求完成此操作, 则用 NotSupportedError 拒绝 promise 并停止。
  10. 如果请求失败,则用从协议 绑定收到的错误 拒绝 promise 并停止。
  11. 处理回复,并令 result 为一个具有从回复中获得的键和值的对象。
  12. 处理响应,并且对于 result 中的每个 key,运行以下 子步骤:
    1. valueresult[key]。
    2. schemathis.[[td]].properties[key]。
    3. property 为在 valueformschema 上运行 parse interaction response 的结果。
  13. result 兑现 promise

8.7 writeProperty() 方法

写入单个属性。接受 propertyNamevalue 以及 可选的 options 作为实参。它返回一个 Promise,该 Promise 在成功时兑现, 并在失败时拒绝。该方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. interactionthis.[[td]].properties[propertyName]。
  4. 如果 interactionundefined, 则用 NotFoundError 拒绝 promise 并停止。
  5. 如果 option.formIndex 不是 undefined,则令 forminteraction.forms 数组中与 formIndex 关联的 表单; 否则,令 forminteraction.forms 中一个 opwriteproperty表单, 由实现选择。
  6. 如果 form 是 failure,则用 SyntaxError 拒绝 promise 并停止。
  7. data 为给定 valueform interaction 运行 create interaction request 步骤的结果。如果这抛出异常,则用该异常 拒绝 promise 并停止。
  8. 向底层平台发出请求(通过 协议绑定), 使用 data 以及 optionsuriVariables 中给定的可选 URI 模板, 写入由 propertyName 给定的 属性
  9. 如果请求失败,则用从协议 绑定收到的错误 拒绝 promise 并停止。
  10. 否则兑现 promise
编辑注

Issue #193 中所讨论,设计决定是写入交互 只返回成功或错误,而不返回写入的值 (可选)。TD 应 捕获属性值的模式,包括 精度和替代格式。当交互预期有返回值时, 应使用动作而不是 属性

8.8 writeMultipleProperties() 方法

用一个请求写入多个属性值。接受 properties 作为实参——它是一个对象,其中键为 属性名称,值为 属性值——并 可选接受 options。它返回一个 Promise,该 Promise 在成功时兑现, 并在失败时拒绝。该方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. 如果 option.formIndex 已定义, 则令 form[[td]].forms 数组中与 formIndex 关联的表单;否则, 令 form[[td]].forms 数组中 opwritemultipleproperties表单, 由实现选择。
  4. 如果 form 是 failure,则用 SyntaxError 拒绝 promise 并停止。
  5. propertyNames 为一个 string 数组,其元素为 properties 对象的键。
  6. 对于 propertyNames 中的每个 name,令 propertythis.[[td]].properties[name]。
  7. 如果 propertynull undefined,或者不是 writeable, 则用 NotSupportedError 拒绝 promise 并停止。
  8. result 为一个对象, 并且对于 propertyNames 中的每个字符串 name,添加一个键为 name 的属性,并令其值为 null
  9. schemas 为一个对象, 并且对于 propertyNames 中的每个 name,添加一个键为 name 的属性,并令其值为 this.[[td]].properties[name]。
  10. 对于 properties 中的每个键 key,给定 properties[key]、formschema[key] 的值,运行 create interaction request 步骤。 如果这对任何 name 抛出异常,则用该异常 拒绝 promise 并停止。
  11. 向底层平台发出单个请求(通过 协议 绑定), 使用 optionsuriVariables 中给定的可选 URI 模板,写入 properties 中提供的每个 属性
  12. 如果无法使用该协议绑定 通过单个请求完成此操作, 则用 NotSupportedError 拒绝 promise 并停止。
  13. 如果请求失败,则返回从 协议 绑定收到的错误并停止。
  14. 否则兑现 promise

8.9 observeProperty() 方法

发出对属性值变更 通知的请求。接受 propertyNamelistener 以及可选的 onerroroptions 作为实参。它 返回一个 Promise,该 Promise 在成功时兑现, 并在失败时拒绝。
编辑注

此算法每个 属性只允许一个活动的 Subscription。 如果在已有活动 Subscription 时创建新的 Subscription, 运行时将抛出 NotAllowedError

该方法 MUST 运行 以下步骤:
  1. thing 为此 ConsumedThing 对象的引用。
  2. 返回一个 Promise promise,并 并行执行 后续步骤。
  3. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  4. 如果 listener 不是 Function, 则用 TypeError 拒绝 promise 并停止。
  5. 如果 onerror 不是 null,并且不是 Function, 则用 TypeError 拒绝 promise 并停止。
  6. 如果 thing.[[activeObservations]][propertyName] [=map/exists],则用 NotAllowedError 拒绝 promise 并停止。
  7. subscription 为一个新的 Subscription 对象,其 内部槽设置如下:
  8. 向底层平台发出请求,使用 form 以及 options uriVariables 中给定的可选 URI 模板,观察由 propertyName 标识的 属性
  9. 如果请求失败,则用从协议 绑定收到的错误 拒绝 promise 并停止。
  10. thing.[[activeObservations]][|propertyName] 设置subscription,并兑现 promise
  11. 每当底层平台检测到此 subscription 的通知,并且该通知以 propertyName, 带有新的属性value 时,运行以下子步骤:
  12. 每当底层平台检测到此订阅的错误时, 运行以下子步骤:
    • 如果该错误不可恢复并停止了 订阅,则将 subscription.active 设置为 false,并抑制后续 通知。
    • error 为一个新的 NetworkError,并将其 message 设置为反映底层错误 条件的内容。
    • 如果 onerror 是一个 Function, 则用 error 调用它。

8.10 invokeAction() 方法

发出调用一个动作并返回结果的请求。 接受 actionName、可选的 params 以及可选的 options 作为实参。它 返回一个 Promise,该 Promise 会以 动作的结果兑现, 该结果表示为一个 InteractionOutput 对象;或以错误拒绝。该方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. interactionthis.[[td]].actions[actionName]。
  4. 如果 interaction 不是一个 object, 则用 NotFoundError 拒绝 promise 并停止。
  5. formssubscription.[[interaction]].forms
  6. 如果 formsundefined, 则用 SyntaxError 拒绝 promise 并停止。
  7. 如果 option.formIndex 不是 undefined 且小于 forms.length,则将 subscription.[[form]] 设置为 forms.[formIndex]。
  8. 否则,将 subscription.[[form]] 设置为 forms 中一个 op"invokeaction"表单, 由实现选择。
  9. 如果 subscription.[[form]] 是 failure,则用 SyntaxError 拒绝 promise 并停止。
  10. args 为在 paramsforminteraction 上运行 create interaction request 步骤的结果。 如果这抛出异常,则用该异常 拒绝 promise 并停止。
  11. 向底层平台发出请求(通过 协议绑定), 在给定 argsoptions.uriVariables 的情况下, 调用由 actionName 标识的 动作
  12. 如果请求在本地失败或通过网络返回错误,则用从 协议 绑定收到的错误 拒绝 promise 并停止。
  13. value 为回复中返回的回复。
  14. result 为使用 valueforminteraction 运行 parse interaction response 的结果。如果这 抛出异常,则用该异常 拒绝 promise 并停止。
  15. result 兑现 promise

8.11 subscribeEvent() 方法

发出订阅事件通知的请求。接受 eventNamelistener 以及可选的 onerroroptions 作为实参。它 返回一个 Promise 来表示成功或 失败。
编辑注

此算法每个 事件只允许一个活动的 Subscription。 如果在已有活动 Subscription 时创建新的 Subscription, 运行时将抛出 NotAllowedError

该方法 MUST 运行 以下步骤:
  1. thing 为此 ConsumedThing 对象的引用。
  2. 返回一个 Promise promise,并 并行执行 后续步骤。
  3. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  4. 如果 listener 不是 一个 Function, 则用 TypeError 拒绝 promise 并停止。
  5. 如果 onerror 不是 null,并且不是 Function, 则用 TypeError 拒绝 promise 并停止。
  6. 如果 thing.[[activeSubscriptions]][eventName] 不存在, 则用 NotAllowedError 拒绝 promise 并停止。
  7. subscription 为一个新的 Subscription 对象,其 内部槽设置如下:
  8. 通过 协议绑定 向底层平台发出请求,以使用 [[form]]options.uriVariables 中给定的可选 URI 模板,以及 options.data 中给定的可选订阅数据, 订阅由 eventName 标识的 事件
  9. 如果请求失败,则用从协议 绑定收到的错误 拒绝 promise 并停止。
  10. eventName 设置thing.[[activeSubscriptions]][eventName] 为 subscription
  11. 兑现 promise
  12. 每当底层平台检测到 以 eventName事件 subscription 通知时,运行以下子步骤:
    1. 使用在该 事件随附的数据、 subscription.[[form]]subscription.[[interaction]] 运行 parse interaction response 的结果, 调用 listener
  13. 每当底层平台检测到以 eventName事件 subscription 的错误时,运行 以下子步骤:
    • 如果该错误不可恢复并停止了 订阅,则将 subscription.active 设置为 false,并抑制后续 通知。
    • error 为一个新的 NetworkError,并将其 message 设置为反映底层错误 条件的内容。
    • 如果 onerror 是一个 Function, 则用 error 调用它。

8.12 InteractionOptions 字典

保存根据物 描述需要向 应用脚本公开的交互选项。

formIndex 属性如果已定义, 表示一个应用提示,指出对于给定的 WoT 交互,应使用 TD 中由该索引标识的哪个 Form 定义。实现 SHOULD 使用具有此索引的 Form 来进行 交互,但如果未找到该索引或索引无效, MAY 覆盖此 值。如果未定义, 实现 SHOULD 尝试 按照 TD 中列出的出现顺序,使用 给定 Wot 交互的 Form 定义。

uriVariables 属性如果已定义, 表示要与 WoT 交互一起使用的 URI 模板变量,这些变量表示为 [WOT-TD] 中定义的 解析后的 JSON 对象

编辑注

对 URI 变量的支持源于 Web of Things(WoT)物描述 1.1 规范所揭示的需求,即能够描述使用它们的现有 RESTful 端点。然而,应该可以 编写一个物描述,使用动作来表示这类 交互,并将 URI 变量建模为动作 参数。在这种情况下,实现可以将 参数序列化为 URI 变量,因此 options 参数可以被省略。

data 属性如果已定义,表示需要传递给交互的 额外不透明数据。

8.13 PropertyReadMap 类型

表示从属性名称到 InteractionOutput 对象的映射,该对象表示属性可以取的值。它用作涉及多个 属性同时进行的交互的 属性包。

8.14 PropertyWriteMap 类型

表示从属性名称到 InteractionInput 的映射,后者表示属性可以取的值。它用作涉及多个 属性同时进行的交互的 属性包。

8.15 InteractionListener 回调

用户提供的回调,会收到一个类型为 InteractionOutput 的实参,用于观察属性变化以及处理 事件 通知。 由于订阅事件是 WoT 交互,并且 可能接受选项甚至数据,因此它们不使用 软件事件建模。

8.16 ErrorListener 回调

用户提供的回调,会收到一个类型为 Error 的实参,用于将来自 协议绑定的 关键和非关键错误传递给 应用。

8.17 Subscription 接口

表示对属性变更和事件交互的订阅。

active 布尔属性表示该订阅是否处于活动状态,即 它未因错误或因调用 stop() 方法而 停止。

8.17.1 Subscription 的内部槽

Subscription 对象具有以下 内部槽
内部槽 初始值 描述(非规范性
[[type]] null 指示该 Subscription 指向哪个WoT 交互。该值可以是 "property""event"null
[[name]] null 属性事件名称。
[[interaction]] null 描述该 WoT 交互物 描述片段。
[[form]] null 与订阅关联的表单
[[thing]] null 与订阅关联的 ConsumedThing

8.17.2 stop() 方法

停止传递该订阅的通知。它 接受一个可选参数 options,并返回一个 Promise。调用时,该方法 MUST 执行以下 步骤:

  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. 如果 optionsformIndex 已定义,则令 unsubscribeForm[[interaction]]forms 数组中与 formIndex 关联的表单
  4. 否则,令 unsubscribeForm 为给定 [[form]] 运行 find a matching unsubscribe form 算法的 结果。
  5. 如果 unsubscribeForm 是 failure, 则用 SyntaxError 拒绝 promise 并停止。
  6. 如果 [[type]] "property",则通过 协议 绑定向底层 平台发出请求,使用 unsubscribeForm 以及 optionsuriVariables 中给定的可选 URI 模板,停止观察由 [[name]] 标识的 属性
  7. 否则,如果 [[type]]"event",则通过 协议 绑定向底层 平台发出请求,使用 unsubscribeFormoptionsuriVariables 中给定的可选 URI 模板,以及 options.data 中给定的可选取消订阅数据, 取消订阅由 [[name]] 标识的 事件
  8. 如果请求失败,则用从协议 绑定收到的错误 拒绝 promise 并停止。
  9. 否则:
  10. 如果底层平台收到该订阅的后续 通知,实现 SHOULD 静默抑制 它们。

8.17.3 查找 unsubscribe 表单

此算法正在开发中,并且是 非规范性内容。实现 MAY 选择另一种算法,为给定的 subscribe 表单查找匹配的 unsubscribe 表单

要在 Subscription 对象的上下文中,给定 subscribeForm查找匹配的 unsubscribe 表单,运行以下步骤:
  1. results 为一个空数组。
  2. 对于 [[interaction]].forms 中的每个 form
    1. form 上添加一个 内部槽 [[matchLevel]], 并将其值设置为 0
    2. 如果 [[type]]"property"form.op"unobserveproperty",或者如果 [[type]]"event"form.op"unsubscribeevent"
      1. form 上的 内部槽 [[matchLevel]] 设置为 1,并将 form 添加到 results
      2. 如果 form.href 和 [[subscribeForm]].href 同源域,则递增 form.[[matchLevel]]。
      3. 如果 form.contentType 等于 [[subscribeForm]] 的 contentType,并且 form.[[matchLevel]] 大于 2, 则递增 form.[[matchLevel]]。
  3. 如果 results 为空,则返回 null 并终止这些步骤。
  4. 返回 results 中第一个具有最高 [[matchLevel]] 值的 form

8.18 ConsumedThing 示例

下一个示例展示如何通过 URL 获取 TD,创建 ConsumedThing, 读取元数据(title)、读取属性值、订阅 属性变更、订阅 WoT 事件以及取消订阅。

示例 2:带数据值的物 客户端 API 示例
try {
  let res = await fetch("https://tds.mythings.org/sensor11");
  let td = res.json();
  let thing = new ConsumedThing(td);
  console.log("物 " + thing.getThingDescription().title + " 已被使用。");
} catch (e) {
  console.log("TD 获取错误:" + e.message);
};

try {
  // 订阅 “temperature” 的属性变更
  await thing.observeProperty("temperature", async (data) => {
    try {
      console.log("温度已变为:" + await data.value());
    } catch (error) {
      console.error("无法读取已观察的 temperature 属性");
      console.error(error);
    }
  });
  // 订阅 TD 中定义的 “ready” 事件
  await thing.subscribeEvent("ready", async (eventData) => {
    try {
      console.log("已就绪;索引:" + await eventData.value());
      // 运行 TD 定义的 “startMeasurement” 动作
      await thing.invokeAction("startMeasurement", { units: "Celsius" });
      console.log("测量已开始。");
    } catch (error) {
      console.error("无法读取 ready 事件,或 startMeasurement 失败");
      console.error(error)
    }
  });
} catch (e) {
  console.log("启动测量时出错。");
}

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

    await thing.unsubscribe("ready");
    console.log("已取消订阅 ‘ready’ 事件。");
  } catch (error) {
    console.log("清理函数中出错");
  }
}, 10000);

下面展示了 InteractionOutput 的高级用法,用于读取没有 DataSchema 的属性。

示例 3:带 arrayBuffer 的物 客户端 API 示例
/*
* takePicture affordance form:
* "form": {
*   "op": "invokeaction",
*   "href" : "http://camera.example.com:5683/takePicture",
*   "response": {
*     "contentType": "image/jpeg",
*     "contentCoding": "gzip"
*   }
*}
* 参见 https://www.w3.org/TR/wot-thing-description/#example-23
*/
let response;
let image;
try {
  response = await thing.invokeAction(“takePicture”));
  image = await response.value() // 抛出 NotReadableError --> 未定义 schema
} catch(ex) {
  image = await response.arrayBuffer();
  // image: ArrayBuffer [0x1 0x2 0x3 0x5 0x15 0x23 ...]
}

最后,接下来的两个示例展示了如何使用来自 InteractionOutputReadableStream

示例 4: 带可读流的物客户端 API 示例(例如 视频流)
/*{
"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("实时视频已停止");
    return;
  }
  const decoded = decode(value)
  UI.show(decoded)
  // 再读取一些内容,并再次调用此函数
  return reader.read().then(processText);
});

这里假设 JSON 对象太大,无法 整体读入内存。因此,我们使用流式处理 来获取远程 Web Thing 记录的事件 总数。

示例 5: 带可读流的物客户端 API 示例(例如 统计 json 对象)
/*
* "eventHistory":
* {
*   "description" : "A long list of the events recorded by this thing",
*   "type": "array",
*   "forms": [
*     {
*       "op": "readproperty",
*       "href": "http://recorder.example.com/eventHistory",
*     }
*   ]
* }
*/

// 流式处理示例:统计 json 对象
let objectCounter = 0
const parser = new Parser() // 用于 json 流式解析的用户库(即 https://github.com/uhop/stream-json/wiki/Parser)

parser.on('data', data => data.name === 'startObject' && ++objectCounter);
parser.on('end', () => console.log(`发现 ${objectCounter} 个对象。`));

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

// 发现 N 个对象

9. ExposedThing 接口

ExposedThing 接口是用于操作的服务器 API,它允许定义请求 处理器、属性动作事件交互。

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

  ExposedThing setActionHandler(DOMString name, ActionHandler action);

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

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

  ThingDescription getThingDescription();
};

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

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

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

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

9.1 ExposedThing 的内部槽

ExposedThing 对象具有以下 内部槽

内部槽 初始值 描述(非规范性
[[td]] null ExposedThing物 描述
[[readHandlers]] {} 一个 Map,其键为属性名称, 值为 PropertyReadHandler
[[writeHandlers]] {} 一个 Map,其键为属性名称, 值为 PropertyWriteHandler
[[observeHandlers]] {} 一个 Map,其键为属性名称, 值为 PropertyReadHandler
[[unobserveHandlers]] {} 一个 Map,其键为属性名称, 值为 Function
[[actionHandlers]] {} 一个 Map,其键为动作名称, 值为 ActionHandler
[[subscribeHandlers]] {} 一个 Map,其键为事件名称, 值为 EventSubscriptionHandler
[[unsubscribeHandlers]] {} 一个 Map,其键为事件名称, 值为 EventSubscriptionHandler
[[propertyObservers]] {} 一个 Map,其键为属性名称, 值为监听器的 Array
[[eventListeners]] {} 一个 Map,其键为事件名称, 值为监听器的 Array

9.2 构造 ExposedThing

ExposedThing 接口扩展了 ConsumedThing。 它从一个完整或部分的 ThingDescription 对象构造。

注意,现有的 ThingDescription 对象可以被可选地修改(例如通过在其 propertiesactionsevents 内部 属性上添加或移除元素),并且所得对象可以用于 构造一个 ExposedThing 对象。这是当前添加和移除 属性动作事件定义的方式,如 示例中所示。

在调用 expose() 之前, ExposedThing 对象不会服务任何请求。这允许先 构造 ExposedThing, 然后在开始服务请求之前初始化其 属性和服务 处理器。

要使用 ExposedThingInit init 构造一个 ExposedThing, 运行以下步骤:
  1. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则抛出 一个 SecurityError 并停止。
  2. init 上运行 expand an ExposedThingInit 步骤。如果失败,则重新抛出该 错误并停止。否则,存储获得的 td
  3. td 上运行 expand a TD 步骤。如果这 失败,则重新抛出 该错误并停止。
  4. thing 为一个新的 ExposedThing 对象。
  5. thing[[td]] 设置为 td
  6. 返回 thing

9.3 getThingDescription() 方法

返回 ExposedThing 对象的 [[td]],该对象表示 物描述。 应用可以 查询存储在 [[td]] 中的 元数据,以便在与其交互之前 内省其能力。

9.4 PropertyReadHandler 回调

当收到读取某个属性的外部请求时被调用的函数, 并定义如何处理此类请求。它返回一个 Promise,并以一个 ReadableStream 对象,或一个符合 DataSchema ECMAScript 值兑现,或者以 错误拒绝。

9.5 setPropertyReadHandler() 方法

接受 namehandler 作为实参。设置 服务处理器,该处理器定义当收到读取与 name 匹配的指定属性的请求时 应该做什么。错误时抛出异常。返回 对 this 对象的引用以支持 链式调用。

编辑注

注意,不需要注册用于处理读取多个或全部属性请求的处理器。 请求和回复在单个网络请求中传输,但 ExposedThing 可以通过多次调用单个读取 处理器来实现它们。

handler 回调函数应该实现读取一个属性,并且当从 底层平台收到读取一个属性的请求时, 实现 SHOULD 调用它。

对于任何给定的属性,最多 MUST 有一个处理器, 因此新添加的 处理器 MUST 替换先前的 处理器。如果没有为任何给定的属性初始化处理器,实现 SHOULD 基于 [[td]] 内部槽中提供的物描述 实现默认属性读取处理器。

当给定 namehandler 调用该方法时,实现 MUST 运行以下步骤:
  1. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则抛出 一个 SecurityError 并停止。
  2. 如果 [[td]].properties[name] 不存在, 则抛出 NotFoundError 并停止。
  3. this.[[readHandlers]][name] 设置为 handler

9.6 处理读取 属性的请求

当实现收到读取属性 name 的网络请求,并带有 options 时,运行 以下步骤:
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. value 为使用 nameoptions 运行以下 read server property 步骤的结果:
    1. interaction[[td]].properties.name
    2. 如果名称为 name属性不存在, 则抛出 NotFoundError 并停止。
    3. handlernull
    4. 如果在 [[readHandlers]] 内部槽中存在用于 interaction 的用户提供的 PropertyReadHandler, 则令 handler 为它。
    5. 否则,如果实现提供了默认读取处理器, 则令 handler 为它。
    6. 如果 handlernull,则抛出 NotSupportedError 并停止。
    7. value 为调用 handler 并给定 options 的结果。如果 这失败,则抛出该错误并停止。
    8. 返回 value

      这里返回的 value SHOULD 要么符合 DataSchema,要么 SHOULD 是一个由 handler 创建的 ReadableStream 对象。

  4. 如果上一步抛出了错误,则按照 协议绑定 创建回复,将该错误发回,并停止。
  5. 序列化并将返回的 value 添加到 按照 协议绑定创建的回复中。

9.7 处理读取 多个属性的请求

当收到读取在对象 propertyNames 中给定的多个属性的网络请求,并带有 options 时,对 propertyNamesoptions 运行以下 read multiple properties 步骤:
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. 对于在 propertyNames 中定义的每个键为 name 的属性,
    1. value 为在 nameoptions 上运行 read server property 步骤的结果。如果 这抛出异常,则按照协议 绑定创建的回复中发回该错误 并停止。
    2. propertyNames.name 设置为 value
  4. 通过发送一个按照 协议绑定propertyNames 创建的单个回复来回复请求。

9.8 处理读取 所有属性的请求

当收到读取所有属性的网络请求, 并带有 options 时,运行 以下步骤:
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. properties 为一个对象,该对象使用 中定义的 所有属性创建,其值设置为 null
  4. propertiesoptions 上运行 read multiple properties 步骤。

9.9 setPropertyObserveHandler() 方法

接受 namehandler 作为实参。设置 服务处理器,该处理器定义当收到观察与 name 匹配的指定属性的请求时 应该做什么。错误时抛出异常。返回 对 this 对象的引用以支持 链式调用。

handler 回调函数应该实现读取一个属性,并 以一个 InteractionOutput 对象兑现,或以 错误拒绝

对于任何给定的属性,最多 MUST 有一个处理器, 因此新添加的 处理器 MUST 替换先前的 处理器。如果没有为任何给定的属性初始化处理器,实现 SHOULD 基于物 描述实现默认属性 读取处理器。

当给定 namehandler 调用该方法时,实现 MUST 运行以下步骤:
  1. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则抛出 一个 SecurityError 并停止。
  2. 如果 this.[[td]].properties[name] 不存在, 则抛出 NotFoundError 并停止。
  3. this[[observeHandlers]][name] 设置为 handler

9.10 处理属性观察 请求

当实现收到观察一个属性 name 的网络请求,并带有 options 时,运行 以下步骤:
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. 如果 this.[[td]].properties[name] 不存在, 则在回复中发回一个 NotFoundError 并停止。
  4. 在内部保存请求发送者信息,以及 optionsthis.[[propertyObservers]][name], 以便能够通知属性值 变更。

每当 property 的值 发生变化时,应用 脚本都需要显式调用 emitPropertyChange()

9.11 setPropertyUnobserveHandler() 方法

接受 namehandler 作为实参。设置 服务处理器,该处理器定义当收到取消观察与 name 匹配的指定属性的请求时 应该做什么。错误时抛出异常。返回 对 this 对象的引用以支持 链式调用。

handler 回调函数应该实现当实现收到 取消观察请求时应做什么。

对于任何给定的属性,最多 MUST 有一个处理器, 因此新添加的 处理器 MUST 替换先前的 处理器。如果没有为任何给定的属性初始化处理器,实现 SHOULD 基于物 描述实现默认处理器。

当给定 namehandler 调用该方法时,实现 MUST 运行以下步骤:
  1. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则抛出 一个 SecurityError 并停止。
  2. 如果 this.[[td]].properties[name] 不存在, 则抛出 NotFoundError 并停止。
  3. this.[[unobserveHandlers]][name] 设置为 handler

9.12 处理属性取消观察 请求

当实现收到对属性 name 取消观察的网络请求,并带有 options 时, 运行以下步骤:
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. 如果 this.[[td]].properties[name] 不存在, 则在回复中发回一个 NotFoundError 并停止。
  4. handlerthis.[[unobserveHandlers]][name];
  5. 如果 handler 是一个 Function, 则使用 options 调用它,然后发回一个 遵循协议绑定的 回复并停止。
  6. 否则,如果 this.[[propertyObservers]][name] 存在, 则将其从 this.[[propertyObservers]] 中移除, 按照协议 绑定中定义的方式发回一个回复并停止。
  7. 否则,按照协议绑定 中定义的方式,在回复中发回一个 NotFoundError 并停止。

9.13 emitPropertyChange() 方法

接受 name 实参, 表示一个属性名称,以及 可选的 data 实参。触发向指定属性的所有观察者发出通知,其中数据 由 data 实参提供;如果未提供, 则由与该属性关联的观察处理器或 读取处理器获得。该方法 MUST 运行以下步骤:
  1. promise 为一个新的 Promise
  2. 返回 promise 并行执行后续步骤。
  3. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  4. name 为第一个 实参。
  5. propertythis.[[td]].properties[name]。
  6. 如果 propertyundefined, 则用 NotFoundError 拒绝 promise 并停止。
  7. data 为 第二个实参。
  8. 如果 dataundefined,则运行以下子步骤:
    1. handlernull.
    2. 如果 name [[readHandlers]] 中不 存在, 则拒绝 promise 并停止。
    3. handler [[readHandlers]][name]。
    4. 如果 handlernullundefined, 则拒绝 promise 并停止。
    5. handled 为调用 handler 并给定 null 的结果。
    6. 如果 handled拒绝, 则 拒绝 promise 并停止。
    7. 否则,如果 handledvalue 兑现, 则令 datavalue
  9. 对于 [[propertyObservers]][name] 中的每个 observer, 运行以下子步骤:
    1. options 为与 observer 一起保存的交互选项。
    2. 请求底层平台按照协议 绑定,从 dataoptions 创建一个 reply
      编辑注

      此条款需要扩展,和/或 引用 [WOT-PROTOCOL-BINDINGS] 中的算法。

    3. reply 发送给 observer
  10. 兑现 promise

9.14 PropertyWriteHandler 回调

当收到写入某个属性的外部请求时被调用的函数, 并定义如何处理此类请求。接受 value 作为实参并返回一个 Promise,当 由设置处理器时提供的名称标识的 属性值已更新时兑现;或者 如果未找到该属性或无法更新该 值,则以错误拒绝。

编辑注

注意,如果需要,此回调函数中的代码 可以在更新属性之前读取该属性,以便 找出旧值。因此旧值 不会提供给此函数。

该值由实现以 InteractionOutput 对象形式提供,以便能够表示 未由 DataSchema 描述的值,例如 流。

9.15 setPropertyWriteHandler() 方法

接受 namehandler 作为实参。设置 服务处理器,该处理器定义当收到写入由设置 处理器时给定的 name 匹配的 属性的请求时 应该做什么。错误时抛出异常。返回 对 this 对象的引用以支持链式调用。

注意,即使对于 readonly 属性,也可以 指定写入处理器,如 Issue 199 中所解释。在这种情况下,写入处理器可以以 应用特定的方式定义使请求失败。

对于任何给定的属性,最多 MUST 有一个写入 处理器,因此新添加的 处理器 MUST 替换先前的 处理器。如果没有为任何给定 属性初始化写入处理器, 实现 SHOULD 在该 属性可写时实现 默认属性更新,并在该属性可观察时通知观察者 变化,这些均基于物 描述

当给定 namehandler 调用该方法时,实现 MUST 运行以下步骤:
  1. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则抛出 一个 SecurityError 并停止。
  2. 如果 this.[[td]].properties[name] 不存在, 则抛出 NotFoundError 并停止。
  3. this.[[writeHandlers]][name] 设置为 handler

9.16 处理写入 属性的请求

当收到对属性 name 写入新值 value 的网络请求,并带有 options 时, 实现 MUST 运行以下 update property steps,给定 namevalueoptions,并将 mode 设置为 "single"
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. interactionthis.[[td]].properties[name]。
  4. 如果 interactionundefined, 则在回复中返回一个 NotFoundError 并停止。
  5. handler this.[[writeHandlers]][name]。
  6. 如果 handlerundefined,并且存在由实现提供的默认写入 处理器,则令 handler 为它。
  7. 如果 handler undefined,则随回复发回一个 NotSupportedError 并停止。
  8. promise 为调用 handler 并给定 nameoptions 的结果。如果它 失败,则在回复中返回 该错误并停止。
  9. 如果 mode"single",则按照协议 绑定 回复请求并报告成功, 然后停止。

9.17 处理写入 多个属性的请求

当收到写入在对象 propertyNames 中给定的多个属性的网络请求,并带有 options 时,运行 以下步骤:
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. 对于 propertyNames 中定义的每个键为 name、值为 value 的属性,使用 namevalueoptions 并将 mode 设置为 "multiple",运行 update property steps。如果这 失败,则用该错误回复请求并停止。
  4. 通过按照协议 绑定发送单个回复来回复请求。

9.18 ActionHandler 回调

当收到调用某个动作 的外部请求时被调用的函数,并定义如何处理此类请求。它会在给定 params 并可选给定一个 options 对象时被调用。它返回一个 Promise,该 Promise 以错误拒绝或 以动作返回的值兑现,该值作为 InteractionInput

应用脚本 MAYActionHandler 返回一个 ReadableStream 对象。实现随后将使用该流来构造 动作的响应。

9.19 setActionHandler() 方法

接受 nameaction 作为实参。设置处理器 函数,该函数定义当收到请求以调用与 name 匹配的动作 时应做什么。错误时抛出 异常。返回对 this 对象的引用以 支持链式调用。

action 回调 函数将实现一个动作,并且当从 底层平台收到调用该动作的 请求时,实现 SHOULD 调用它。

对于任何给定的动作, 最多 MUST 有一个处理器, 因此新添加的处理器 MUST 替换 先前的处理器。

当给定 nameaction 调用该方法时,运行以下步骤:
  1. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则抛出 一个 SecurityError 并停止。
  2. interactionthis.[[td]].actions[name]。
  3. 如果 interactionundefined, 则抛出 一个 NotFoundError 并停止。
  4. this.[[actionHandlers]][name] 设置为 action

9.20 处理动作请求

当收到调用由 name 标识的动作的网络请求,并给定 inputs 以及可选的 options 时,运行以下 步骤:
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. interactionthis.[[td]].properties[name]。
  4. 如果 interactionundefined, 则在回复中返回一个 NotFoundError 并停止。
  5. handler this.[[actionHandlers]][name]。
  6. 如果 handler undefined,则返回一个 NotSupportedError,并带有按照 协议 绑定创建的回复,然后停止。
  7. promise 为调用 handler 并给定 nameinputsoptions 的结果。
  8. 如果 promise 拒绝,则随 回复发送该错误并停止。
  9. promisedata 兑现时,使用 data 按照 协议 绑定创建并发送回复。

9.21 EventSubscriptionHandler 回调

当收到订阅某个事件的外部请求时被调用的函数, 并定义如何处理此类请求。它在给定由 实现提供并来自订阅者的 options 对象时被调用。它返回一个 Promise,该 Promise 以错误拒绝或 在订阅被接受时兑现。

9.22 setEventSubscribeHandler() 方法

接受 namehandler 作为实参。设置 处理器函数,该函数定义当收到针对由 name 匹配的指定事件的 订阅请求时应做什么。错误时抛出异常。返回对 this 对象的引用以支持链式调用。

handler 回调函数 SHOULD 实现当收到 订阅请求时应做什么,例如必要的 初始化。注意,用于发出事件的处理器是单独设置的。

对于任何给定的事件,最多 MUST 有一个事件 订阅处理器,因此新添加的处理器 MUST 替换先前的 处理器。

当给定 namehandler 调用该方法时,运行以下 步骤:
  1. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则抛出 一个 SecurityError 并停止。
  2. interactionthis.[[td]].events[name]。
  3. 如果 interactionundefined, 则抛出 一个 NotFoundError 并停止。
  4. this.[[subscribeHandlers]][name] 设置为 handler
  5. 返回 this

9.23 处理事件订阅 请求

当底层平台收到针对 name事件 订阅请求,并带有可选的 options 时,运行以下 步骤:
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. interactionthis.[[td]].events[name]。
  4. 如果 interactionundefined, 则发回一个 NotFoundError 并停止。
  5. 如果 this.[[subscribeHandlers]][name] 是一个 Function, 则用 options 调用它并停止。
  6. 否则,实现默认订阅者机制:
    1. subscriber 为一个元组,它由 options (可从其中使用 uriVariablesdata)以及创建事件通知 响应所需的订阅者信息组成。
    2. this.[[eventListeners]][name] 设置为 subscriber

9.24 setEventUnsubscribeHandler() 方法

接受 namehandler 作为实参。设置 处理器函数,该函数定义当由 name 匹配的指定事件 被取消订阅时应做什么。错误时抛出异常。返回对 this 对象的引用以支持链式调用。

handler 回调函数 SHOULD 实现当收到 取消订阅请求时应做什么。

对于任何给定的事件,最多 MUST 有一个处理器, 因此新添加的处理器 MUST 替换 先前的处理器。

当给定 namehandler 调用该方法时,运行以下 步骤:
  1. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则抛出 一个 SecurityError 并停止。
  2. interactionthis.[[td]].events[name]。
  3. 如果 interactionundefined, 则抛出 一个 NotFoundError 并停止。
  4. this.[[unsubscribeHandlers]][name] 设置为 handler
  5. 返回 this

9.25 处理事件取消订阅 请求

当底层平台收到针对 name事件 取消订阅请求,并可选带有 options 时,运行以下 步骤:
  1. 如果此操作不受支持,则按照协议 绑定发回一个 NotSupportedError 并停止。
  2. 如果此操作不被允许,则按照协议 绑定发回一个 NotAllowedError 并停止。
  3. interactionthis.[[td]].events[name]。
  4. 如果 interactionundefined, 则发回一个 NotFoundError 并停止。
  5. 如果 this.[[unsubscribeHandlers]][name] 存在 并且是一个 Function, 则用 options 调用它并停止。
  6. 否则,如果 namethis.[[eventListeners]] 中 [=map/exists], 则移除 name
  7. 返回 this

9.26 处理事件

emitEvent() 方法发出一个名称为 name、带有 data事件时,运行 以下步骤:
  1. listeners[[eventListeners]].name
  2. 对于 listeners 中的每个 subscriber,运行以下子步骤:
    1. 按照协议 绑定,从 datasubscriber 创建一个事件通知 response,包括其 options
    2. 如果 dataundefined,则假定该 通知 response 将包含一个 空数据载荷,如协议 绑定所规定。
    3. 如果底层协议栈允许 传达事件错误,并且 UA 检测到错误条件, 则按照协议 绑定,使用 datasubscriber 及其 options,将 response 创建为错误通知。

      错误报告是协议 特定的,并由 实现封装。在客户端端,如果客户端 UA 检测到该错误, 则会调用随订阅传入的错误 监听器。

    4. response 发送给由 subscriber 标识的订阅者。

9.27 emitEvent() 方法

接受 name 作为实参, 表示一个事件 名称,并可选接受 data。触发发出带有 可选数据的 事件。该方法 MUST 运行 以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. interaction[[td]].events.name
  4. 如果找不到名称为 name事件, 则用 NotFoundError 拒绝 promise 并停止。
  5. 向底层平台发出请求,以发出一个带有可选 data事件。调用 handling events 步骤。

9.28 expose() 方法

开始为该服务外部请求,使得使用属性动作事件WoT 交互成为可能。该 方法 MUST 运行以下 步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. [[td]] 上运行 expand a TD 步骤。
  4. [[td]] 上运行 validate a TD。如果失败, 则用 TypeError 拒绝 promise 并停止。
  5. 对于 [[td]].properties 中的每个 key, 将 this.[[propertyObservers]].key 初始化为一个空 Array,以存储在值 变化时通知观察者所需的观察 请求数据。
  6. 对于 this.[[td]].events 中的每个 key, 将 this.[[eventListeners]].key 初始化为一个空 Array,以存储在事件 发出时通知订阅者所需的订阅 请求数据。
  7. 基于内省 [[td]] 来设置 WoT 交互, 如 [WOT-TD] 和 [WOT-PROTOCOL-BINDINGS] 中所解释。 向底层平台发出请求以初始化 协议 绑定,然后基于 协议 绑定,开始服务针对 WoT 交互 的外部请求(读取、写入和观察属性,调用 动作并管理 事件订阅)。实现 MAY 因任何原因拒绝此步骤 (例如,如果它们想对交互形式强制执行进一步检查和 约束)。
  8. 如果请求期间发生错误, 则用一个 Error 对象 error 拒绝 promise,其中 error.message 设置为 协议 绑定看到的错误 代码,然后停止。
  9. 否则兑现 promise 并停止。

9.29 destroy() 方法

停止为该服务外部请求并销毁对象。 注意,最终的注销应在 调用此方法之前完成。该方法 MUST 运行以下步骤:
  1. 返回一个 Promise promise,并 并行执行 后续步骤。
  2. 如果出于安全原因,当前脚本上下文 不允许调用此方法,则用 SecurityError 拒绝 promise 并停止。
  3. 向底层平台发出请求,以基于 协议 绑定, 停止服务针对WoT 交互的外部请求。
  4. 如果请求期间发生错误, 则用一个 Error 对象 error 拒绝 promise,其 message 设置为 协议 绑定看到的错误代码,然后停止。
  5. 否则兑现 promise 并停止。

9.30 ExposedThing 示例

下一个示例展示如何基于预先构造的部分 TD 对象创建一个 ExposedThing

示例 6:使用简单属性创建 ExposedThing
try {
  let temperaturePropertyDefinition = {
    type: "number",
    minimum: -50,
    maximum: 10000
  };
  let tdFragment = {
    properties: {
      temperature: temperaturePropertyDefinition
    },
    actions: {
      reset: {
        description: "重置温度传感器",
        input: {
          temperature: temperatureValueDefinition
        },
        output: null,
        forms: []
      },
    },
    events: {
      onchange: temperatureValueDefinition
    }
  };
  let thing1 = await WOT.produce(tdFragment);
  // 初始化属性
  await thing1.writeProperty("temperature", 0);
  // 添加服务处理器
  thing1.setPropertyReadHandler("temperature", () => {
     return readLocalTemperatureSensor();  // Promise
  });
  // 开始服务请求
  await thing1.expose();
} catch (err) {
   console.log("创建 ExposedThing 时出错:" + err);
}

下一个示例展示如何在现有的 ExposedThing 上添加或修改一个 属性定义: 取其 td 属性,添加或修改它,然后 用它创建另一个 ExposedThing

示例 7:添加一个 对象属性
try {
  // 创建 thing1 的 TD 的深拷贝
  let instance = JSON.parse(JSON.stringify(thing1.td));
  const statusValueDefinition = {
    type: "object",
    properties: {
      brightness: {
        type: "number",
        minimum: 0.0,
        maximum: 100.0,
        required: true
      },
      rgb: {
        type: "array",
        "minItems": 3,
        "maxItems": 3,
        items : {
            "type" : "number",
            "minimum": 0,
            "maximum": 255
        }
      }
  };
  instance["name"] = "mySensor";
  instance.properties["brightness"] = {
    type: "number",
    minimum: 0.0,
    maximum: 100.0,
    required: true,
  };
  instance.properties["status"] = statusValueDefinition;
  instance.actions["getStatus"] = {
    description: "获取状态对象",
    input: null,
    output: {
      status : statusValueDefinition;
    },
    forms: [...]
  };
  instance.events["onstatuschange"] = statusValueDefinition;
  instance.forms = [...];  // 更新
  var thing2 = new ExposedThing(instance);
  // TODO: 添加服务处理器
  await thing2.expose();
  });
} catch (err) {
   console.log("创建 ExposedThing 时出错:" + err);
}

以下内容将涵盖一组示例,说明如何使用 expand an ExposedThingInit 步骤,从 ExposedThingInit 生成一个物描述。 作为假设,运行时 支持 HTTP 和 COAP 协议绑定,并托管在 192.168.0.1。

下一个示例展示如何利用 ExposedThingInit 创建一个简单的物描述, 其中包含一个使用默认值的属性

编辑注

TODO:添加更多示例,其中 ExposedThingInit 包含由算法替换的建议值。

10. ThingDiscoveryProcess 인터페이스

Discovery는 참여하는 네트워크 노드 (클라이언트, 서버, 디렉터리 서비스)의 프로비저닝과 지원이 필요한 분산 애플리케이션입니다. 이 API는 다양한 IoT 배포에서 지원되는 일반적인 discovery 방식의 클라이언트 측을 모델링합니다.

ThingDiscoveryProcess 객체는 discovery process를 제어하고 결과를 반환하는 속성과 메서드를 제공합니다.

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

ThingDiscoveryProcess 객체에는 다음 internal slot이 있습니다.

Internal Slot 초기값 설명(비규범적)
[[filter]] undefined discovery에서 사용되는 ThingFilter 객체입니다.
[[url]] undefined discovery에서 TD Directory를 나타내는 URL입니다.

done 속성은 discovery가 중지되었거나 더 이상 보고할 결과 없이 완료된 경우 true입니다.

error 속성은 discovery process 중 발생한 마지막 오류를 나타냅니다. 일반적으로 discovery를 중지시키는 심각한 오류에 사용됩니다.

ThingDiscoveryProcess 객체는 async iterator 개념을 구현합니다.

10.1 ThingDiscoveryProcess 구성하기

filterThingDiscoveryProcess를 생성하려면 다음 단계를 실행합니다:
  1. filter가 객체 또는 null이 아니면, TypeErrorthrow하고 중지합니다.
  2. ThingDiscoveryProcess 객체를 discovery로 둡니다.
  3. discovery.[[filter]]filter로 설정합니다.
  4. discovery.donefalse로 설정합니다.
  5. discovery.errornull로 설정합니다.
  6. discovery를 반환합니다.

10.2 ThingFilter dictionary

Thing을 discovery하기 위한 제약 조건을 key-value 쌍으로 포함하는 객체를 나타냅니다.

WebIDLdictionary ThingFilter {
  object? fragment;
  
};

fragment 속성은 발견된 Thing과 속성별로 매칭하는 데 사용되는 템플릿 객체를 나타냅니다.

편집자 주

query 속성은 WoT Discovery 태스크포스에서 표준화될 때까지 ThingFilter에서 일시적으로 제거되었습니다. 이는 구현이 수락하는 query string, 예를 들어 SPARQL 또는 JSON query를 나타냈습니다. 지원은 WoT Runtime에서 로컬로 구현되거나, TD Directory의 서비스로 원격 구현될 예정이었습니다.

편집자 주

url 속성은 제거되었습니다. 이 속성은 discovery 요청을 제공하는 대상 엔티티, 예를 들어 TD Directory의 URL 또는 직접 대상으로 지정된 Thing의 URL을 나타냈지만, 이제는 전용 메서드로 구현됩니다.

10.3 discovery process 알고리즘

이미 시작된 discovery process 동안 무엇을 해야 하는지 설명합니다. 이 알고리즘은 discovery가 주어졌을 때 다음 단계를 실행합니다:
  1. Thing Description으로 이어지는 새 link가 발견되어 기본 플랫폼에서 제공될 때마다 다음 하위 단계를 실행합니다:
    1. 기본 discovery process에서 사용하는 Protocol Binding(link로 지정됨)을 사용하여 td를 JSON 객체로 가져옵니다. HTTP(S) Binding의 경우, 이 과정은 td를 가져오기 위해 Fetch API를 사용할 수 있습니다.
    2. 실패하면 discovery.error 속성을 SyntaxError로 설정하고, td를 폐기한 다음 discovery process를 계속합니다.
  2. Thing Description td가 발견되어 기본 플랫폼 또는 이전 단계에서 제공될 때마다 다음 하위 단계를 실행합니다:
    참고

    이 시점에서 구현은 discovery process의 흐름을 제어할 수 MAY 있습니다 (예를 들어 메모리 제약에 따라 결과를 큐에 넣거나, 큐가 너무 커지면 discovery를 일시적으로 중지하거나, 큐가 충분히 비워지면 discovery를 재개할 수 있습니다). 이 단계들은 발견/가져온 각 td에 대해 실행됩니다.

    1. discovery.[[filter]].fragmentfragment로 둡니다.
    2. fragmentobject이면, 그 안에 정의된 각 key에 대해:
      1. keyjson 안에 exists하는지, 그리고 json[key]가 fragment.key와 같은지 확인합니다.
      2. 이 검사 중 어느 하나라도 실패하면, td를 폐기하고 discovery process를 계속합니다.
    3. asyncIterator를 사용하여 td를 yield합니다.
      편집자 주

      적절한 asyncIterator 용어를 사용하여 이 단계를 개선합니다.

  3. discovery process 중 오류가 발생할 때마다 다음 하위 단계를 실행합니다:
    참고

    마지막 오류가 유지됩니다. 구현은 오류가 보고되어야 한다고 판단하면 discovery process를 중지하기로 선택할 수 MAY 있습니다.

    1. Error 객체를 error로 둡니다. error.name"DiscoveryError"로 설정합니다.
    2. Protocol Bindings에서 제공한 오류 코드 또는 메시지가 있었다면, error.message를 그 값의 문자열로 설정합니다.
    3. discovery.errorerror로 설정합니다.
    4. 오류가 복구 불가능하고 기본 플랫폼에 의해 discovery가 중지되었거나, 구현이 discovery process를 중지하고 오류를 보고하기로 결정했다면, discovery.donetrue로 설정하고 이 단계를 종료합니다.

10.4 stop() 메서드

discovery process를 중지하거나 억제합니다. 모든 discovery 메서드와 엔드포인트에서 지원되는 것은 아닐 수 있지만, 이후의 discovery 결과 또는 오류는 모두 폐기되고 discovery는 완료된 것으로 표시됩니다. 이 메서드는 다음 단계를 실행해야 MUST 합니다:
  1. 보안상의 이유로 현재 스크립팅 컨텍스트에서 이 메서드 호출이 허용되지 않으면, SecurityErrorthrow하고 중지합니다.
  2. 기본 플랫폼에 discovery process를 중지하도록 요청합니다. 이것이 오류를 반환하거나, 예를 들어 discovery가 끝이 열린 multicast 요청에 기반한 경우처럼 불가능하다면, 구현은 이후 발견된 항목을 폐기해야 SHOULD 합니다.
  3. done 속성을 true로 설정합니다.

10.5 Discovery 예제

다음 예제는 로컬 하드웨어에 의해 노출된 ThingThingDescription 객체를 찾습니다. 실행 중인 WoT Runtime 인스턴스 수와 관계없이, Discovery 객체가 제공하는 asyncIterator를 사용하여 결과를 비동기적으로 순회하고, 얻은 ThingDescription 객체로 작업을 수행할 수 있습니다.

예제 9: Thing의 Thing Description 가져오기
let url = "https://mythings.com/thing1";
let td = await WOT.requestThingDescription(url);
console.log("Found Thing Description for " + td.title);

다음 예제는 TD Directory 서비스에 나열된 ThingThingDescription 객체를 찾습니다. 안전을 위해 timeout을 설정합니다.

예제 10: directory를 통해 Things discovery하기
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);
}

다음 예제는 WOT runtime에 프로비저닝된 어떤 방식으로든 수행되는 일반 discovery를 위한 것으로, 사용 가능한 경우 로컬 Things도 포함합니다.

예제 11: 네트워크에서 Things discovery하기
let discovery = await WOT.discover();
setTimeout( () => {
    discovery.stop();
    console.log("Stopped open-ended discovery");
  },
  10000);
for await (const td of discovery) {
  console.log("Found Thing Description for " + td.title);
};
if (discovery.error) {
  console.log("Discovery stopped because of an error: " + error.message);
}

11. 보안 및 프라이버시

다양한 상황에 맞게 조정할 수 있는 위협 모델을 포함하여 Web of Things의 보안 및 프라이버시 고려사항에 대한 자세한 논의는 정보성 문서 [WOT-SECURITY]에 제시되어 있습니다. 이 섹션에서는 스크립트와 WoT Scripting API에 직접 관련된 보안 및 프라이버시 위험과 가능한 완화책만 논의합니다.

WoT devices 및 services의 보안을 개선하기 위한 권장 best practices 세트는 [WOT-SECURITY]에 문서화되어 있습니다. 그 문서는 보안 조치가 발전함에 따라 업데이트될 수 있습니다. 이러한 관행을 따른다고 보안이 보장되는 것은 아니지만, 일반적으로 알려진 취약점을 피하는 데 도움이 될 수 있습니다.

WoT 보안 위험 및 가능한 완화책은 다음 그룹과 관련됩니다:

11.1 Scripting Runtime 보안 및 프라이버시 위험

이 섹션은 규범적이며 WoT Scripting Runtime과 관련된 구체적인 위험을 포함합니다.

11.1.1 손상된 입력 보안 및 프라이버시 위험

어떤 프로세스를 손상시키는 일반적인 방법은 노출된 인터페이스 중 하나를 통해 손상된 입력을 보내는 것입니다. 이는 script instance가 노출하는 WoT interface를 사용하여 수행될 수 있습니다.

완화책:
이 API의 구현자는 모든 script 입력에 대해 검증을 수행해야 SHOULD 합니다. 입력 검증에 더해, 입력 처리가 올바르게 수행되는지 확인하기 위해 fuzzing을 사용해야 합니다. 이러한 검증을 수행하기 위한 많은 도구와 기법이 존재합니다. 자세한 내용은 [WOT-SECURITY]에서 찾을 수 있습니다.

11.1.2 물리적 장치 직접 접근 보안 및 프라이버시 위험

script가 손상되었거나 오동작하는 경우, script가 직접 노출된 native device interface를 사용할 수 있다면 기본 물리적 장치 (및 잠재적으로 주변 환경)가 손상될 수 있습니다. 이러한 인터페이스에 입력에 대한 안전성 검사가 없다면, 기본 물리적 장치(또는 환경)를 안전하지 않은 상태(예: 장치가 과열되어 폭발)로 만들 수 있습니다.

완화책:
WoT Scripting Runtime은 native device interface를 script 개발자에게 직접 노출하는 것을 피해야 SHOULD 합니다. 대신 WoT Scripting Runtime은 native device interface에 접근하기 위한 hardware abstraction layer를 제공해야 합니다. 이러한 hardware abstraction layer는 장치(또는 환경)를 안전하지 않은 상태로 만들 수 있는 명령 실행을 거부해야 합니다. 또한 script가 손상된 경우 물리적 WoT device에 대한 피해를 줄이기 위해, 특정 script의 기능에 기반하여 노출되거나 접근 가능한 interface의 수를 최소화하는 것이 중요합니다.

11.1.3 프로비저닝 및 업데이트 보안 위험

WoT Scripting Runtime이 제조 이후의 script 프로비저닝이나 업데이트, WoT Scripting Runtime 또는 관련 데이터(보안 자격 증명 포함)를 지원한다면, 이는 주요 공격 벡터가 될 수 있습니다. 공격자는 업데이트 또는 프로비저닝 과정에서 위에서 설명한 요소 중 하나를 수정하려 하거나, 공격자의 코드와 데이터를 직접 프로비저닝하려 할 수 있습니다.

완화책:
제조 이후 script, WoT Scripting Runtime 또는 관련 데이터의 프로비저닝이나 업데이트는 안전한 방식으로 수행되어야 합니다. 안전한 업데이트와 제조 이후 프로비저닝을 위한 권장사항 세트는 [WOT-SECURITY]에서 찾을 수 있습니다.

11.1.4 보안 자격 증명 저장 보안 및 프라이버시 위험

일반적으로 WoT Scripting Runtime은 WoT network에서 작동하기 위해 WoT device에 프로비저닝된 보안 자격 증명을 저장해야 합니다. 공격자가 이러한 자격 증명의 기밀성 또는 무결성을 손상시킬 수 있다면, WoT assets에 접근하거나, WoT things 또는 devices를 가장하거나, Denial-Of-Service(DoS) 공격을 만들 수 있습니다.

완화책:
WoT Scripting Runtime은 프로비저닝된 보안 자격 증명을 안전하게 저장하여 그 무결성과 기밀성을 보장해야 합니다. 단일 WoT-enabled device에 둘 이상의 tenant가 있는 경우, WoT Scripting Runtime은 각 tenant의 프로비저닝된 보안 자격 증명의 격리를 보장해야 합니다. 또한 프로비저닝된 보안 자격 증명이 손상될 위험을 최소화하기 위해, WoT Scripting Runtime은 script가 프로비저닝된 보안 자격 증명을 질의할 수 있는 API를 노출하지 않아야 합니다.

11.2 Script 보안 및 프라이버시 위험

이 섹션은 비규범적입니다.

이 섹션은 script 개발자와 관련된 구체적인 위험을 설명합니다.

11.2.1 손상된 Script 입력 보안 및 프라이버시 위험

script instance는 TD가 정의한 데이터 형식 또는 애플리케이션이 정의한 데이터 형식을 수신할 수 있습니다. WoT Scripting Runtime은 TD가 정의한 모든 입력 필드에 대해 검증을 수행해야 SHOULD 하지만, script는 여전히 입력 데이터에 의해 악용될 수 있습니다.

완화책:
Script 개발자는 애플리케이션이 정의한 모든 script 입력에 대해 검증을 수행해야 합니다. 입력 검증에 더해, 입력 처리가 올바르게 수행되는지 확인하기 위해 fuzzing을 사용할 수 있습니다. 이러한 검증을 수행하기 위한 많은 도구와 기법이 존재합니다. 자세한 내용은 [WOT-SECURITY]에서 찾을 수 있습니다.

11.2.2 Denial Of Service 보안 위험

요청이 인증되기 전에 script가 수신한 요청에 대해 무거운 기능 처리를 수행하면, Denial-Of-Service(DOS) 공격에 큰 위험을 제공합니다.

완화책:
Scripts는 요청자의 성공적인 사전 인증 없이 무거운 기능 처리를 피해야 합니다. 권장되는 인증 메커니즘 세트는 [WOT-SECURITY]에서 찾을 수 있습니다.

A. API 설계 근거

API 근거는 보통 별도의 문서에 속하지만, WoT의 경우 context의 복잡성이 여기에 기본 근거를 포함하는 것을 정당화합니다.

A.1 WoT 애플리케이션 개발 접근 방식

WoT Interest Group과 Working Group은 모두 구현되고 테스트된 WoT 애플리케이션 개발을 위한 다양한 접근 방식을 탐구했습니다.

A.1.1 Scripting API 없음

WoT network interface만 사용하는 WoT 애플리케이션을 개발할 수 있습니다. 이는 일반적으로 클라이언트에게 RESTful API를 제공하고 지원되는 IoT 배포와 통신하는 IoT protocol plugins를 구현하는 WoT gateway에 의해 노출됩니다. 그러한 구현 중 하나는 Mozilla WebThings 플랫폼입니다.

A.1.2 단순 Scripting API

WoT Thing은 software object와 좋은 시너지를 보이므로, Thing은 software object로 표현될 수 있으며, Properties는 object properties로, Action은 methods로, Event는 events로 표현됩니다. 또한 metadata는 특수 properties에 저장됩니다. consuming 및 exposing은 remote Thing과 그 interactions를 직접 나타내는 software object를 생성하는 factory methods로 수행됩니다. 그러한 구현 중 하나는 Arena Web Hub 프로젝트입니다.

다음 예제에서 lock과의 interactions를 나타내는 Thing은 다음과 같습니다: status 속성과 open() 메서드는 객체에 직접 노출됩니다.

예제 12: 단순 API로 lock 열기
let lock = await WoT.consume(‘https://td.my.com/lock-00123’);
console.log(lock.status);
lock.open('withThisKey');

A.1.3 Web of Things (WoT) Thing Description 1.1 명세와 정렬된 이 API

Thing을 software object로 직접 매핑하는 방식에는 몇 가지 어려움이 있었기 때문에, 이 명세는 Thing metadata를 data property로, WoT interactions를 methods로 표현하기 위해 software object를 노출하는 다른 접근 방식을 취합니다. 한 구현은 이 문서에 명시된 API의 현재 참조 구현인 node-wot이며, 이는 Eclipse ThingWeb 프로젝트에 속합니다.

이제 같은 예제는 다음과 같이 보입니다: status 속성과 open() 메서드는 간접적으로 표현됩니다.

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

결론적으로, WoT WG는 Web of Things (WoT) Thing Description 1.1 명세를 밀접하게 따르는 세 번째 옵션을 탐구하기로 결정했습니다. 이를 기반으로 단순 API도 구현할 수 있습니다. Scripting은 WoT의 선택적 모듈이므로, 이는 WoT network interface만 사용하는 애플리케이션을 위한 여지를 남깁니다. 따라서 위의 세 가지 접근 방식 모두 Web of Things (WoT) Thing Description 1.1 명세에서 지원됩니다.

또한 WoT network interface는 많은 언어와 runtime에서 구현될 수 있습니다. 이 API를 WoT를 위한 Scripting API를 설계할 때 고려해야 할 사항의 예로 간주하십시오.

A.2 TD 가져오기 및 검증

fetch(url) 메서드는 이 API의 이전 버전에 포함되어 있었습니다. 그러나 이제 URL이 주어진 TD를 가져오는 것은 Fetch API 또는 HTTP client library와 같은 외부 메서드로 수행되어야 하며, 이러한 메서드는 fetch 세부사항을 지정하기 위한 이미 표준화된 옵션을 제공합니다. 그 이유는 단순 fetch 작업(대부분의 사용 사례를 포함)은 이 API에서 수행될 수 있었지만, 다양한 fetch 옵션이 필요할 때 기존 작업을 중복하여 이 API에서 그러한 옵션을 다시 노출할 이유가 없었기 때문입니다.

TD 가져오기가 범위에서 제외되었고, TD 검증도 Web of Things (WoT) Thing Description 1.1 명세에서 외부적으로 정의되므로, 그것 역시 범위에서 제외됩니다. 이 명세는 Web of Things (WoT) Thing Description 1.1 명세에 따라 검증된 parsed JSON object로서의 TD를 기대합니다.

A.3 Factory와 constructors

Thing을 consume하고 expose하는 factory methods는 비동기적이며 입력 TD를 완전히 검증합니다. 또한, parsed되고 검증된 TD를 제공하여 ConsumedThingExposedThing도 구성할 수 있습니다. 그러면 platform 초기화는 WoT interactions 중 필요할 때 수행됩니다.

A.4 Observers

이전 초안은 Observer 구조를 사용했지만, 표준이 되지 않았기 때문에 embedded implementations에 충분히 가벼운 새로운 설계가 필요했습니다. 따라서 Property 변경 관찰과 WoT Event 처리는 callback registrations로 수행됩니다.

A.5 Events 사용하기

이 API는 결국 software events를 전혀 사용하지 않게 되었으며, 그 이유는 다음과 같습니다:
  • WoT Event 구독은 software events 처리와 다를 수 있습니다 (구독에 parameters가 필요하거나, security token 등이 관련될 수 있습니다).
  • 대부분의 구현은 Node.js용이며, browser 구현은 (native implementations에서 가능한 dependency management 문제 때문에) library가 될 가능성이 높아 Events 사용이 어려웠습니다.
  • Property 변경 관찰과 WoT Event 처리는 위의 해결책으로 수행됩니다.

A.6 Polymorphic functions

일반적인 polymorphic read() 함수 대신 readProperty(), readMultipleProperties() 등의 함수 이름을 사용하는 이유는, 현재 이름들이 Web of Things (WoT) Thing Description 1.1 명세의 Form 정의에서 가져온 "op" vocabulary에 정확히 매핑되기 때문입니다.

B. 변경사항

다음은 이 문서의 주요 변경사항 목록입니다. 이 명세의 주요 버전은 다음과 같습니다:

전체 변경사항 목록은 github change log를 참조하십시오. 또한 최근 닫힌 issues를 볼 수 있습니다.

C. 전체 Web IDL

WebIDLtypedef object ThingDescription;

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

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

typedef object ExposedThingInit;

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

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

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

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

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

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

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

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

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

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

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

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

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

  ExposedThing setActionHandler(DOMString name, ActionHandler action);

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

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

  ThingDescription getThingDescription();
};

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

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

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

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

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

dictionary ThingFilter {
  object? fragment;
  
};

D. 감사의 말

이 명세를 개발한 이전 편집자 Johannes Hund(2017년 8월까지, 당시 Siemens AG 재직)와 Kazuaki Nimura(2018년 12월까지)에게 특별한 감사를 표합니다. 또한 편집자들은 Dave Raggett, Matthias Kovatsch, Michael Koster, Elena Reshetova, Michael McCool 및 다른 WoT WG 구성원들의 의견, 기여와 지침에 감사드립니다.

E. 참고문헌

E.1 규범 참고문헌

[ECMASCRIPT]
ECMAScript 언어 명세. Ecma International. URL: https://tc39.es/ecma262/multipage/
[fetch]
Fetch Standard. Anne van Kesteren. WHATWG. Living Standard. URL: https://fetch.spec.whatwg.org/
[html]
HTML Standard. Anne van Kesteren; Domenic Denicola; Ian Hickson; Philip Jägenstedt; Simon Pieters. WHATWG. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[infra]
Infra Standard. Anne van Kesteren; Domenic Denicola. WHATWG. Living Standard. URL: https://infra.spec.whatwg.org/
[JSON-SCHEMA]
JSON Schema: JSON 문서를 설명하기 위한 Media Type. Austin Wright; Henry Andrews; Ben Hutton; Greg Dennis. Internet Engineering Task Force (IETF). 2020년 12월 8일. Internet-Draft. URL: https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema
[RFC2119]
RFC에서 요구 수준을 나타내기 위해 사용하는 핵심 단어. S. Bradner. IETF. 1997년 3월. Best Current Practice. URL: https://www.rfc-editor.org/rfc/rfc2119
[RFC8174]
RFC 2119 핵심 단어에서 대문자와 소문자의 모호성. B. Leiba. IETF. 2017년 5월. Best Current Practice. URL: https://www.rfc-editor.org/rfc/rfc8174
[streams]
Streams Standard. Adam Rice; Domenic Denicola; Mattias Buelens; 吉野剛史 (Takeshi Yoshino). WHATWG. Living Standard. URL: https://streams.spec.whatwg.org/
[TYPESCRIPT]
TypeScript 언어 명세. Microsoft. 2012년 10월 1일. URL: https://www.typescriptlang.org/docs/handbook/intro.html
[url]
URL Standard. Anne van Kesteren. WHATWG. Living Standard. URL: https://url.spec.whatwg.org/
[WEBIDL]
Web IDL Standard. Edgar Chen; Timothy Gu. WHATWG. Living Standard. URL: https://webidl.spec.whatwg.org/
[WOT-ARCHITECTURE]
Web of Things (WoT) Architecture 1.1. W3C. 2023년 1월 19일. URL: https://www.w3.org/TR/2023/CR-wot-architecture11-20230119/
[WOT-PROTOCOL-BINDINGS]
Web of Things (WoT) Binding Templates. W3C. 2020년 1월 30일. URL: https://www.w3.org/TR/2020/NOTE-wot-binding-templates-20200130/
[WOT-SECURITY]
Web of Things (WoT) Security and Privacy Guidelines. W3C. 2019년 11월 6일. URL: https://www.w3.org/TR/2019/NOTE-wot-security-20191106/
[WOT-TD]
Web of Things (WoT) Thing Description 1.1. W3C. 2023년 1월 19일. URL: https://www.w3.org/TR/2023/CR-wot-thing-description11-20230119/

E.2 정보성 참고문헌

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