"exports" gains "import" and "types" next to "require": import "alex2node" failed with ERR_PACKAGE_PATH_NOT_EXPORTED. scripts/build.mjs compiles src/ to dist/cjs, dist/esm and dist/types in a staging directory and swaps them in only when both compiler runs passed. "main" and "types" follow for resolvers that do not read "exports". Target ES6 -> ES2020, "engines": node >= 18, relative imports carry ".js" for Node's ESM loader. uuid gives way to crypto.randomUUID(); mqtt is the only runtime dependency left. Tests load the package by name. New: both entry points, the modules the builds import, dist/ equal to a fresh build, a round trip on the ES module build, and two compiled type fixtures in place of the .d.ts regexes. 22 tests pass in 5.7 s. dist/: 16 files, 79,711 B -> 25 files, 132,291 B; the npm tarball is 25,125 B. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
104 lines
5.2 KiB
JavaScript
104 lines
5.2 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"]
|
|
);
|
|
});
|