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>
116 lines
5.9 KiB
JavaScript
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" }] });
|
|
});
|