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

@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DoorbellEventSource = void 0;
const events_js_1 = require("../events.js");
const types_js_1 = require("../types.js");
/**
* A doorbell. It takes no directive and reports nothing; device.raise(DoorbellEventSource, "DoorbellPress") says
* that somebody rang. Alexa wants 30 seconds between two presses of one doorbell, and announces a press when the
* user switched the announcements on for the doorbell in the Alexa app.
*/
exports.DoorbellEventSource = (0, types_js_1.defineInterface)({
namespace: "Alexa.DoorbellEventSource",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-doorbelleventsource.html",
kind: "eventSource",
tier: 2,
instanced: false,
properties: {},
directives: {},
events: {
DoorbellPress: { name: "DoorbellPress", payload: (0, events_js_1.happened)("PHYSICAL_INTERACTION"), topic: "proactive" },
},
// The capability is the announcement that the endpoint raises the event: proactivelyReported whatever was declared
discovery: () => ({ properties: false, topLevel: { proactivelyReported: true } }),
// alexa-doorbelleventsource.html, "Discovery"
validate(capability, { displayCategories }) {
const doorbell = displayCategories.indexOf("DOORBELL");
if (doorbell < 0)
throw new types_js_1.DeclarationError(capability, "a doorbell has the display category DOORBELL");
if (displayCategories.indexOf("CAMERA") > doorbell) {
throw new types_js_1.DeclarationError(capability, "a video doorbell lists the display category CAMERA before DOORBELL");
}
},
});

View file

@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InventoryLevelSensor = void 0;
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
// As the examples of the page spell them; its table has them in lower case
const KINDS = ["Count", "Percentage", "Volume", "Weight"];
// A volume and a weight have a unit: MILLILITER, GRAM (alexa-property-schemas.html, "Volume unit values" and
// "Weight unit values")
function withUnit(fields) {
return {
expects: fields.expects,
parse(input, path = "") {
const measured = fields.parse(input, path);
const what = measured["@type"].toLowerCase();
const needed = what === "volume" || what === "weight";
if (needed && measured.unit === undefined)
throw (0, schema_js_1.mismatch)((0, schema_js_1.at)(path, "unit"), `the unit of the ${what}, like MILLILITER or GRAM`, undefined);
if (!needed && measured.unit !== undefined)
throw (0, schema_js_1.mismatch)((0, schema_js_1.at)(path, "unit"), `no unit for a ${what}`, measured.unit);
return measured;
},
};
}
const kind = schema_js_1.s.enum(...KINDS);
const measurement = withUnit(schema_js_1.s.object({ "@type": kind, unit: schema_js_1.s.optional(schema_js_1.s.string({ min: 1 })) }, { unknownKeys: "reject" }));
const level = withUnit(schema_js_1.s.object({ "@type": kind, value: schema_js_1.s.number({ min: 0 }), unit: schema_js_1.s.optional(schema_js_1.s.string({ min: 1 })) }));
/**
* How much is left of something a device uses up: ink, paper, detergent. Each instance is one sensor, and Amazon
* orders a refill by its Dash replenishment id. The level is reported, there is nothing to ask for by voice.
*/
exports.InventoryLevelSensor = (0, types_js_1.defineInterface)({
namespace: "Alexa.InventoryLevelSensor",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-inventorylevelsensor.html",
kind: "sensor",
tier: 2,
instanced: true,
properties: {
level: { name: "level", value: level, note: "measured as the capability declares it" },
},
directives: {},
options: schema_js_1.s.object({
/** How the level is measured: { "@type": "Volume", unit: "MILLILITER" }, { "@type": "Count" }. */
measurement,
/** The id the Dash console gave for the product. */
replenishment: schema_js_1.s.object({ "@type": schema_js_1.s.enum("DashReplenishmentId"), value: schema_js_1.s.string({ min: 1 }) }, { unknownKeys: "reject" }),
}, { unknownKeys: "reject" }),
// A capability declared the 1.x way has neither; check() says so
discovery({ options: { measurement: measured, replenishment } }) {
const configuration = { ...(measured && { measurement: measured }), ...(replenishment && { replenishment }) };
return Object.keys(configuration).length > 0 ? { configuration } : {};
},
});

View file

@ -0,0 +1,67 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SceneController = void 0;
const catalog_js_1 = require("../catalog.js");
const events_js_1 = require("../events.js");
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
const started = (name) => ({ name, payload: (0, events_js_1.happened)("VOICE_INTERACTION"), topic: "response" });
const events = {
ActivationStarted: started("ActivationStarted"),
DeactivationStarted: started("DeactivationStarted"),
};
/**
* A scene: an endpoint that is not a device, but several devices set to a state each. Activate is answered with
* ActivationStarted and Deactivate with DeactivationStarted; ctx.respond() sends them, with the time and
* VOICE_INTERACTION as the cause. A scene reports nothing and has no Alexa.EndpointHealth.
*
* Alexa takes no scene with a lock, a garage door, a camera, a cooking appliance or a security device in it.
*/
exports.SceneController = (0, types_js_1.defineInterface)({
namespace: "Alexa.SceneController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-scenecontroller.html",
kind: "controller",
tier: 1,
instanced: false,
properties: {},
directives: {
Activate: { name: "Activate", payload: schema_js_1.s.object({}) },
Deactivate: {
name: "Deactivate",
payload: schema_js_1.s.object({}),
when: ({ options }) => options.supportsDeactivation !== false,
},
},
events,
options: schema_js_1.s.object({
/** The scene can be switched off again. */
supportsDeactivation: schema_js_1.s.optional(schema_js_1.s.boolean()),
}, { unknownKeys: "reject" }),
// A scene declared without the option is announced as 1.5.2 announced every scene, which Alexa accepted: it can
// be deactivated, and proactivelyReported is on the capability. The example of the page has supportsDeactivation
// alone.
discovery({ proactivelyReported, options: { supportsDeactivation } }) {
const topLevel = supportsDeactivation === undefined ? { supportsDeactivation: true, proactivelyReported } : { supportsDeactivation };
return { properties: false, topLevel };
},
// alexa-scenecontroller.html, "Discovery"
validate(capability, endpoint) {
const { friendlyName, description, displayCategories } = endpoint;
if (!displayCategories.includes("SCENE_TRIGGER") && !displayCategories.includes("ACTIVITY_TRIGGER")) {
throw new types_js_1.DeclarationError(capability, "a scene has the display category SCENE_TRIGGER, or ACTIVITY_TRIGGER when the order of its steps matters");
}
if (!/scene/i.test(description)) {
throw new types_js_1.DeclarationError(capability, 'the description of a scene has the word "scene" in it, like "Party scene connected by Alex2Node"');
}
if (friendlyName.length > catalog_js_1.LIMITS.sceneFriendlyNameLength) {
throw new types_js_1.DeclarationError(capability, `the name of a scene takes up to ${catalog_js_1.LIMITS.sceneFriendlyNameLength} characters, this one has ${friendlyName.length}`);
}
},
responseFor(directive) {
if (directive !== "Activate" && directive !== "Deactivate")
return undefined;
const { name, payload } = directive === "Activate" ? events.ActivationStarted : events.DeactivationStarted;
return { namespace: "Alexa.SceneController", name, payload };
},
});

View file

@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SimpleEventSource = void 0;
const events_js_1 = require("../events.js");
const resources_js_1 = require("../resources.js");
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
/**
* A button of a remote, or anything else that routines start on. Each instance is one button with the events it
* has, a single and a double push; device.raise(SimpleEventSource, "Event", { id }, { instance }) says that one
* happened. Alexa wants the instance to be unique among all endpoints, "preferably a version 4 UUID".
*
* The page folds its discovery example away. The capability is announced with the fields of its table and without a
* properties object, as the interface has no property.
*/
exports.SimpleEventSource = (0, types_js_1.defineInterface)({
namespace: "Alexa.SimpleEventSource",
version: "1.0",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-simpleeventsource.html",
kind: "eventSource",
tier: 2,
instanced: true,
properties: {},
directives: {},
events: {
Event: {
name: "Event",
/** id: the id of one of the supportedEvents of the instance. */
payload: schema_js_1.s.object({ id: schema_js_1.s.string({ min: 1 }), timestamp: events_js_1.timestamp }),
topic: "proactive",
},
},
options: schema_js_1.s.object({
/** For a REMOTE the names are assets of the catalog: Alexa.Button.SinglePush, Alexa.Gesture.Tap. */
supportedEvents: schema_js_1.s.array(schema_js_1.s.object({ id: schema_js_1.s.string({ min: 1 }), friendlyNames: resources_js_1.labels }, { unknownKeys: "reject" }), { min: 1 }),
}, { unknownKeys: "reject" }),
// A capability declared the 1.x way has no events; check() says so
discovery: ({ options: { supportedEvents } }) => ({
properties: false,
...(supportedEvents && { configuration: { supportedEvents } }),
}),
validate(capability) {
const { supportedEvents } = capability.options;
const ids = supportedEvents.map(({ id }) => id);
// "The first friendly name in the array must be unique per instance of the capability."
const names = supportedEvents.map(({ friendlyNames: [first] }) => (0, resources_js_1.shownAs)(first));
const twice = (list) => list.find((entry, i) => list.indexOf(entry) !== i);
if (twice(ids))
throw new types_js_1.DeclarationError(capability, `the event ${twice(ids)} is listed twice`);
if (twice(names))
throw new types_js_1.DeclarationError(capability, `two events have ${twice(names)} as their first friendly name, the Alexa app shows an event by it`);
},
});

View file

@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TimeHoldController = void 0;
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
/**
* Pausing what a device does, a microwave that heats, and going on with it. Amazon pairs the interface with
* Alexa.Cooking. Alexa sends Resume to a device declared with allowRemoteResume only; for another one it asks the
* user to press start on the device.
*/
exports.TimeHoldController = (0, types_js_1.defineInterface)({
namespace: "Alexa.TimeHoldController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-timeholdcontroller.html",
kind: "controller",
tier: 2,
instanced: false,
properties: {
holdStartTime: { name: "holdStartTime", value: schema_js_1.s.dateTime() },
holdEndTime: { name: "holdEndTime", value: schema_js_1.s.dateTime() },
},
directives: {
Hold: { name: "Hold", payload: schema_js_1.s.object({}) },
Resume: { name: "Resume", payload: schema_js_1.s.object({}), when: ({ options }) => options.allowRemoteResume !== false },
},
options: schema_js_1.s.object({
allowRemoteResume: schema_js_1.s.boolean(),
}, { unknownKeys: "reject" }),
// A capability declared the 1.x way has no allowRemoteResume; check() says so
discovery: ({ options: { allowRemoteResume } }) => (allowRemoteResume === undefined ? {} : { configuration: { allowRemoteResume } }),
});

View file

@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WakeOnLANController = void 0;
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
// 00-14-22-01-23-45 in the example of the page; the colon is the other common way to write one
const MAC_ADDRESS = /^[0-9A-Fa-f]{2}([-:][0-9A-Fa-f]{2}){5}$/;
/**
* A device that is switched on by a Wake-on-LAN message, which an Echo of the user sends to the MAC addresses of
* the capability. The interface has no directive and no property: the device gets TurnOn of Alexa.PowerController,
* answers with a DeferredResponse, sends the WakeUp event and, when it is up, the Response.
*
* Alex2MQTT has no topic yet that takes the WakeUp event to Alexa.
*/
exports.WakeOnLANController = (0, types_js_1.defineInterface)({
namespace: "Alexa.WakeOnLANController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-wakeonlancontroller.html",
kind: "controller",
tier: 2,
instanced: false,
properties: {},
directives: {},
events: {
WakeUp: { name: "WakeUp", payload: schema_js_1.s.object({}), topic: "response" },
},
options: schema_js_1.s.object({
macAddresses: schema_js_1.s.array(schema_js_1.s.string({ pattern: MAC_ADDRESS, expects: "a MAC address like 00-14-22-01-23-45" }), { min: 1 }),
}, { unknownKeys: "reject" }),
// The example has a properties object without fields, which topLevel puts where properties: false left none. A
// capability declared the 1.x way has no address; check() says so.
discovery: ({ options: { macAddresses } }) => ({
properties: false,
topLevel: { properties: {} },
...(macAddresses && { configuration: { MACAddresses: macAddresses } }),
}),
validate(capability) {
const addresses = capability.options.macAddresses.map((address) => address.toUpperCase().replace(/:/g, "-"));
const twice = addresses.find((address, i) => addresses.indexOf(address) !== i);
if (twice)
throw new types_js_1.DeclarationError(capability, `${twice} is listed twice`);
},
deferrable: true,
defers: [{ namespace: "Alexa.PowerController", name: "TurnOn" }],
});

