registry: SceneController events, DoorbellEventSource, SimpleEventSource, TimeHoldController, InventoryLevelSensor, WakeOnLANController

Six descriptors written from their pages replace the last stubs of tiers 1 and 2. A scene answers Activate and
Deactivate through ctx.respond() with ActivationStarted and DeactivationStarted, the time and the cause filled
in. device.raise(descriptor, name, payload) publishes DoorbellPress and the Event of a button on <root>/event
with the endpoint and a new messageId; it throws a MessageError for an interface or instance the device did
not declare, an event that answers a directive, a payload that does not fit and a message over 16000 bytes.
TurnOn of a device with WakeOnLANController is deferred without the warning.

On the wire: a doorbell has no properties object and proactivelyReported on the capability; SimpleEventSource
is version 1.0, InventoryLevelSensor and WakeOnLANController version 3 (1.5.2: 1). A scene declared without
options is announced as before. Alex2MQTT has no topic yet for the WakeUp event.
24 examples of the six pages are saved as fixtures. 267 tests pass, 241 before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 21:01:53 +00:00
parent d7dfc5b122
commit 3a07c861b1
116 changed files with 3136 additions and 166 deletions

View file

@ -34,10 +34,11 @@ function likeTheZoo(endpointId) {
return device;
}
for (const { namespace, page: own, declared } of interfaces) {
for (const { namespace, page: own, declared, endpoint: { categories, description } = {} } of interfaces) {
for (const { options, example, page = own, n = 0 } of declared) {
test(`${namespace}: device.add() gives the capability object of ${page}.html, ${example}${n > 0 ? ` (${n})` : ""}`, () => {
const device = endpoint();
const device = endpoint("lamp-1", "Lamp", categories);
if (description) device.setDescription(description);
const capability = device.add(registry.get(namespace), options);
assert.deepEqual(capability.toJSON(), capabilities(page, namespace, example)[n]);
assert.deepEqual(device.check(), []);

145
test/device/raise.test.js Normal file
View file

@ -0,0 +1,145 @@
"use strict";
// device.raise(): an event nobody asked for goes to <root>/event, and one that would not arrive is refused.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const {
Alex2MQTT, AlexaInterfaceType, DoorbellEventSource, MemoryPublisher, MessageError, PowerController, SceneController,
SimpleEventSource, asset, topics,
} = require("alex2node");
const { endpoint } = require("../helpers/endpoint.js");
const { doc } = require("../helpers/fixtures.js");
const { UUID_V4 } = require("../helpers/harness.js");
function doorbell() {
const sent = new MemoryPublisher();
const errors = [];
const bridge = new Alex2MQTT("u", "p", "root", false, { publisher: sent, log: () => {} });
bridge.on("error", (err) => errors.push(err));
const device = bridge.addDevice({ endpointId: "door-1", name: "Front door", categories: ["DOORBELL"] });
device.add(DoorbellEventSource);
return { bridge, device, sent, errors };
}
const button = (name, events) => ({
instance: `Remote.${name}`,
friendlyNames: [asset(`Alexa.Button.${name}`)],
supportedEvents: events.map((push) => ({ id: `Button.${push}.1`, friendlyNames: [asset(`Alexa.Button.${push}`)] })),
});
const refused = (raise, problem) => assert.throws(raise, (err) => err instanceof MessageError && err.problem === problem);
test("raise() publishes the event on <root>/event, with the endpoint, a new messageId, the time and the usual cause", async () => {
const { device, sent } = doorbell();
const printed = doc("alexa-doorbelleventsource", "DoorbellPress.event").event;
const before = Date.now();
assert.deepEqual(await device.raise(DoorbellEventSource, "DoorbellPress"), { ok: true, topic: "root/event" });
await device.raise(DoorbellEventSource, "DoorbellPress", printed.payload);
await device.raise(DoorbellEventSource, "DoorbellPress", { cause: { type: "APP_INTERACTION" } }, { messageId: "press-3" });
assert.equal(topics.event("root"), "root/event");
assert.deepEqual(sent.published.map(({ topic }) => topic), ["root/event", "root/event", "root/event"]);
const [first, second, third] = sent.published.map(({ message }) => message.event);
for (const event of [first, second, third]) {
assert.deepEqual(event.endpoint, { endpointId: "door-1" });
const { messageId, ...header } = event.header;
assert.deepEqual(header, { namespace: printed.header.namespace, name: printed.header.name, payloadVersion: printed.header.payloadVersion });
}
assert.match(first.header.messageId, UUID_V4);
assert.match(second.header.messageId, UUID_V4);
assert.notEqual(first.header.messageId, second.header.messageId);
assert.equal(third.header.messageId, "press-3");
assert.deepEqual(first.payload.cause, { type: "PHYSICAL_INTERACTION" });
assert.ok(Date.parse(first.payload.timestamp) >= before && Date.parse(first.payload.timestamp) <= Date.now(), first.payload.timestamp);
assert.deepEqual(second.payload, printed.payload);
assert.deepEqual(third.payload.cause, { type: "APP_INTERACTION" });
});
test("raise() for an instance: the Event of a button has the instance in its header and the version of the interface", async () => {
const sent = new MemoryPublisher();
const remote = endpoint("remote-1", "Remote", ["REMOTE"]);
remote.publisher = sent;
const top = remote.add(SimpleEventSource, button("TopButton", ["SinglePush", "DoublePush"]));
remote.add(SimpleEventSource, button("BottomButton", ["SinglePush"]));
const printed = doc("alexa-simpleeventsource", "Event.event").event;
await remote.raise(SimpleEventSource, "Event", printed.payload, { instance: top.instance, messageId: "push-1" });
assert.deepEqual(sent.published, [{
topic: "root/event",
message: {
event: {
header: { namespace: "Alexa.SimpleEventSource", name: "Event", instance: "Remote.TopButton", messageId: "push-1", payloadVersion: "1.0" },
endpoint: { endpointId: "remote-1" },
payload: printed.payload,
},
},
}]);
assert.equal(printed.header.payloadVersion, "1.0");
const raised = "remote-1: Alexa.SimpleEventSource.Event was not raised: ";
assert.throws(() => remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }), {
name: "MessageError",
message: `${raised}the device did not declare Alexa.SimpleEventSource. Declare it with add() first, and pass the instance that raises the event`,
});
assert.throws(() => remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }, { instance: "Remote.LeftButton" }), {
message: `${raised}the device did not declare the instance "Remote.LeftButton" of Alexa.SimpleEventSource. Declare it with add() first`,
});
assert.throws(() => remote.raise(SimpleEventSource, "Event", {}, { instance: top.instance }), {
message: `${raised}payload.id: expected a string of 1 or more characters, got nothing`,
});
assert.equal(sent.published.length, 1);
});
test("raise() refuses an event the device has no capability for, and one that would not arrive", () => {
const { device, sent } = doorbell();
device.add(PowerController);
const scene = endpoint("scene-1", "Evening", ["SCENE_TRIGGER"]);
scene.setDescription("Evening scene");
scene.publisher = sent;
scene.add(SceneController);
const was = (name) => `${name} was not raised: `;
refused(() => scene.raise(DoorbellEventSource, "DoorbellPress"), `${was("Alexa.DoorbellEventSource.DoorbellPress")}the device did not declare Alexa.DoorbellEventSource. Declare it with add() first`);
refused(() => device.raise(DoorbellEventSource, "Knock"), `${was("Alexa.DoorbellEventSource.Knock")}it is not an event of the interface, which has DoorbellPress`);
refused(() => device.raise(PowerController, "TurnedOn"), `${was("Alexa.PowerController.TurnedOn")}it is not an event of the interface, which has none`);
refused(
() => scene.raise(SceneController, "ActivationStarted"),
`${was("Alexa.SceneController.ActivationStarted")}it answers a directive. Send it with respond() in the handler of the directive`
);
refused(
() => device.raise(DoorbellEventSource, "DoorbellPress", { timestamp: "just now" }),
`${was("Alexa.DoorbellEventSource.DoorbellPress")}payload.timestamp: expected a UTC time like 2017-08-30T01:18:21Z, got "just now"`
);
refused(
() => device.raise(DoorbellEventSource, "DoorbellPress", { cause: { type: "KNOCK" } }),
`${was("Alexa.DoorbellEventSource.DoorbellPress")}payload.cause.type: expected APP_INTERACTION | PERIODIC_POLL | PHYSICAL_INTERACTION | RULE_TRIGGER | VOICE_INTERACTION, got "KNOCK"`
);
refused(
() => device.raise(DoorbellEventSource, "DoorbellPress", { picture: "x".repeat(topics.EVENT_BYTES) }),
`${was("Alexa.DoorbellEventSource.DoorbellPress")}it is 16296 bytes as JSON, Alex2MQTT takes 16000`
);
assert.deepEqual(sent.published, []);
});
test("raise() of a doorbell declared the 1.x way, of a device on no bridge and with a broker that is gone", async () => {
const sent = new MemoryPublisher();
const bell = endpoint("door-2", "Back door", ["DOORBELL"]);
bell.addCapability(AlexaInterfaceType.DOORBELL_EVENT_SOURCE);
const reported = [];
bell.onPublishError = (err) => reported.push(err.message);
const nowhere = await bell.raise(DoorbellEventSource, "DoorbellPress");
assert.equal(nowhere.ok, false);
assert.deepEqual(reported, ["nothing was published to root/event: the device is on no bridge, register it with addDevice() or registerDevice()"]);
bell.publisher = sent;
assert.equal((await bell.raise(DoorbellEventSource, "DoorbellPress")).ok, true);
assert.equal(sent.published[0].message.event.endpoint.endpointId, "door-2");
const { device, sent: broker, errors } = doorbell();
broker.failWith = new Error("the broker is gone");
const result = await device.raise(DoorbellEventSource, "DoorbellPress");
assert.deepEqual([result.ok, result.topic, result.error.message], [false, "root/event", "the broker is gone"]);
assert.deepEqual(errors.map((err) => err.message), ["the broker is gone"]);
});

View file

@ -4,8 +4,8 @@
const { test } = require("node:test");
const assert = require("node:assert/strict");
const {
Alex2MQTT, AlexaErrors, LockController, MemoryPublisher, PowerLevelController, SecurityPanelController,
ThermostatController, ThermostatControllerSchedule, registry,
Alex2MQTT, AlexaErrors, LockController, MemoryPublisher, PowerController, PowerLevelController, SceneController,
SecurityPanelController, ThermostatController, TimeHoldController, WakeOnLANController, ThermostatControllerSchedule, registry,
} = require("alex2node");
const interfaces = require("../fixtures/interfaces.js");
const { doc, examples } = require("../helpers/fixtures.js");
@ -181,3 +181,70 @@ test("Alexa.SecurityPanelController: a payload that is no Arm.Response is not se
message: "payload.exitDelayInSeconds: expected an integer from 0 to 255, got 300",
}]]);
});
test("Alexa.SceneController: respond() answers Activate with ActivationStarted and Deactivate with DeactivationStarted", async () => {
const sent = new MemoryPublisher();
const bridge = new Alex2MQTT("u", "p", "root", false, { publisher: sent, log: () => {} });
const scenes = ["party", "bedtime"].map((endpointId) => bridge.addDevice({
endpointId, name: endpointId, categories: ["SCENE_TRIGGER"], description: `The ${endpointId} scene`,
}));
scenes[0].add(SceneController).on("*", (ctx) => ctx.respond());
scenes[1].add(SceneController, { supportsDeactivation: false }).on("*", (ctx) => ctx.respond(undefined, { payload: { timestamp: "2017-02-03T16:20:50Z" } }));
const before = Date.now();
let count = 0;
for (const [endpointId, name] of [["party", "Activate"], ["party", "Deactivate"], ["bedtime", "Activate"], ["bedtime", "Deactivate"]]) {
count += 1;
const { header, payload } = doc("alexa-scenecontroller", `${name}.directive`).directive;
const message = { header: { ...header, messageId: `message-${count}`, correlationToken: `ct-${count}` }, endpoint: { endpointId }, payload };
await bridge.receive(`root/${endpointId}/alexaDirective`, JSON.stringify(message));
}
const events = sent.published.map(({ message }) => message.event);
assert.deepEqual(sent.published.map(({ topic }) => topic), ["root/party/alexaResponce", "root/party/alexaResponce", "root/bedtime/alexaResponce", "root/bedtime/alexaResponce"]);
assert.deepEqual(events.map(({ header }) => [header.namespace, header.name, header.correlationToken]), [
["Alexa.SceneController", "ActivationStarted", "ct-1"],
["Alexa.SceneController", "DeactivationStarted", "ct-2"],
["Alexa.SceneController", "ActivationStarted", "ct-3"],
["Alexa", "ErrorResponse", "ct-4"],
]);
assert.deepEqual(events[2].payload, doc("alexa-scenecontroller", "ActivationStarted.event").event.payload);
assert.equal(events[3].payload.message, "Alexa.SceneController as bedtime declares it does not take Deactivate");
for (const { payload } of events.slice(0, 2)) {
assert.deepEqual(payload.cause, { type: "VOICE_INTERACTION" });
assert.ok(Date.parse(payload.timestamp) >= before && Date.parse(payload.timestamp) <= Date.now(), payload.timestamp);
}
});
test("Alexa.TimeHoldController: Resume reaches a device declared with allowRemoteResume only", async () => {
const { device, receive, answers } = bridgeWithout();
device.add(TimeHoldController, { allowRemoteResume: false }).on("*", (ctx) => ctx.respond());
await receive(doc("alexa-timeholdcontroller", "Hold.directive").directive);
await receive(doc("alexa-timeholdcontroller", "Resume.directive").directive);
assert.deepEqual(answers(), [
["alexaResponce", "Alexa", "Response", {}],
["alexaResponce", "Alexa", "ErrorResponse", {
type: "INVALID_DIRECTIVE",
message: "Alexa.TimeHoldController as device-1 declares it does not take Resume",
}],
]);
});
test("Alexa.WakeOnLANController: TurnOn is deferred without a warning, TurnOff with the one of the PowerController", async () => {
const { device, logged, receive, answers } = bridgeWithout();
device.add(WakeOnLANController, { macAddresses: ["00-14-22-01-23-45"] });
const power = device.add(PowerController);
power.on("*", async (ctx) => {
await ctx.defer(15);
return ctx.respond((s) => s.set(power, "powerState", ctx.name === "TurnOn" ? "ON" : "OFF"));
});
await receive(doc("alexa-wakeonlancontroller", "TurnOn.directive").directive);
assert.deepEqual(answers(), [
["alexaResponce", "Alexa", "DeferredResponse", doc("alexa-wakeonlancontroller", "TurnOn.deferred").event.payload],
["deferredResponse", "Alexa", "Response", doc("alexa-wakeonlancontroller", "TurnOn.response").event.payload],
]);
const warnings = () => logged.filter((line) => line.includes("DeferredResponse"));
assert.deepEqual(warnings(), []);
await receive(doc("alexa-powercontroller", "TurnOff.directive").directive);
assert.deepEqual(warnings(), ["warning: Amazon documents no DeferredResponse for Alexa.PowerController, Alexa may not wait for the answer"]);
});

View file

@ -0,0 +1,24 @@
{
"context": {},
"event": {
"header": {
"messageId": "Unique identifier, preferably a version 4 UUID",
"namespace": "Alexa.DoorbellEventSource",
"name": "DoorbellPress",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "access-token-from-Amazon"
},
"endpointId": "appliance-001"
},
"payload": {
"cause": {
"type": "PHYSICAL_INTERACTION"
},
"timestamp": "2018-06-09T23:23:23.23Z"
}
}
}

View file

@ -0,0 +1,60 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Sample Manufacturer",
"description": "Description that appears in the Alexa app",
"friendlyName": "Your device name, displayed in the Alexa app",
"displayCategories": [
"CAMERA",
"DOORBELL"
],
"additionalAttributes": {
"manufacturer": "Sample Manufacturer",
"model": "Sample Model",
"serialNumber": "Serial number of the device",
"firmwareVersion": "Firmware version of the device",
"softwareVersion": "Software version of the device",
"customIdentifier": "Optional custom identifier for the device"
},
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.DoorbellEventSource",
"version": "3",
"proactivelyReported": true
},
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3.1",
"properties": {
"supported": [
{
"name": "connectivity"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -0,0 +1,65 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "ChangeReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {
"change": {
"cause": {
"type": "PERIODIC_POLL"
},
"properties": [
{
"namespace": "Alexa.InventoryLevelSensor",
"instance": "InkSensor.Cyan",
"name": "level",
"value": {
"@type": "Volume",
"value": 5,
"unit": "MILLILITER"
},
"timeOfSample": "2019-10-31T17:00:00.00Z",
"uncertaintyInMilliseconds": 0
},
{
"namespace": "Alexa.InventoryLevelSensor",
"instance": "PaperSensor.FrontTray",
"name": "level",
"value": {
"@type": "Count",
"value": 200
},
"timeOfSample": "2019-10-31T17:00:00.00Z",
"uncertaintyInMilliseconds": 0
},
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "ON",
"timeOfSample": "2019-10-31T17:00:00.00Z",
"uncertaintyInMilliseconds": 0
}
]
}
}
},
"context": {
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": "OK"
},
"timeOfSample": "2019-10-31T16:58:10:00.00Z",
"uncertaintyInMilliseconds": 0
}
}

View file

@ -0,0 +1,123 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "AddOrUpdateReport",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Printer Plus",
"description": "Smart Printer by Printer Maker Plus",
"friendlyName": "Printer",
"displayCategories": [
"OTHER"
],
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.InventoryLevelSensor",
"instance": "InkSensor.Cyan",
"version": "3",
"properties": {
"supported": [
{
"name": "level"
}
],
"retrievable": true,
"proactivelyReported": true
},
"configuration": {
"measurement": {
"@type": "Volume",
"unit": "MILLILITER"
},
"replenishment": {
"@type": "DashReplenishmentId",
"value": "replenishment ID for refill options"
}
},
"capabilityResources": {
"friendlyNames": [
{
"@type": "text",
"value": {
"text": "Cyan ink",
"locale": "en-US"
}
},
{
"@type": "text",
"value": {
"text": "Encre cyan",
"locale": "fr-FR"
}
}
]
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.InventoryLevelSensor",
"instance": "PaperSensor.FrontTray",
"version": "3",
"properties": {
"supported": [
{
"name": "level"
}
],
"retrievable": true,
"proactivelyReported": true
},
"configuration": {
"measurement": {
"@type": "Count"
},
"replenishment": {
"@type": "DashReplenishmentId",
"value": "replenishment ID for refill options"
}
},
"capabilityResources": {
"friendlyNames": [
{
"@type": "text",
"value": {
"text": "Front tray",
"locale": "en-US"
}
}
]
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [
{
"name": "powerState"
}
],
"retrievable": true,
"proactivelyReported": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -0,0 +1,137 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Printer Plus",
"description": "Smart Printer by Printer Maker Plus",
"friendlyName": "Printer",
"displayCategories": [
"OTHER"
],
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.InventoryLevelSensor",
"instance": "InkSensor.Cyan",
"version": "3",
"properties": {
"supported": [
{
"name": "level"
}
],
"retrievable": false,
"proactivelyReported": true
},
"configuration": {
"measurement": {
"@type": "Volume",
"unit": "MILLILITER"
},
"replenishment": {
"@type": "DashReplenishmentId",
"value": "replenishment ID for refill options"
}
},
"capabilityResources": {
"friendlyNames": [
{
"@type": "text",
"value": {
"text": "Cyan ink",
"locale": "en-US"
}
},
{
"@type": "text",
"value": {
"text": "Encre cyan",
"locale": "fr-FR"
}
}
]
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.InventoryLevelSensor",
"instance": "PaperSensor.FrontTray",
"version": "3",
"properties": {
"supported": [
{
"name": "level"
}
],
"retrievable": false,
"proactivelyReported": true
},
"configuration": {
"measurement": {
"@type": "Count"
},
"replenishment": {
"@type": "DashReplenishmentId",
"value": "replenishment ID for refill options"
}
},
"capabilityResources": {
"friendlyNames": [
{
"@type": "text",
"value": {
"text": "Front tray",
"locale": "en-US"
}
}
]
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [
{
"name": "powerState"
}
],
"retrievable": true,
"proactivelyReported": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3",
"properties": {
"supported": [
{
"name": "connectivity"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -0,0 +1,19 @@
{
"directive": {
"header": {
"namespace": "Alexa.SceneController",
"name": "Activate",
"messageId": "Unique version 4 UUID",
"correlationToken": "Opaque correlation token",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "endpoint id"
},
"payload": {}
}
}

View file

@ -0,0 +1,25 @@
{
"event": {
"header": {
"namespace": "Alexa.SceneController",
"name": "ActivationStarted",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "endpoint id"
},
"payload": {
"cause": {
"type": "VOICE_INTERACTION"
},
"timestamp": "2017-02-03T16:20:50Z"
}
},
"context": {}
}

View file

@ -0,0 +1,19 @@
{
"directive": {
"header": {
"namespace": "Alexa.SceneController",
"name": "Deactivate",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "endpoint id"
},
"payload": {}
}
}

View file

@ -0,0 +1,25 @@
{
"event": {
"header": {
"namespace": "Alexa.SceneController",
"name": "DeactivationStarted",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "endpoint id"
},
"payload": {
"cause": {
"type": "VOICE_INTERACTION"
},
"timestamp": "2017-02-03T16:20:50Z"
}
},
"context": {}
}

View file

@ -0,0 +1,37 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "unique ID of the scene",
"manufacturerName": "Sample Vendor",
"description": "Party scene by Sample Vendor",
"friendlyName": "Living Room Party",
"displayCategories": [
"SCENE_TRIGGER"
],
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.SceneController",
"version": "3",
"supportsDeactivation": false
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -0,0 +1,23 @@
{
"event": {
"header": {
"namespace": "Alexa.SimpleEventSource",
"name": "Event",
"instance": "Unique identifier, preferably a version 4 UUID",
"messageId": "Unique identifier, preferably a version 4 UUID",
"payloadVersion": "1.0"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2 bearer token"
},
"endpointId": "Endpoint id",
"cookie": {}
},
"payload": {
"id": "Button.SinglePush.1",
"timestamp": "2021-12-12T16:20:50.52Z"
}
}
}

View file

@ -0,0 +1,20 @@
{
"directive": {
"header": {
"namespace": "Alexa.TimeHoldController",
"name": "Hold",
"messageId": "Unique version 4 UUID",
"correlationToken": "Opaque correlation token",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID",
"cookie": {}
},
"payload": {}
}
}

View file

@ -0,0 +1,37 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "Response",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.TimeHoldController",
"name": "holdStartTime",
"value": "2018-05-31T14:30:00.00Z",
"timeOfSample": "2018-05-31T23:30:02.32Z",
"uncertaintyInMilliseconds": 0
},
{
"namespace": "Alexa.TimeHoldController",
"name": "holdEndTime",
"value": "2018-05-31T14:40:00.00Z",
"timeOfSample": "2018-05-31T23:30:02.32Z",
"uncertaintyInMilliseconds": 0
}
]
}
}

View file

@ -0,0 +1,20 @@
{
"directive": {
"header": {
"namespace": "Alexa.TimeHoldController",
"name": "Resume",
"messageId": "Unique version 4 UUID",
"correlationToken": "Opaque correlation token",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID",
"cookie": {}
},
"payload": {}
}
}

View file

@ -0,0 +1,20 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "Response",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {}
}

View file

@ -0,0 +1,37 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "StateReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.TimeHoldController",
"name": "holdStartTime",
"value": "2018-05-31T14:30:00.00Z",
"timeOfSample": "2018-08-31T23:30:00Z",
"uncertaintyInMilliseconds": 0
},
{
"namespace": "Alexa.TimeHoldController",
"name": "holdEndTime",
"value": "2018-05-31T14:40:00.00Z",
"timeOfSample": "2018-08-31T23:30:00Z",
"uncertaintyInMilliseconds": 0
}
]
}
}

