Alex2Node/test/discovery.test.js
David aa0ffd64ea registry: describe an Alexa interface as data
src/registry/ holds what the library knows about an interface: namespace, version, the page it was read from,
properties with their value schemas and directives with their payload schemas. Five interfaces are described
(Alexa, PowerController, BrightnessController, TemperatureSensor, EndpointHealth); the other 65 names of
AlexaInterfaceType are stubs with the version and property names of 1.5.2. schema.ts is the run-time check behind
it (241 lines, no new dependency), catalog.ts the vocabularies of the pages: 103 assets (23 units), 6 actions,
9 states, 56 display categories, 22 reserved words, 73 error types under 11 namespaces.

AlexaInterface.getVersion() and getProps() read the registry; the two switch statements are gone (-167 lines).
On the wire: Alexa.EndpointHealth is announced at 3.1 (was 3.3; the page is titled 3.1 and no page mentions 3.3),
and TimeHoldController and Camera.LiveViewController at 3 and 1.7 (1.5.2 sent the string "UNKNOWN").
DisplayCategory gains VACUUM. New exports: registry, DeclarationError, SchemaError, Assets, Units, Actions,
States, DisplayCategories and the descriptor types.

Tests: 20 JSON examples of the five pages under test/fixtures/alexa-docs; every directive payload and property
value in them parses with its descriptor. npm test: 57 pass (was 30) in 10.8 s, also on Node 18.20.8 and 20.20.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:18:22 +00:00

116 lines
5.9 KiB
JavaScript

"use strict";
// Discovery: a message on <root>/discover is answered on <root>/discover_r with one JSON array, an endpoint object
// per registered device.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { Alex2MQTT, AlexaInterfaceType, DisplayCategory } = require("alex2node");
const { setup, connected, until } = require("./helpers/harness.js");
// Ask as Alex2MQTT does and return the endpoints of the one answer.
async function discover(alexa, root) {
alexa.publish(`${root}/discover`, { namespace: "Alexa.Discovery", name: "Discover" });
await until(() => alexa.on(`${root}/discover_r`).length === 1, 3000, "discovery reply");
return alexa.on(`${root}/discover_r`)[0];
}
test("discovery answers with every registered device and its capabilities", async () => {
const { alexa, bridge } = await setup(Alex2MQTT, "root");
const discovered = [];
bridge.on("discover", (n) => discovered.push(n));
const lamp = bridge.registerDevice("Dining Room Light", "5020AA", DisplayCategory.LIGHT);
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER, { proactivelyReported: true });
lamp.addCapability(AlexaInterfaceType.BRIGHTNESS_CONTROLLER);
const scene = bridge.registerDevice("Evening", "scene-1", DisplayCategory.SCENE_TRIGGER);
scene.addCapability(AlexaInterfaceType.SCENE_CONTROLLER);
const endpoints = await discover(alexa, "root");
assert.equal(endpoints.length, 2);
assert.equal(endpoints[0].friendlyName, "Dining Room Light");
assert.deepEqual(endpoints[0].displayCategories, ["LIGHT"]);
const power = endpoints[0].capabilities.find((c) => c.interface === "Alexa.PowerController");
assert.equal(power.properties.proactivelyReported, true);
assert.deepEqual(power.properties.supported, [{ name: "powerState" }]);
// Alexa.SceneController has no properties block
const activation = endpoints[1].capabilities.find((c) => c.interface === "Alexa.SceneController");
assert.equal(activation.supportsDeactivation, true);
assert.equal(activation.version, "3");
assert.equal(activation.properties, undefined);
await until(() => discovered.length === 1, 3000, "the discover event");
assert.deepEqual(discovered, [2]);
assert.notEqual(bridge.lastDiscoveryAt, null);
});
test("unregisterDevice: the device is gone from the next discovery answer", async () => {
const { alexa, bridge } = await setup(Alex2MQTT, "root");
const lamp = bridge.registerDevice("Dining Room Light", "5020AA", DisplayCategory.LIGHT);
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
const scene = bridge.registerDevice("Evening", "scene-1", DisplayCategory.SCENE_TRIGGER);
scene.addCapability(AlexaInterfaceType.SCENE_CONTROLLER);
assert.equal(bridge.unregisterDevice("scene-1"), true);
assert.equal(bridge.unregisterDevice("scene-1"), false);
assert.equal(bridge.getDevices().length, 1);
const endpoints = await discover(alexa, "root");
assert.deepEqual(endpoints.map((e) => e.endpointId), ["5020AA"]);
});
test("registerDevice with an endpointId already registered returns the existing device and warns", async () => {
const logs = [];
const { url, alexa, bridge } = await setup(Alex2MQTT, "root", { log: (m) => logs.push(m) });
const first = bridge.registerDevice("Lamp A", "same", DisplayCategory.LIGHT);
first.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
const again = bridge.registerDevice("Lamp B", "same", DisplayCategory.SWITCH);
assert.equal(again, first);
assert.equal(again.name, "Lamp A");
assert.equal(bridge.getDevices().length, 1);
assert.ok(logs.some((m) => /warning: registerDevice\("same"\)/.test(m)), "warned through the log hook");
const endpoints = await discover(alexa, "root");
assert.deepEqual(endpoints.map((e) => e.endpointId), ["same"]);
// Without a log hook (the examples and the Quick Start set none) the warning falls back to console.warn.
const quiet = await connected(new Alex2MQTT("u", "p", "root-quiet", false, { host: url }));
const warned = [];
const realWarn = console.warn;
console.warn = (...args) => warned.push(args.join(" "));
try {
quiet.registerDevice("Q", "same", null);
assert.equal(quiet.registerDevice("Q again", "same", null).name, "Q");
} finally {
console.warn = realWarn;
}
assert.ok(warned.some((m) => /warning: registerDevice\("same"\)/.test(m)), "console.warn without a log hook");
});
test("ThermostatController discovery: version 3.2, targetSetpoint listed with both setpoints and the mode", async () => {
const { alexa, bridge } = await setup(Alex2MQTT, "root");
const lamp = bridge.registerDevice("Lamp", "lamp-1", DisplayCategory.LIGHT);
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
const thermostat = bridge.registerDevice("Thermostat", "thermo-1", DisplayCategory.THERMOSTAT);
thermostat.addCapability(AlexaInterfaceType.THERMOSTAT_CONTROLLER);
const endpoints = await discover(alexa, "root");
assert.deepEqual(endpoints.map((e) => e.endpointId), ["lamp-1", "thermo-1"]);
const capability = endpoints[1].capabilities.find((c) => c.interface === "Alexa.ThermostatController");
assert.equal(capability.version, "3.2");
assert.deepEqual(
capability.properties.supported.map((p) => p.name),
["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"]
);
});
test("EndpointHealth discovery: version 3.1 with connectivity (1.x announced 3.3, which no Alexa page mentions)", 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, { proactivelyReported: true });
const [endpoint] = await discover(alexa, "root");
const health = endpoint.capabilities.find((c) => c.interface === "Alexa.EndpointHealth");
assert.equal(health.version, "3.1");
assert.deepEqual(health.properties, { retrievable: true, proactivelyReported: true, supported: [{ name: "connectivity" }] });
});