"use strict"; // Discovery: a message on /discover is answered on /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" }] }); });