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>
136 lines
9 KiB
JavaScript
136 lines
9 KiB
JavaScript
"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",
|
|
]);
|
|
});
|