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
|
|
@ -5,7 +5,7 @@ const { test } = require("node:test");
|
|||
const assert = require("node:assert/strict");
|
||||
const {
|
||||
Alex2MQTT, ActionMapping, Alexa, AlexaActions, AlexaInterfaceType, BrightnessController, DisplayCategory, EndpointHealth,
|
||||
PowerController, TemperatureSensor, registry,
|
||||
ModeController, PowerController, RangeController, TemperatureSensor, ToggleController, asset, registry, semantics, text,
|
||||
} = require("alex2node");
|
||||
const interfaces = require("../fixtures/interfaces.js");
|
||||
const { endpoint } = require("../helpers/endpoint.js");
|
||||
|
|
@ -23,13 +23,26 @@ async function discover(alexa, root) {
|
|||
return alexa.on(`${root}/discover_r`)[before];
|
||||
}
|
||||
|
||||
for (const { namespace, page, declared } of interfaces) {
|
||||
test(`${namespace}: device.add() gives the capability object of ${page}.html`, () => {
|
||||
for (const { options, as: [example, n = 0] } of declared) {
|
||||
const capability = endpoint().add(registry.get(namespace), options);
|
||||
assert.deepEqual(capability.toJSON(), capabilities(page, namespace, example)[n], `${example}[${n}]`);
|
||||
}
|
||||
});
|
||||
// A device with the fields of a zoo endpoint and no capabilities.
|
||||
function likeTheZoo(endpointId) {
|
||||
const accepted = zoo(endpointId);
|
||||
const device = endpoint(endpointId, accepted.friendlyName, accepted.displayCategories);
|
||||
device.setDescription(accepted.description);
|
||||
device.setManufacturerName(accepted.manufacturerName);
|
||||
device.setManufacturer(accepted.additionalAttributes.manufacturer);
|
||||
device.setModel(accepted.additionalAttributes.model);
|
||||
return device;
|
||||
}
|
||||
|
||||
for (const { namespace, page: own, declared } of interfaces) {
|
||||
for (const { options, example, page = own, n = 0 } of declared) {
|
||||
test(`${namespace}: device.add() gives the capability object of ${page}.html, ${example}${n > 0 ? ` (${n})` : ""}`, () => {
|
||||
const device = endpoint();
|
||||
const capability = device.add(registry.get(namespace), options);
|
||||
assert.deepEqual(capability.toJSON(), capabilities(page, namespace, example)[n]);
|
||||
assert.deepEqual(device.check(), []);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test("the fields of a capability object are in the order of the examples: type, interface, version, properties", () => {
|
||||
|
|
@ -111,17 +124,115 @@ test("1.x declarations are announced as 1.5.2 announced them, with the Alexa int
|
|||
};
|
||||
for (const [endpointId, declare] of Object.entries(declarations)) {
|
||||
const accepted = zoo(endpointId);
|
||||
const device = endpoint(endpointId, accepted.friendlyName, accepted.displayCategories);
|
||||
device.setDescription(accepted.description);
|
||||
device.setManufacturerName(accepted.manufacturerName);
|
||||
device.setManufacturer(accepted.additionalAttributes.manufacturer);
|
||||
device.setModel(accepted.additionalAttributes.model);
|
||||
const device = likeTheZoo(endpointId);
|
||||
declare(device);
|
||||
assert.deepEqual(device.getJSON(), { ...accepted, capabilities: [...accepted.capabilities, ALEXA] }, endpointId);
|
||||
assert.deepEqual(device.check(), [], endpointId);
|
||||
}
|
||||
});
|
||||
|
||||
test("a RangeController blind declared with device.add() is the zoo's blind, whose JSON was written by hand", () => {
|
||||
const blinds = likeTheZoo("zoo-blinds");
|
||||
blinds.add(RangeController, {
|
||||
instance: "Blind.Lift",
|
||||
friendlyNames: [asset("Alexa.Setting.Opening")],
|
||||
range: { min: 0, max: 100, precision: 1 },
|
||||
unit: "Alexa.Unit.Percent",
|
||||
semantics: semantics()
|
||||
.action("Close", "SetRangeValue", { rangeValue: 0 })
|
||||
.action("Open", "SetRangeValue", { rangeValue: 100 })
|
||||
.action("Lower", "AdjustRangeValue", { rangeValueDelta: -10, rangeValueDeltaDefault: false })
|
||||
.action("Raise", "AdjustRangeValue", { rangeValueDelta: 10, rangeValueDeltaDefault: false })
|
||||
.state("Closed", 0)
|
||||
.stateRange("Open", 1, 100),
|
||||
retrievable: true,
|
||||
proactivelyReported: true,
|
||||
});
|
||||
const accepted = zoo("zoo-blinds");
|
||||
assert.deepEqual(blinds.getJSON(), { ...accepted, capabilities: [...accepted.capabilities, ALEXA] });
|
||||
assert.deepEqual(blinds.check(), []);
|
||||
});
|
||||
|
||||
test("the zoo's fan and curtain declared with device.add(): a ModeController and a ToggleController with semantics", () => {
|
||||
const fan = likeTheZoo("zoo-fan");
|
||||
fan.add(PowerController, { proactivelyReported: true });
|
||||
fan.add(ModeController, {
|
||||
instance: "Fan.Speed",
|
||||
friendlyNames: [text("Speed")],
|
||||
supportedModes: ["Low", "Medium", "High"].map((name) => ({ value: `Speed.${name}`, friendlyNames: [text(name)] })),
|
||||
ordered: true,
|
||||
proactivelyReported: true,
|
||||
});
|
||||
assert.deepEqual(fan.getJSON().capabilities, [...zoo("zoo-fan").capabilities, ALEXA]);
|
||||
|
||||
const curtain = likeTheZoo("zoo-curtain");
|
||||
curtain.add(PowerController, { proactivelyReported: true });
|
||||
curtain.add(ToggleController, {
|
||||
instance: "Zoo.Shade",
|
||||
friendlyNames: [text("Shade")],
|
||||
semantics: semantics().action("Close", "TurnOn").action("Open", "TurnOff"),
|
||||
proactivelyReported: true,
|
||||
});
|
||||
assert.deepEqual(curtain.getJSON().capabilities, [...zoo("zoo-curtain").capabilities, ALEXA]);
|
||||
});
|
||||
|
||||
test("ModeController: the modes are not ordered unless the declaration says so; a mode is declared in either form", () => {
|
||||
const modes = endpoint("washer-1", "Washer", ["WASHER"]).add(ModeController, {
|
||||
instance: "Washer.WashCycle",
|
||||
friendlyNames: [asset("Alexa.Setting.WashCycle")],
|
||||
supportedModes: [
|
||||
{ value: "WashCycle.Normal", friendlyNames: [text("Normal"), text("Cottons")] },
|
||||
{ value: "WashCycle.Delicates", modeResources: { friendlyNames: [asset("Alexa.Value.Delicate"), text("Knits")] } },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(modes.toJSON().configuration, {
|
||||
ordered: false,
|
||||
supportedModes: [
|
||||
{ value: "WashCycle.Normal", modeResources: { friendlyNames: [text("Normal"), text("Cottons")] } },
|
||||
{ value: "WashCycle.Delicates", modeResources: { friendlyNames: [asset("Alexa.Value.Delicate"), text("Knits")] } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("1.x addSupportedModes: ordered as 1.x announced it, or as the second argument says; a string becomes { value }", () => {
|
||||
const fan = endpoint("fan-1", "Fan", ["FAN"]);
|
||||
const named = (value) => ({ value, modeResources: { friendlyNames: [{ "@type": "text", value: { text: value, locale: "en-US" } }] } });
|
||||
const speed = fan.addCapability(AlexaInterfaceType.MODE_CONTROLLER, { instance: "Fan.Speed" });
|
||||
speed.addFriendlyName("Speed", "en-US");
|
||||
speed.addSupportedModes([named("Low"), named("High")]);
|
||||
assert.equal(speed.getJSON().configuration.ordered, true);
|
||||
|
||||
const source = fan.addCapability(AlexaInterfaceType.MODE_CONTROLLER, { instance: "Fan.Mode" });
|
||||
source.addFriendlyName("Mode", "en-US");
|
||||
source.addSupportedModes([named("Auto"), named("On")], { ordered: false });
|
||||
assert.deepEqual(source.getJSON().configuration, { ordered: false, supportedModes: [named("Auto"), named("On")] });
|
||||
assert.deepEqual(fan.check(), []);
|
||||
|
||||
const plain = fan.addCapability(AlexaInterfaceType.MODE_CONTROLLER, { instance: "Fan.Timer" });
|
||||
plain.addFriendlyName("Countdown", "en-US");
|
||||
plain.addSupportedModes(["Short", named("Long")]);
|
||||
assert.deepEqual(plain.getJSON().configuration.supportedModes, [{ value: "Short" }, named("Long")]);
|
||||
assert.deepEqual(fan.check(), [
|
||||
'fan-1: Alexa.ModeController "Fan.Timer": supportedModes[0].friendlyNames: expected a list with 1 or more entries, got []',
|
||||
'fan-1: Alexa.ModeController "Fan.Timer": the mode Short was given as a string, pass { value, modeResources }',
|
||||
]);
|
||||
});
|
||||
|
||||
test("a 1.x RangeController has no way to give its range: announced as 1.5.2 did, with its property now, and check() says it", () => {
|
||||
const blinds = endpoint("blinds-1", "Blinds", ["INTERIOR_BLIND"]);
|
||||
const lift = blinds.addCapability(AlexaInterfaceType.RANGE_CONTROLLER, { instance: "Blind.Lift", proactivelyReported: true });
|
||||
lift.addFriendlyName("Lift", "en-US");
|
||||
assert.deepEqual(lift.getJSON(), {
|
||||
type: "AlexaInterface",
|
||||
interface: "Alexa.RangeController",
|
||||
instance: "Blind.Lift",
|
||||
version: "3",
|
||||
properties: { supported: [{ name: "rangeValue" }], proactivelyReported: true, retrievable: true },
|
||||
capabilityResources: { friendlyNames: [text("Lift")] },
|
||||
});
|
||||
assert.deepEqual(blinds.check(), ['blinds-1: Alexa.RangeController "Blind.Lift": range: expected an object with min, max, precision, got nothing']);
|
||||
});
|
||||
|
||||
test("registerDevice: every endpoint ends with the Alexa interface; alexaInterface: false leaves it out", async () => {
|
||||
const { alexa, bridge } = await setup(Alex2MQTT, "root");
|
||||
const lamp = bridge.registerDevice("Lamp", "lamp-1", DisplayCategory.LIGHT);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue