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>
382 lines
20 KiB
JavaScript
382 lines
20 KiB
JavaScript
"use strict";
|
|
// The discovery JSON of an endpoint: generated from the descriptors of its capabilities, in the order they were
|
|
// declared, with the capabilities the library adds at the end.
|
|
const { test } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const {
|
|
Alex2MQTT, ActionMapping, Alexa, AlexaActions, AlexaInterfaceType, BrightnessController, DisplayCategory, EndpointHealth,
|
|
ModeController, PowerController, RangeController, TemperatureSensor, ToggleController, asset, registry, semantics, text,
|
|
} = require("alex2node");
|
|
const interfaces = require("../fixtures/interfaces.js");
|
|
const { endpoint } = require("../helpers/endpoint.js");
|
|
const { capabilities, zoo } = require("../helpers/fixtures.js");
|
|
const { setup, until } = require("../helpers/harness.js");
|
|
|
|
const ALEXA = { type: "AlexaInterface", interface: "Alexa", version: "3" };
|
|
const names = (json) => json.capabilities.map((capability) => capability.interface);
|
|
|
|
// Ask as Alex2MQTT does and return the endpoints of the one answer.
|
|
async function discover(alexa, root) {
|
|
const before = alexa.on(`${root}/discover_r`).length;
|
|
alexa.publish(`${root}/discover`, { namespace: "Alexa.Discovery", name: "Discover" });
|
|
await until(() => alexa.on(`${root}/discover_r`).length === before + 1, 3000, "discovery reply");
|
|
return alexa.on(`${root}/discover_r`)[before];
|
|
}
|
|
|
|
// 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, endpoint: { categories, description } = {} } 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("lamp-1", "Lamp", categories);
|
|
if (description) device.setDescription(description);
|
|
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", () => {
|
|
const lamp = endpoint();
|
|
lamp.add(PowerController, { proactivelyReported: true });
|
|
assert.equal(
|
|
JSON.stringify(lamp.getJSON().capabilities[0]),
|
|
'{"type":"AlexaInterface","interface":"Alexa.PowerController","version":"3",'
|
|
+ '"properties":{"supported":[{"name":"powerState"}],"proactivelyReported":true,"retrievable":true}}'
|
|
);
|
|
});
|
|
|
|
test("capabilities are listed as declared, the Alexa interface after them", () => {
|
|
const thermostat = endpoint("thermo-1", "Thermostat", ["THERMOSTAT", "TEMPERATURE_SENSOR"]);
|
|
thermostat.add(TemperatureSensor);
|
|
thermostat.add(PowerController);
|
|
thermostat.add(BrightnessController);
|
|
assert.deepEqual(names(thermostat.getJSON()), ["Alexa.TemperatureSensor", "Alexa.PowerController", "Alexa.BrightnessController", "Alexa"]);
|
|
assert.deepEqual(thermostat.getJSON().capabilities.at(-1), ALEXA);
|
|
assert.deepEqual(thermostat.getCapabilities().map((capability) => capability.namespace), ["Alexa.TemperatureSensor", "Alexa.PowerController", "Alexa.BrightnessController"]);
|
|
|
|
// Declared by the device itself, it stays where it was declared and is listed once
|
|
const lamp = endpoint();
|
|
lamp.add(Alexa);
|
|
lamp.add(PowerController);
|
|
assert.deepEqual(names(lamp.getJSON()), ["Alexa", "Alexa.PowerController"]);
|
|
});
|
|
|
|
test("defaults of a declaration: retrievable, not proactively reported, nonControllable left out unless given", () => {
|
|
const lamp = endpoint();
|
|
assert.deepEqual(lamp.add(PowerController).toJSON().properties, { supported: [{ name: "powerState" }], proactivelyReported: false, retrievable: true });
|
|
assert.deepEqual(
|
|
lamp.add(BrightnessController, { retrievable: false, proactivelyReported: true, nonControllable: true }).toJSON().properties,
|
|
{ supported: [{ name: "brightness" }], proactivelyReported: true, retrievable: false, nonControllable: true }
|
|
);
|
|
assert.equal(lamp.add(TemperatureSensor, { nonControllable: false }).toJSON().properties.nonControllable, false);
|
|
});
|
|
|
|
test("PowerController verificationsRequired: the directives Alexa asks the user to confirm", () => {
|
|
const capability = endpoint().add(PowerController, { verificationsRequired: ["TurnOff"] });
|
|
assert.deepEqual(capability.toJSON().verificationsRequired, [{ directive: "TurnOff", methods: [{ "@type": "Confirmation" }] }]);
|
|
assert.equal("verificationsRequired" in endpoint().add(PowerController).toJSON(), false);
|
|
});
|
|
|
|
test("1.x declarations are announced as 1.5.2 announced them, with the Alexa interface added", () => {
|
|
const T = AlexaInterfaceType;
|
|
// zoo.js, the devices whose discovery JSON 1.5.2 produced without corrections
|
|
const declarations = {
|
|
"zoo-dimmer": (device) => {
|
|
device.addCapability(T.POWER_CONTROLLER, { proactivelyReported: true });
|
|
device.addCapability(T.BRIGHTNESS_CONTROLLER, { proactivelyReported: true });
|
|
},
|
|
"zoo-thermostat": (device) => {
|
|
device.addCapability(T.THERMOSTAT_CONTROLLER, { proactivelyReported: true });
|
|
device.addCapability(T.TEMPERATURE_SENSOR, { proactivelyReported: true });
|
|
},
|
|
"zoo-curtain": (device) => {
|
|
device.addCapability(T.POWER_CONTROLLER, { proactivelyReported: true });
|
|
const toggle = device.addCapability(T.TOGGLE_CONTROLLER, { proactivelyReported: true, instance: "Zoo.Shade" });
|
|
toggle.addFriendlyName("Shade", "en-US");
|
|
toggle.addActionMapping(new ActionMapping([AlexaActions.Close], "TurnOn"));
|
|
toggle.addActionMapping(new ActionMapping([AlexaActions.Open], "TurnOff"));
|
|
},
|
|
"zoo-fan": (device) => {
|
|
device.addCapability(T.POWER_CONTROLLER, { proactivelyReported: true });
|
|
const mode = device.addCapability(T.MODE_CONTROLLER, { proactivelyReported: true });
|
|
mode.setInstance("Fan.Speed");
|
|
mode.addFriendlyName("Speed", "en-US");
|
|
mode.addSupportedModes(["Low", "Medium", "High"].map((text) => ({
|
|
value: `Speed.${text}`,
|
|
modeResources: { friendlyNames: [{ "@type": "text", value: { text, locale: "en-US" } }] },
|
|
})));
|
|
},
|
|
"zoo-scene": (device) => device.addCapability(T.SCENE_CONTROLLER),
|
|
"zoo-tv": (device) => {
|
|
device.addCapability("Alexa.PowerController", { proactivelyReported: true });
|
|
device.addCapability("Alexa.ChannelController", { proactivelyReported: true });
|
|
},
|
|
};
|
|
for (const [endpointId, declare] of Object.entries(declarations)) {
|
|
const accepted = zoo(endpointId);
|
|
const device = likeTheZoo(endpointId);
|
|
declare(device);
|
|
assert.deepEqual(device.getJSON(), { ...accepted, capabilities: [...accepted.capabilities, ALEXA] }, endpointId);
|
|
assert.deepEqual(device.check(), [], endpointId);
|
|
}
|
|
});
|
|
|
|
test("1.x declarations of the zoo's lock, motion sensor and heater list their properties, which 1.5.2 left to the caller", () => {
|
|
const T = AlexaInterfaceType;
|
|
const declarations = {
|
|
"zoo-lock": [T.LOCK_CONTROLLER],
|
|
"zoo-motion": [T.MOTION_SENSOR],
|
|
"zoo-heater": [T.POWER_CONTROLLER, T.POWER_LEVEL_CONTROLLER],
|
|
"zoo-color-lamp": [T.POWER_CONTROLLER, T.BRIGHTNESS_CONTROLLER, T.COLOR_CONTROLLER],
|
|
"zoo-warm-lamp": [T.POWER_CONTROLLER, T.COLOR_TEMPERATURE_CONTROLLER],
|
|
};
|
|
for (const [endpointId, types] of Object.entries(declarations)) {
|
|
const device = likeTheZoo(endpointId);
|
|
for (const type of types) device.addCapability(type, { proactivelyReported: true });
|
|
assert.deepEqual(device.getJSON().capabilities, [...zoo(endpointId).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);
|
|
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
|
lamp.addCapability(AlexaInterfaceType.ENDPOINT_HEALTH);
|
|
const scene = bridge.registerDevice("Evening", "scene-1", DisplayCategory.SCENE_TRIGGER);
|
|
scene.addCapability(AlexaInterfaceType.SCENE_CONTROLLER);
|
|
// A registerDevice() device gets no Alexa.EndpointHealth it did not declare
|
|
const bare = bridge.registerDevice("Plug", "plug-1", DisplayCategory.SMARTPLUG);
|
|
bare.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
|
|
|
assert.deepEqual((await discover(alexa, "root")).map(names), [
|
|
["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"],
|
|
["Alexa.SceneController", "Alexa"],
|
|
["Alexa.PowerController", "Alexa"],
|
|
]);
|
|
|
|
const without = await setup(Alex2MQTT, "root-1x", { alexaInterface: false });
|
|
without.bridge.registerDevice("Lamp", "lamp-1", DisplayCategory.LIGHT).addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
|
without.bridge.addDevice({ endpointId: "lamp-2", name: "Lamp Two", categories: ["LIGHT"], endpointHealth: false }).add(PowerController);
|
|
assert.deepEqual((await discover(without.alexa, "root-1x")).map(names), [["Alexa.PowerController"], ["Alexa.PowerController"]]);
|
|
});
|
|
|
|
test("addDevice: the fields of the definition reach discovery; the defaults are those of registerDevice", async () => {
|
|
const { alexa, bridge } = await setup(Alex2MQTT, "root");
|
|
bridge.addDevice({
|
|
endpointId: "bedroom-blinds",
|
|
name: "Bedroom Blinds",
|
|
categories: ["INTERIOR_BLIND", DisplayCategory.OTHER],
|
|
description: "Roller blind by Acme",
|
|
manufacturerName: "Acme",
|
|
manufacturer: "Acme Inc",
|
|
model: "RB-2",
|
|
serialNumber: "0042",
|
|
firmwareVersion: "2.1.0",
|
|
softwareVersion: "2.1.3",
|
|
customIdentifier: "acme-rb2-0042",
|
|
cookie: { room: "bedroom" },
|
|
endpointHealth: false,
|
|
});
|
|
bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"], endpointHealth: false });
|
|
const registered = bridge.registerDevice("Lamp", "lamp-2", DisplayCategory.LIGHT);
|
|
|
|
const [blinds, lamp] = await discover(alexa, "root");
|
|
assert.deepEqual(blinds, {
|
|
endpointId: "bedroom-blinds",
|
|
friendlyName: "Bedroom Blinds",
|
|
description: "Roller blind by Acme",
|
|
manufacturerName: "Acme",
|
|
displayCategories: ["INTERIOR_BLIND", "OTHER"],
|
|
additionalAttributes: {
|
|
manufacturer: "Acme Inc",
|
|
model: "RB-2",
|
|
serialNumber: "0042",
|
|
firmwareVersion: "2.1.0",
|
|
softwareVersion: "2.1.3",
|
|
customIdentifier: "acme-rb2-0042",
|
|
},
|
|
cookie: { room: "bedroom" },
|
|
capabilities: [ALEXA],
|
|
});
|
|
assert.deepEqual(lamp, { ...registered.getJSON(), endpointId: "lamp-1" });
|
|
assert.equal("cookie" in lamp, false);
|
|
});
|
|
|
|
test("addDevice: Alexa.EndpointHealth is listed for the device unless it declares one, opts out or is a scene", async () => {
|
|
const { alexa, bridge } = await setup(Alex2MQTT, "root");
|
|
bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] }).add(PowerController);
|
|
const reporting = bridge.addDevice({ endpointId: "lamp-2", name: "Lamp Two", categories: ["LIGHT"] });
|
|
reporting.add(EndpointHealth, { proactivelyReported: true });
|
|
reporting.add(PowerController);
|
|
bridge.addDevice({ endpointId: "lamp-3", name: "Lamp Three", categories: ["LIGHT"], endpointHealth: false }).add(PowerController);
|
|
bridge.addDevice({ endpointId: "scene-1", name: "Evening", categories: ["SCENE_TRIGGER"], description: "Evening scene" })
|
|
.addCapability(AlexaInterfaceType.SCENE_CONTROLLER);
|
|
|
|
const [lamp, declared, optedOut, scene] = await discover(alexa, "root");
|
|
assert.deepEqual(names(lamp), ["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"]);
|
|
assert.deepEqual(lamp.capabilities[1], {
|
|
type: "AlexaInterface",
|
|
interface: "Alexa.EndpointHealth",
|
|
version: "3.1",
|
|
properties: { supported: [{ name: "connectivity" }], proactivelyReported: false, retrievable: true },
|
|
});
|
|
assert.deepEqual(names(declared), ["Alexa.EndpointHealth", "Alexa.PowerController", "Alexa"]);
|
|
assert.equal(declared.capabilities[0].properties.proactivelyReported, true);
|
|
assert.deepEqual(names(optedOut), ["Alexa.PowerController", "Alexa"]);
|
|
assert.deepEqual(names(scene), ["Alexa.SceneController", "Alexa"]);
|
|
});
|
|
|
|
test("discovery logs what check() says about the devices, each line once", async () => {
|
|
const logged = [];
|
|
const { alexa, bridge } = await setup(Alex2MQTT, "root", { log: (message) => logged.push(message) });
|
|
const warnings = () => logged.filter((message) => message.startsWith("warning: "));
|
|
const lamp = bridge.registerDevice("Dave's Lamp", "lamp-1", DisplayCategory.LIGHT);
|
|
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
|
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
|
lamp.addCapability(AlexaInterfaceType.KEYPAD_CONTROLLER);
|
|
bridge.registerDevice("Lamp", "lamp-2", DisplayCategory.LIGHT).addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
|
|
|
// Announced all the same: 1.x announced it, and Alexa may have taken it
|
|
const [first] = await discover(alexa, "root");
|
|
assert.deepEqual(names(first), ["Alexa.PowerController", "Alexa.PowerController", "Alexa.KeypadController", "Alexa"]);
|
|
assert.deepEqual(warnings(), [
|
|
'warning: lamp-1: the name "Dave\'s Lamp" takes up to 256 letters, digits and spaces, no punctuation',
|
|
"warning: lamp-1: Alexa.PowerController: the interface is declared twice",
|
|
"warning: lamp-1: Alexa.KeypadController: no property list known, discovery lists it with no supported properties",
|
|
]);
|
|
|
|
lamp.setName("Daves Lamp");
|
|
bridge.registerDevice("Timer!", "lamp-3", DisplayCategory.LIGHT);
|
|
await discover(alexa, "root");
|
|
assert.deepEqual(warnings().slice(3), ['warning: lamp-3: the name "Timer!" takes up to 256 letters, digits and spaces, no punctuation']);
|
|
});
|
|
|
|
test("a device that cannot be described is left out of the answer and reported; the others are announced", async () => {
|
|
const { alexa, bridge } = await setup(Alex2MQTT, "root");
|
|
const errors = [];
|
|
bridge.on("error", (err) => errors.push(err.message));
|
|
bridge.registerDevice("Lamp", "lamp-1", DisplayCategory.LIGHT).addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
|
const lock = bridge.registerDevice("Front Door", "lock-1", DisplayCategory.SMARTLOCK);
|
|
lock.addCapability(AlexaInterfaceType.LOCK_CONTROLLER).getJSON = () => { throw new TypeError("state is not defined"); };
|
|
|
|
assert.deepEqual((await discover(alexa, "root")).map((each) => each.endpointId), ["lamp-1"]);
|
|
assert.deepEqual(errors, ["lock-1 is not in the discovery answer: state is not defined"]);
|
|
|
|
// Without an error listener the same discovery is answered and nothing is thrown
|
|
bridge.removeAllListeners("error");
|
|
assert.deepEqual((await discover(alexa, "root")).map((each) => each.endpointId), ["lamp-1"]);
|
|
});
|