View file

@ -0,0 +1,94 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Manufacturer of the endpoint",
"description": "Description to be shown in the Alexa app",
"friendlyName": "Microwave",
"displayCategories": [
"MICROWAVE"
],
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.TimeHoldController",
"version": "3",
"properties": {
"supported": [
{
"name": "holdStartTime"
},
{
"name": "holdEndTime"
}
],
"proactivelyReported": true,
"retrievable": true
},
"configuration": {
"allowRemoteResume": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.Cooking",
"version": "3",
"properties": {
"supported": [
{
"name": "cookingMode"
},
{
"name": "foodItem"
},
{
"name": "cookingTimeInterval"
}
],
"proactivelyReported": true,
"retrievable": true
},
"configuration": {
"supportsRemoteStart": false,
"supportedCookingModes": [
"REHEAT",
"DEFROST",
"PRESET",
"OFF"
]
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3",
"properties": {
"supported": [
{
"name": "connectivity"
}
],
"proactivelyReported": false,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -0,0 +1,42 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "ChangeReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {
"change": {
"cause": {
"type": "VOICE_INTERACTION"
},
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "ON",
"timeOfSample": "2024-02-03T16:16:00.00Z",
"uncertaintyInMilliseconds": 0
}
]
}
}
},
"context": {
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": "OK"
},
"timeOfSample": "2024-02-03T16:15:00.00Z",
"uncertaintyInMilliseconds": 0
}
}

