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
106
dist/esm/registry/semantics.js
vendored
Normal file
106
dist/esm/registry/semantics.js
vendored
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// Semantics of a generic controller: the phrases "open", "close", "raise", "lower" mapped to directives, and states
|
||||
// like "open" and "closed" mapped to values of the property (alexa-discovery-objects.html, "Semantics object").
|
||||
import { ACTIONS, STATES } from "./catalog.js";
|
||||
import { at, mismatch, s, SchemaError } from "./schema.js";
|
||||
const listed = (given) => (Array.isArray(given) ? [...given] : [given]);
|
||||
const withPrefix = (prefix, name) => (name.startsWith(prefix) ? name : `${prefix}${name}`);
|
||||
/**
|
||||
* Collects the mappings of one capability:
|
||||
*
|
||||
* semantics()
|
||||
* .action("Close", "SetRangeValue", { rangeValue: 0 })
|
||||
* .action(["Open", "Raise"], "SetRangeValue", { rangeValue: 100 })
|
||||
* .state("Closed", 0)
|
||||
* .stateRange("Open", 1, 100)
|
||||
*
|
||||
* What is collected is checked when the capability is declared, against the directives and the property of its
|
||||
* interface.
|
||||
*/
|
||||
export class SemanticsBuilder {
|
||||
constructor() {
|
||||
this.actionMappings = [];
|
||||
this.stateMappings = [];
|
||||
}
|
||||
/** The phrases send the directive. payload is its payload; leave it out for a directive that has none. */
|
||||
action(actions, directive, payload) {
|
||||
this.actionMappings.push({
|
||||
"@type": "ActionsToDirective",
|
||||
actions: listed(actions).map((name) => withPrefix("Alexa.Actions.", name)),
|
||||
directive: payload === undefined ? { name: directive } : { name: directive, payload },
|
||||
});
|
||||
return this;
|
||||
}
|
||||
/** The property at this value is in these states: "is the garage door open?" */
|
||||
state(states, value) {
|
||||
this.stateMappings.push({ "@type": "StatesToValue", states: listed(states).map((name) => withPrefix("Alexa.States.", name)), value });
|
||||
return this;
|
||||
}
|
||||
/** The property between the two values, both included, is in these states. For an interface with a numeric property. */
|
||||
stateRange(states, minimumValue, maximumValue) {
|
||||
this.stateMappings.push({
|
||||
"@type": "StatesToRange",
|
||||
states: listed(states).map((name) => withPrefix("Alexa.States.", name)),
|
||||
range: { minimumValue, maximumValue },
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
export function semantics() {
|
||||
return new SemanticsBuilder();
|
||||
}
|
||||
const isHunch = (id) => /\.(SetEcoOn|SetEcoOff|EcoOn|EcoOff)$/.test(id);
|
||||
/** The schema of the semantics option of one interface. It takes a semantics() builder or the object as discovery has it. */
|
||||
export function semanticsOf({ directives, stateValue, ranges = false, hunches = false }) {
|
||||
const allowed = (ids, what) => {
|
||||
const choice = ids.filter((id) => hunches || !isHunch(id));
|
||||
return s.array(s.oneOf(choice, `${what}: ${choice.map((id) => id.split(".")[2]).join(", ")}`), { min: 1 });
|
||||
};
|
||||
const names = Object.keys(directives);
|
||||
const lists = s.object({ actionMappings: s.optional(s.array(s.unknown())), stateMappings: s.optional(s.array(s.unknown())) });
|
||||
const action = s.object({
|
||||
"@type": s.literal("ActionsToDirective"),
|
||||
actions: allowed(ACTIONS, "a phrase of Alexa.Actions"),
|
||||
directive: s.object({ name: s.oneOf(names, `a directive of the interface: ${names.join(", ")}`) }),
|
||||
});
|
||||
const state = s.object({
|
||||
"@type": ranges ? s.enum("StatesToValue", "StatesToRange") : s.enum("StatesToValue"),
|
||||
states: allowed(STATES, "a state of Alexa.States"),
|
||||
});
|
||||
const range = s.object({ minimumValue: s.number(), maximumValue: s.number() }, { unknownKeys: "reject" });
|
||||
function actionMapping(input, path) {
|
||||
const mapping = action.parse(input, path);
|
||||
const { name, payload } = mapping.directive;
|
||||
// Checked as the directive will arrive; a mapping without a payload stays without one in discovery
|
||||
const checked = directives[name].payload.parse(payload ?? {}, at(path, "directive.payload"));
|
||||
return { "@type": mapping["@type"], actions: mapping.actions, directive: payload === undefined ? { name } : { name, payload: checked } };
|
||||
}
|
||||
function stateMapping(input, path) {
|
||||
const mapping = state.parse(input, path);
|
||||
if (mapping["@type"] === "StatesToValue") {
|
||||
return { "@type": mapping["@type"], states: mapping.states, value: stateValue.parse(mapping.value, at(path, "value")) };
|
||||
}
|
||||
const { minimumValue, maximumValue } = range.parse(mapping.range, at(path, "range"));
|
||||
if (minimumValue > maximumValue)
|
||||
throw new SchemaError(at(path, "range"), `the minimumValue ${minimumValue} is above the maximumValue ${maximumValue}`);
|
||||
return { "@type": mapping["@type"], states: mapping.states, range: { minimumValue, maximumValue } };
|
||||
}
|
||||
const expects = "semantics() with at least one action or state";
|
||||
return {
|
||||
expects,
|
||||
parse(input, path = "") {
|
||||
if (typeof input !== "object" || input === null)
|
||||
throw mismatch(path, expects, input);
|
||||
const { actionMappings = [], stateMappings = [] } = lists.parse(input, path);
|
||||
if (actionMappings.length + stateMappings.length === 0)
|
||||
throw mismatch(path, expects, {});
|
||||
const parsed = {};
|
||||
if (actionMappings.length > 0) {
|
||||
parsed.actionMappings = actionMappings.map((mapping, i) => actionMapping(mapping, `${at(path, "actionMappings")}[${i}]`));
|
||||
}
|
||||
if (stateMappings.length > 0) {
|
||||
parsed.stateMappings = stateMappings.map((mapping, i) => stateMapping(mapping, `${at(path, "stateMappings")}[${i}]`));
|
||||
}
|
||||
return parsed;
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue