registry: RangeController, ModeController, ToggleController with resources, presets and semantics

The three generic controllers are described in full and leave the stub table (62 stubs remain). A declaration
takes an instance name, friendly names from text() and asset(), and the options of the interface: range, unit and
presets for a range; supportedModes and ordered for a mode; semantics() for all three, with actions mapped to
directives and states mapped to a value or a range of values.

Refused at device.add(), each with the endpoint, interface and instance in the message: no instance name or no
friendly name; an instance declared twice; a first friendly name or a phrase used by another capability of the
endpoint; a reserved word, an asset id that is not in the catalog; range.max not above range.min; a preset off the
precision grid or outside the range; fewer than two modes or a mode listed twice; a mapping to a directive the
interface does not have, to a payload that does not fit, to a mode that is not listed, to AdjustMode on modes
that are not ordered; SetEcoOn, SetEcoOff, EcoOn and EcoOff on anything but a toggle.

For 1.x callers: Alexa.RangeController lists rangeValue (1.5.2 sent supported: []); addSupportedModes() announces
a mode given as a string as { value } and notes it, and takes { ordered: false } as a second argument, the default
stays true as 1.5.2 sent it (device.add() defaults to false). Declared the 1.x way, the zoo fan and curtain give
the same JSON as before.

Tests: the zoo blind, whose range, unit and semantics were written by hand into the JSON of 1.5.2, is declared
with device.add() and equals that JSON plus the Alexa capability; five capability objects of Amazon's examples
are reproduced the same way. 27 examples of four pages added. npm test: 108 pass (was 85) in 10.5-11.3 s, also on
Node 18.20.8 and 20.20.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 15:50:25 +00:00
parent c492ef74d1
commit 2558e21787
80 changed files with 3131 additions and 107 deletions

View file

@ -30,16 +30,20 @@ test("a descriptor of tier 1 or 2 has an entry in test/fixtures/interfaces.js, a
assert.deepEqual(described.map((descriptor) => descriptor.namespace), interfaces.map((entry) => entry.namespace).sort());
});
for (const { namespace, page } of interfaces) {
for (const { namespace, page, declared } of interfaces) {
test(`${namespace}: version, properties, directive payloads and property values of ${page}.html`, () => {
const descriptor = registry.get(namespace);
assert.ok(descriptor.doc.endsWith(`/${page}.html`), descriptor.doc);
for (const name of examples(page).filter((example) => example.startsWith("discovery"))) {
for (const capability of capabilities(page, namespace, name)) {
assert.equal(capability.version, descriptor.version, `${name}: version`);
// The discovery examples of the page, and of another page when the entry declares from one
const discovery = examples(page).filter((example) => example.startsWith("discovery")).map((example) => [page, example]);
for (const { page: other = page, example } of declared) discovery.push([other, example]);
for (const [from, example] of discovery) {
for (const capability of capabilities(from, namespace, example)) {
assert.equal(capability.version, descriptor.version, `${example}: version`);
assert.equal("instance" in capability, descriptor.instanced, `${example}: instance`);
const supported = capability.properties ? capability.properties.supported.map((property) => property.name) : [];
for (const property of supported) assert.ok(property in descriptor.properties, `${name}: ${property}`);
assert.deepEqual(supported, Object.keys(descriptor.properties), `${example}: properties`);
}
}
@ -64,6 +68,19 @@ for (const { namespace, page } of interfaces) {
});
}
test("the error types of an interface are in the error type table under its error namespace", () => {
const { ERROR_TYPES } = require("../../dist/cjs/registry/catalog.js");
const withErrors = registry.list().filter((descriptor) => descriptor.errorNamespace || descriptor.errorTypes);
// The generic controllers answer a safety problem under Alexa.Safety
for (const name of ["Alexa.ModeController", "Alexa.RangeController", "Alexa.ToggleController"]) {
assert.deepEqual(registry.get(name).errorTypes, ["OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED"], name);
}
for (const { namespace, errorNamespace, errorTypes } of withErrors) {
assert.ok(errorTypes.length > 0, namespace);
for (const type of errorTypes) assert.equal(ERROR_TYPES[type], errorNamespace, `${namespace}: ${type}`);
}
});
test("Alexa.EndpointHealth is version 3.1 and reports connectivity, with a reason when it has one", () => {
const { version, properties } = registry.get("Alexa.EndpointHealth");
assert.equal(version, "3.1");