View file

@ -0,0 +1,30 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "StateReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "OFF",
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 0
}
]
}
}

View file

@ -0,0 +1,14 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "DeferredResponse",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"payload": {
"estimatedDeferralInSeconds": 15
}
}
}

View file

@ -0,0 +1,20 @@
{
"directive": {
"header": {
"namespace": "Alexa.PowerController",
"name": "TurnOn",
"messageId": "Unique version 4 UUID",
"correlationToken": "Opaque correlation token",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID",
"cookie": {}
},
"payload": {}
}
}

View file

@ -0,0 +1,30 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "Response",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "ON",
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 500
}
]
}
}

View file

@ -0,0 +1,30 @@
{
"event": {
"header": {
"namespace": "Alexa.WakeOnLANController",
"name": "WakeUp",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "OFF",
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 500
}
]
}
}

View file

@ -0,0 +1,70 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Manufacturer of the endpoint",
"description": "Description to be shown in the Alexa app",
"friendlyName": "Device name, displayed in the Alexa app",
"displayCategories": [
"TV"
],
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.WakeOnLANController",
"version": "3",
"properties": {},
"configuration": {
"MACAddresses": [
"00-14-22-01-23-45"
]
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [
{
"name": "powerState"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3",
"properties": {
"supported": [
{
"name": "connectivity"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -14,6 +14,7 @@
// unfit [[property, value]]: a value the property does not take
// chosen true: a declaration lists the properties the device has, an example some of those of the interface
// folded the directives whose example the page folds away, so that it was not saved
// endpoint { categories, description } of the endpoint the interface is declared on, where it has rules for them
//
// A directive is answered with a Response, or with the event responseFor() of its descriptor names.
//
@ -61,6 +62,12 @@ module.exports = [
unfit: [["colorTemperatureInKelvin", 10001], ["colorTemperatureInKelvin", "warm"]],
},
{ namespace: "Alexa.ContactSensor", page: "alexa-contactsensor", declared, unfit: [["detectionState", "OPEN"], ["detectionState", true]] },
{
namespace: "Alexa.DoorbellEventSource",
page: "alexa-doorbelleventsource",
endpoint: { categories: ["CAMERA", "DOORBELL"] },
declared: [{ options: {}, example: "discovery" }],
},
{ namespace: "Alexa.EndpointHealth", page: "alexa-endpointhealth", declared: [{ options: reported, example: "discovery" }] },
{
namespace: "Alexa.EqualizerController",
@ -100,6 +107,41 @@ module.exports = [
],
unfit: [["input", "hdmi 1"], ["input", 1]],
},
{
namespace: "Alexa.InventoryLevelSensor",
page: "alexa-inventorylevelsensor",
declared: [
{
example: "discovery",
options: {
retrievable: false,
proactivelyReported: true,
instance: "InkSensor.Cyan",
friendlyNames: [text("Cyan ink", "en-US"), text("Encre cyan", "fr-FR")],
measurement: { "@type": "Volume", unit: "MILLILITER" },
replenishment: { "@type": "DashReplenishmentId", value: "replenishment ID for refill options" },
},
},
{
example: "discovery",
n: 1,
options: {
retrievable: false,
proactivelyReported: true,
instance: "PaperSensor.FrontTray",
friendlyNames: [text("Front tray", "en-US")],
measurement: { "@type": "Count" },
replenishment: { "@type": "DashReplenishmentId", value: "replenishment ID for refill options" },
},
},
],
unfit: [
["level", 5],
["level", { "@type": "Volume", value: 5 }],
["level", { "@type": "Count", value: 200, unit: "SHEET" }],
["level", { "@type": "Length", value: 5, unit: "METER" }],
],
},
{
namespace: "Alexa.LockController",
page: "alexa-lockcontroller",
@ -175,6 +217,12 @@ module.exports = [
},
}],
},
{
namespace: "Alexa.SceneController",
page: "alexa-scenecontroller",
endpoint: { categories: ["SCENE_TRIGGER"], description: "Party scene by Sample Vendor" },
declared: [{ options: { supportsDeactivation: false }, example: "discovery" }],
},
{
namespace: "Alexa.SecurityPanelController",
page: "alexa-securitypanelcontroller",
@ -196,6 +244,8 @@ module.exports = [
],
unfit: [["armState", "ARMED"], ["burglaryAlarm", "ALARM"], ["fireAlarm", { value: "FIRE" }]],
},
// The page folds its discovery example away
{ namespace: "Alexa.SimpleEventSource", page: "alexa-simpleeventsource", declared: [] },
{
namespace: "Alexa.Speaker",
page: "alexa-speaker",
@ -270,6 +320,14 @@ module.exports = [
],
unfit: [["scheduleEnabled", "ON"], ["adaptiveRecoveryEnabled", 1]],
},
{
// The ChangeReport example of the page reports "2018-05-31T14:02.32Z", which is no time: it was not saved
namespace: "Alexa.TimeHoldController",
page: "alexa-timeholdcontroller",
declared: [{ options: { ...reported, allowRemoteResume: true }, example: "discovery" }],
refused: [["Hold", 10, "payload: expected an object, got 10"]],
unfit: [["holdStartTime", "14:30"], ["holdEndTime", "2018-05-31T14:40:00+02:00"]],
},
{
namespace: "Alexa.ToggleController",
page: "alexa-togglecontroller",
@ -314,4 +372,9 @@ module.exports = [
},
],
},
{
namespace: "Alexa.WakeOnLANController",
page: "alexa-wakeonlancontroller",
declared: [{ options: { macAddresses: ["00-14-22-01-23-45"] }, example: "discovery" }],
},
];

View file

@ -0,0 +1,136 @@
"use strict";
// Scenes, event sources and the interfaces declared with them: what the examples of their pages do not show. The
// examples themselves are run by descriptors.test.js, device/discovery.test.js and dispatch/interfaces.test.js.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const {
AlexaInterfaceType, DeclarationError, DoorbellEventSource, InventoryLevelSensor, SceneController, SimpleEventSource,
TimeHoldController, WakeOnLANController, asset, registry, text,
} = require("alex2node");
const { endpoint } = require("../helpers/endpoint.js");
const { doc } = require("../helpers/fixtures.js");
function scene(name = "Evening", categories = ["SCENE_TRIGGER"], description = "Evening scene by Alex2Node") {
const device = endpoint("scene-1", name, categories);
device.setDescription(description);
return device;
}
const refused = (device, descriptor, options, problem) => assert.throws(
() => device.add(descriptor, options),
(err) => err instanceof DeclarationError && err.problem === problem
);
test("the events of the pages fit the payload of their descriptor, and come back as they are", () => {
const printed = [
["Alexa.SceneController", "alexa-scenecontroller", "ActivationStarted", "response"],
["Alexa.SceneController", "alexa-scenecontroller", "DeactivationStarted", "response"],
["Alexa.DoorbellEventSource", "alexa-doorbelleventsource", "DoorbellPress", "proactive"],
["Alexa.SimpleEventSource", "alexa-simpleeventsource", "Event", "proactive"],
["Alexa.WakeOnLANController", "alexa-wakeonlancontroller", "WakeUp", "response"],
];
for (const [namespace, page, name, topic] of printed) {
const { header, payload } = doc(page, `${name}.event`).event;
const descriptor = registry.get(namespace);
const event = descriptor.events[name];
assert.deepEqual([header.namespace, header.name, header.payloadVersion], [namespace, event.name, descriptor.version]);
assert.equal(event.topic, topic, name);
assert.equal("correlationToken" in header, topic === "response", name);
assert.deepEqual(event.payload.parse(payload, "payload"), payload);
}
const names = registry.list().filter((descriptor) => descriptor.events).map((descriptor) => descriptor.namespace);
assert.deepEqual(names, [...new Set(printed.map(([namespace]) => namespace))].sort());
});
test("SceneController: a scene declared without options is announced as 1.5.2 announced it", () => {
const OF_1X = { type: "AlexaInterface", interface: "Alexa.SceneController", version: "3", supportsDeactivation: true, proactivelyReported: false };
assert.deepEqual(scene().addCapability(AlexaInterfaceType.SCENE_CONTROLLER).getJSON(), OF_1X);
assert.deepEqual(scene().add(SceneController).toJSON(), OF_1X);
assert.deepEqual(scene().add(SceneController, { supportsDeactivation: true }).toJSON(), {
type: "AlexaInterface", interface: "Alexa.SceneController", version: "3", supportsDeactivation: true,
});
});
test("SceneController: the endpoint of a scene has a category of scenes, the word scene in its description and a name of up to 128", () => {
const device = scene("Evening", ["ACTIVITY_TRIGGER", "OTHER"], "Evening Scene");
device.add(SceneController);
assert.deepEqual(device.check(), []);
refused(scene("Evening", ["LIGHT"]), SceneController, {}, "a scene has the display category SCENE_TRIGGER, or ACTIVITY_TRIGGER when the order of its steps matters");
refused(
scene("Evening", ["SCENE_TRIGGER"], "All lights off"),
SceneController,
{},
'the description of a scene has the word "scene" in it, like "Party scene connected by Alex2Node"'
);
refused(scene("a".repeat(129)), SceneController, {}, "the name of a scene takes up to 128 characters, this one has 129");
// Declared the 1.x way the scene is announced, and check() says what Alexa may not take
const old = scene("Evening", ["SCENE_TRIGGER"], "All lights off");
old.addCapability(AlexaInterfaceType.SCENE_CONTROLLER);
assert.deepEqual(old.check(), ['scene-1: Alexa.SceneController: the description of a scene has the word "scene" in it, like "Party scene connected by Alex2Node"']);
});
test("DoorbellEventSource: proactivelyReported on the capability, DOORBELL among the categories and CAMERA before it", () => {
const announced = { type: "AlexaInterface", interface: "Alexa.DoorbellEventSource", version: "3", proactivelyReported: true };
const bell = endpoint("door-1", "Door", ["DOORBELL"]);
assert.deepEqual(bell.add(DoorbellEventSource, { proactivelyReported: false }).toJSON(), announced);
assert.deepEqual(endpoint("door-1", "Door", ["DOORBELL"]).addCapability(AlexaInterfaceType.DOORBELL_EVENT_SOURCE).getJSON(), announced);
refused(endpoint("door-1", "Door", ["CAMERA"]), DoorbellEventSource, {}, "a doorbell has the display category DOORBELL");
refused(endpoint("door-1", "Door", ["DOORBELL", "CAMERA"]), DoorbellEventSource, {}, "a video doorbell lists the display category CAMERA before DOORBELL");
});
test("SimpleEventSource: version 1.0, an instance for each button with its events, no properties object", () => {
const remote = () => endpoint("remote-1", "Remote", ["REMOTE"]);
const single = { id: "Button.SinglePush.1", friendlyNames: [asset("Alexa.Button.SinglePush")] };
const double = { id: "Button.DoublePush.1", friendlyNames: [asset("Alexa.Button.DoublePush"), text("Twice")] };
const top = { instance: "Remote.TopButton", friendlyNames: [asset("Alexa.Button.TopButton")] };
assert.deepEqual(remote().add(SimpleEventSource, { ...top, supportedEvents: [single, double] }).toJSON(), {
type: "AlexaInterface",
interface: "Alexa.SimpleEventSource",
instance: "Remote.TopButton",
version: "1.0",
capabilityResources: { friendlyNames: top.friendlyNames },
configuration: { supportedEvents: [single, double] },
});
refused(remote(), SimpleEventSource, { supportedEvents: [single] }, "needs an instance name, like Blind.Lift");
refused(remote(), SimpleEventSource, { ...top, supportedEvents: [] }, "supportedEvents: expected a list with 1 or more entries, got []");
refused(remote(), SimpleEventSource, { ...top, supportedEvents: [single, single] }, "the event Button.SinglePush.1 is listed twice");
refused(
remote(),
SimpleEventSource,
{ ...top, supportedEvents: [single, { ...double, friendlyNames: single.friendlyNames }] },
"two events have Alexa.Button.SinglePush as their first friendly name, the Alexa app shows an event by it"
);
});
test("TimeHoldController, InventoryLevelSensor and WakeOnLANController: what a declaration has to say", () => {
const device = () => endpoint("device-1", "Device", ["OTHER"]);
refused(device(), TimeHoldController, {}, "allowRemoteResume: expected true or false, got nothing");
const ink = { instance: "Ink.Cyan", friendlyNames: [text("Cyan ink")], replenishment: { "@type": "DashReplenishmentId", value: "id-1" } };
refused(device(), InventoryLevelSensor, { ...ink, measurement: { "@type": "Volume" } }, "measurement.unit: expected the unit of the volume, like MILLILITER or GRAM, got nothing");
refused(device(), InventoryLevelSensor, { ...ink, measurement: { "@type": "Percentage", unit: "PERCENT" } }, 'measurement.unit: expected no unit for a percentage, got "PERCENT"');
refused(device(), InventoryLevelSensor, { ...ink, measurement: { "@type": "Count" }, replenishment: undefined }, "replenishment: expected an object with @type, value, got nothing");
assert.deepEqual(InventoryLevelSensor.properties.level.value.parse({ "@type": "Weight", value: 0.5, unit: "KILOGRAM" }), { "@type": "Weight", value: 0.5, unit: "KILOGRAM" });
assert.equal(WakeOnLANController.deferrable, true);
assert.deepEqual(device().add(WakeOnLANController, { macAddresses: ["00:14:22:01:23:45"] }).toJSON().configuration, { MACAddresses: ["00:14:22:01:23:45"] });
refused(device(), WakeOnLANController, {}, "macAddresses: expected a list with 1 or more entries, got nothing");
refused(device(), WakeOnLANController, { macAddresses: ["0014.2201.2345"] }, 'macAddresses[0]: expected a MAC address like 00-14-22-01-23-45, got "0014.2201.2345"');
refused(device(), WakeOnLANController, { macAddresses: ["00-14-22-01-23-ab", "00:14:22:01:23:AB"] }, "00-14-22-01-23-AB is listed twice");
// Declared the 1.x way none of the three can say it: they are announced, and check() lists what is missing
const old = device();
old.addCapability(AlexaInterfaceType.TIME_HOLD_CONTROLLER);
old.addCapability(AlexaInterfaceType.WAKE_ON_LAN_CONTROLLER);
assert.deepEqual(old.getJSON().capabilities.slice(0, 2), [
{ type: "AlexaInterface", interface: "Alexa.TimeHoldController", version: "3", properties: { supported: [{ name: "holdStartTime" }, { name: "holdEndTime" }], proactivelyReported: false, retrievable: true } },
{ type: "AlexaInterface", interface: "Alexa.WakeOnLANController", version: "3", properties: {} },
]);
assert.deepEqual(old.check(), [
"device-1: Alexa.TimeHoldController: allowRemoteResume: expected true or false, got nothing",
"device-1: Alexa.WakeOnLANController: macAddresses: expected a list with 1 or more entries, got nothing",
]);
});