View file

@ -32,8 +32,6 @@ const TABLE = [
["Alexa.DataController", "1", [], "alexa-datacontroller.html"],
["Alexa.DeviceUsage.Estimation", "1", [], "alexa-deviceusage-estimation.html"],
["Alexa.DeviceUsage.Meter", "1", [], "alexa-deviceusage-meter.html"],
["Alexa.DoorbellEventSource", "3", [], "alexa-doorbelleventsource.html"],
["Alexa.InventoryLevelSensor", "1", [], "alexa-inventorylevelsensor.html"],
["Alexa.InventoryLevelUsageSensor", "1", [], "alexa-inventorylevelusagesensor.html"],
["Alexa.InventoryUsageSensor", "1", [], "alexa-inventoryusagesensor.html"],
["Alexa.KeypadController", "1", [], "alexa-keypadcontroller.html"],
@ -45,29 +43,16 @@ const TABLE = [
["Alexa.RTCSessionController", "1", [], "alexa-rtcsessioncontroller.html"],
["Alexa.RecordController", "3", [], "alexa-recordcontroller.html"],
["Alexa.RemoteVideoPlayer", "1", [], "alexa-remotevideoplayer.html"],
["Alexa.SceneController", "3", [], "alexa-scenecontroller.html"],
["Alexa.SecurityPanelController.Alert", "1", [], "alexa-securitypanelcontroller-alert.html"],
["Alexa.SeekController", "3", [], "alexa-seekcontroller.html"],
["Alexa.SimpleEventSource", "1", [], "alexa-simpleeventsource.html"],
["Alexa.SmartVision.ObjectDetectionSensor", "1", [], "alexa-smartvision-objectdetectionsensor.html"],
["Alexa.SmartVision.SnapshotProvider", "1", [], "alexa-smartvision-snapshotprovider.html"],
["Alexa.ThermostatController.Configuration", "1", [], "alexa-thermostatcontroller-configuration.html"],
["Alexa.ThermostatController.HVAC.Components", "1", [], "alexa-thermostatcontroller-hvac-components.html"],
// "UNKNOWN" in 1.5.2 as well; the page is titled "Interface 3"
["Alexa.TimeHoldController", "3", [], "alexa-timeholdcontroller.html"],
["Alexa.UIController", "1", [], "alexa-uicontroller.html"],
["Alexa.UserPreference", "1", [], "alexa-userpreference.html"],
["Alexa.VideoRecorder", "1", [], "alexa-videorecorder.html"],
["Alexa.WakeOnLANController", "1", [], "alexa-wakeonlancontroller.html"],
];
// What 1.5.2 added to the capability object of one of them
const EXTRAS = {
// No properties object; supportsDeactivation and proactivelyReported on the capability itself
"Alexa.SceneController": ({ proactivelyReported }) => ({
properties: false,
topLevel: { supportsDeactivation: true, proactivelyReported },
}),
};
function kindOf(namespace) {
if (namespace.endsWith("Sensor"))
return "sensor";
@ -85,7 +70,6 @@ function stub([namespace, version, properties, page]) {
instanced: false,
properties: Object.fromEntries(properties.map((name) => [name, { name, value: schema_js_1.s.unknown() }])),
directives: {},
discovery: EXTRAS[namespace],
};
}
exports.STUBS = TABLE.map(stub);