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:
parent
c492ef74d1
commit
2558e21787
80 changed files with 3131 additions and 107 deletions
104
test/registry/semantics.test.js
Normal file
104
test/registry/semantics.test.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"use strict";
|
||||
// semantics(): the mappings of phrases to directives and of states to values, as discovery lists them.
|
||||
const { test } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { Actions, AlexaActions, ModeController, RangeController, SchemaError, States, ToggleController, semantics } = require("alex2node");
|
||||
const { doc } = require("../helpers/fixtures.js");
|
||||
|
||||
// The schema of the semantics option of an interface, and the message of the SchemaError it throws.
|
||||
const semanticsOf = (descriptor) => ({
|
||||
parse: (input) => descriptor.options.parse({ ...COMPLETE[descriptor.namespace], semantics: input }).semantics,
|
||||
});
|
||||
const COMPLETE = {
|
||||
"Alexa.RangeController": { range: { min: 0, max: 100, precision: 1 } },
|
||||
"Alexa.ModeController": {
|
||||
supportedModes: ["Up", "Down"].map((name) => ({ value: `Position.${name}`, friendlyNames: [{ "@type": "text", value: { text: name, locale: "en-US" } }] })),
|
||||
},
|
||||
"Alexa.ToggleController": {},
|
||||
};
|
||||
function refusal(descriptor, input) {
|
||||
try {
|
||||
semanticsOf(descriptor).parse(input);
|
||||
} catch (err) {
|
||||
assert.ok(err instanceof SchemaError, `threw ${err}`);
|
||||
return err.message;
|
||||
}
|
||||
return assert.fail("the semantics were accepted");
|
||||
}
|
||||
|
||||
test("semantics() builds the Semantics object of alexa-discovery-objects.html", () => {
|
||||
const built = semantics()
|
||||
.action(["Close", "Lower"], "SetRangeValue", { rangeValue: 0 })
|
||||
.action(["Open", "Raise"], "SetRangeValue", { rangeValue: 100 })
|
||||
.state("Closed", 0)
|
||||
.stateRange("Open", 1, 100);
|
||||
const { semantics: example } = doc("alexa-discovery-objects", "semantics");
|
||||
assert.deepEqual(semanticsOf(RangeController).parse(built), example);
|
||||
assert.deepEqual(built.actionMappings[0], doc("alexa-discovery-objects", "actionMapping"));
|
||||
assert.deepEqual(built.stateMappings[1], doc("alexa-discovery-objects", "stateMapping"));
|
||||
// What the schema returns is the plain object, and it takes that object as it takes the builder
|
||||
assert.equal(Object.getPrototypeOf(semanticsOf(RangeController).parse(built)), Object.prototype);
|
||||
assert.deepEqual(semanticsOf(RangeController).parse(example), example);
|
||||
});
|
||||
|
||||
test("a phrase and a state are given by name, by id or by the AlexaActions enum of 1.x", () => {
|
||||
const { actionMappings, stateMappings } = semantics()
|
||||
.action("Open", "TurnOn")
|
||||
.action("Alexa.Actions.Close", "TurnOff")
|
||||
.action([AlexaActions.Raise, Actions[3]], "TurnOn")
|
||||
.state(["Closed", States[0]], "OFF");
|
||||
assert.deepEqual(actionMappings.map((mapping) => mapping.actions), [
|
||||
["Alexa.Actions.Open"], ["Alexa.Actions.Close"], ["Alexa.Actions.Raise", "Alexa.Actions.Lower"],
|
||||
]);
|
||||
assert.deepEqual(stateMappings[0].states, ["Alexa.States.Closed", "Alexa.States.Open"]);
|
||||
// No payload given, none in discovery; an empty one is kept, as in the ToggleController examples
|
||||
assert.deepEqual(actionMappings[0].directive, { name: "TurnOn" });
|
||||
assert.deepEqual(semantics().action("Open", "TurnOn", {}).actionMappings[0].directive, { name: "TurnOn", payload: {} });
|
||||
assert.deepEqual(semanticsOf(ToggleController).parse(semantics().action("Open", "TurnOn")).actionMappings[0].directive, { name: "TurnOn" });
|
||||
});
|
||||
|
||||
test("a mapping is checked against the directives and the property of the interface", () => {
|
||||
const cases = [
|
||||
[RangeController, semantics().action("Open", "SetRange", { rangeValue: 100 }),
|
||||
'semantics.actionMappings[0].directive.name: expected a directive of the interface: SetRangeValue, AdjustRangeValue, got "SetRange"'],
|
||||
[RangeController, semantics().action("Open", "SetRangeValue", { rangeValue: "100" }),
|
||||
'semantics.actionMappings[0].directive.payload.rangeValue: expected a number, got "100"'],
|
||||
[RangeController, semantics().action("Open", "SetRangeValue"),
|
||||
"semantics.actionMappings[0].directive.payload.rangeValue: expected a number, got nothing"],
|
||||
[RangeController, semantics().action("Lower", "AdjustRangeValue", { rangeValueDelta: -10 }),
|
||||
"semantics.actionMappings[0].directive.payload.rangeValueDeltaDefault: expected true or false, got nothing"],
|
||||
[RangeController, semantics().action("Shut", "SetRangeValue", { rangeValue: 0 }),
|
||||
'semantics.actionMappings[0].actions[0]: expected a phrase of Alexa.Actions: Open, Close, Raise, Lower, got "Alexa.Actions.Shut"'],
|
||||
[RangeController, semantics().state("Ajar", 50),
|
||||
'semantics.stateMappings[0].states[0]: expected a state of Alexa.States: Open, Closed, Low, Empty, Full, Done, Stuck, got "Alexa.States.Ajar"'],
|
||||
[RangeController, semantics().state("Closed", "OFF"), 'semantics.stateMappings[0].value: expected a number, got "OFF"'],
|
||||
[RangeController, semantics().stateRange("Open", 100, 1), "semantics.stateMappings[0].range: the minimumValue 100 is above the maximumValue 1"],
|
||||
[ToggleController, semantics().state("Open", "on"), 'semantics.stateMappings[0].value: expected ON | OFF, got "on"'],
|
||||
[ToggleController, semantics().stateRange("Open", 1, 100), 'semantics.stateMappings[0].@type: expected StatesToValue, got "StatesToRange"'],
|
||||
[ModeController, semantics().state("Closed", 0), "semantics.stateMappings[0].value: expected a string of 1 or more characters, got 0"],
|
||||
[ToggleController, semantics(), "semantics: expected semantics() with at least one action or state, got {}"],
|
||||
[ToggleController, "open", 'semantics: expected semantics() with at least one action or state, got "open"'],
|
||||
[ToggleController, { actionMappings: [{ "@type": "ActionToDirective", actions: ["Alexa.Actions.Open"], directive: { name: "TurnOn" } }] },
|
||||
'semantics.actionMappings[0].@type: expected "ActionsToDirective", got "ActionToDirective"'],
|
||||
];
|
||||
for (const [descriptor, input, message] of cases) assert.equal(refusal(descriptor, input), message);
|
||||
});
|
||||
|
||||
test("the phrases and states of Alexa hunches are for a ToggleController", () => {
|
||||
const eco = semantics().action("SetEcoOn", "TurnOn").action("SetEcoOff", "TurnOff").state("EcoOn", "ON").state("EcoOff", "OFF");
|
||||
assert.deepEqual(semanticsOf(ToggleController).parse(eco), {
|
||||
actionMappings: [
|
||||
{ "@type": "ActionsToDirective", actions: ["Alexa.Actions.SetEcoOn"], directive: { name: "TurnOn" } },
|
||||
{ "@type": "ActionsToDirective", actions: ["Alexa.Actions.SetEcoOff"], directive: { name: "TurnOff" } },
|
||||
],
|
||||
stateMappings: [
|
||||
{ "@type": "StatesToValue", states: ["Alexa.States.EcoOn"], value: "ON" },
|
||||
{ "@type": "StatesToValue", states: ["Alexa.States.EcoOff"], value: "OFF" },
|
||||
],
|
||||
});
|
||||
assert.match(
|
||||
refusal(RangeController, semantics().action("SetEcoOn", "SetRangeValue", { rangeValue: 20 })),
|
||||
/^semantics\.actionMappings\[0\]\.actions\[0\]: expected a phrase of Alexa\.Actions: Open, Close, Raise, Lower, got "Alexa\.Actions\.SetEcoOn"$/
|
||||
);
|
||||
assert.match(refusal(ModeController, semantics().state("EcoOn", "Position.Up")), /expected a state of Alexa\.States: Open, Closed, Low, Empty, Full, Done, Stuck, got/);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue