Alex2Node/dist/cjs/device/Device.js
David 3a07c861b1 registry: SceneController events, DoorbellEventSource, SimpleEventSource, TimeHoldController, InventoryLevelSensor, WakeOnLANController
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>
2026-09-28 21:01:53 +00:00

383 lines
18 KiB
JavaScript

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = require("events");
const AlexaErrorResponse_js_1 = require("../compat/AlexaErrorResponse.js");
const AlexaInterface_js_1 = require("../compat/AlexaInterface.js");
const AlexaStatusMessage_js_1 = require("../compat/AlexaStatusMessage.js");
const enums_js_1 = require("../compat/enums.js");
const build_js_1 = require("../messages/build.js");
const Alexa_js_1 = require("../registry/interfaces/Alexa.js");
const EndpointHealth_js_1 = require("../registry/interfaces/EndpointHealth.js");
const SceneController_js_1 = require("../registry/interfaces/SceneController.js");
const schema_js_1 = require("../registry/schema.js");
const types_js_1 = require("../registry/types.js");
const topics = __importStar(require("../topics.js"));
const transport_js_1 = require("../transport.js");
const Capability_js_1 = require("./Capability.js");
const validate_js_1 = require("./validate.js");
class Device extends events_1.EventEmitter {
/** client is ignored: in 1.x it was the broker client, and a device could only be built after connect(). */
constructor(client, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") {
super();
this.rootTopic = rootTopic;
this.name = name;
this.endpointId = endpointId;
this.displayCategory = displayCategory;
this.description = description;
this.manufacturerName = manufacturerName;
this.manufacturer = manufacturer;
this.model = model;
this.softwareVersion = "1.0.0";
this.serialNumber = "Alex2Node";
this.firmwareVersion = "1.0.0";
this.customIdentifier = "Alex2Node";
/**
* Discovery lists the Alexa interface on the endpoint (alexa-interface.html: "You must explicitly identify your
* support for the Alexa interface"). The bridge sets it from its alexaInterface option.
*/
this.alexaInterface = true;
/**
* Discovery lists Alexa.EndpointHealth when the device did not declare it and is not a scene. On for a device of
* bridge.addDevice(), off for one of registerDevice(), which is announced as 1.x announced it.
*/
this.endpointHealth = false;
this.capabilities = [];
/**
* What the device and the messages it builds publish through. The bridge sets it when the device is registered
* and clears it when the device is unregistered; a MemoryPublisher here tests a device without a broker. While it
* is null every send() resolves "" and reports why.
*/
this.publisher = null;
}
getName() {
return this.name;
}
setName(name) {
this.name = name;
}
getEndpointId() {
return this.endpointId;
}
setDisplayCategory(category) {
this.displayCategory = Array.isArray(category) ? category : [category];
}
getDisplayCategory() {
return this.displayCategory || [enums_js_1.DisplayCategory.LIGHT];
}
setDescription(description) {
this.description = description;
}
getDescription() {
return this.description;
}
getErrorMessage(correlationToken) {
const msg = new AlexaErrorResponse_js_1.AlexaErrorResponse(correlationToken, this.rootTopic, this.endpointId, this.publisher);
msg.onPublishError = this.onPublishError;
return msg;
}
getStatusMessage(correlationToken, isResponse = false, isDeferred = false) {
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.publisher, isResponse, isDeferred);
msg.onPublishError = this.onPublishError;
return msg;
}
/**
* A proactive Alexa.ChangeReport (1.5.1): add the changed properties (the default target), optionally
* .unchanged() then the others, and .send() - it goes to <root>/changeReport, which Alex2MQTT forwards to the Alexa
* event gateway with the user's token. Alexa only accepts it for capabilities registered with proactivelyReported.
* Without a changed property send() publishes nothing and resolves with "": Alex2MQTT would drop the report.
*/
getChangeReport(cause = "PHYSICAL_INTERACTION") {
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage("", this.rootTopic, this.endpointId, this.publisher, false, false, cause);
msg.onPublishError = this.onPublishError;
return msg;
}
/**
* Alexa.SceneController: answer an Activate / Deactivate directive with ActivationStarted / DeactivationStarted
* (1.5.1). Resolves with the topic published to, or "" when the publish failed (never rejects, 1.5.2).
*/
sendSceneResponse(correlationToken, activated, cause = "VOICE_INTERACTION", sendAsync = false) {
const answer = sendAsync ? topics.deferred : topics.response;
const build = () => (0, build_js_1.sceneEvent)({ endpointId: this.endpointId, correlationToken, activated, cause });
return (0, transport_js_1.send)(this.publisher, answer(this.rootTopic, this.endpointId), build, this.onPublishError);
}
/**
* Say that something happened on the device, which nobody asked for:
*
* door.raise(DoorbellEventSource, "DoorbellPress");
* remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }, { instance: topButton.instance });
*
* The event goes to <root>/event, which Alex2MQTT posts to Alexa: up to 30 events a minute for a root. The
* payload is checked by the descriptor, which also sets the time of the event to now and its cause to the usual
* one when the payload has none. Resolves with what became of the publish and does not reject.
*
* Throws a MessageError for an event that would not arrive: of an interface or an instance the device did not
* declare, not an event of the interface, with a payload that does not fit, or too long for Alex2MQTT.
*/
raise(descriptor, name, payload = {}, options = {}) {
const { endpointId } = this;
const { namespace } = descriptor;
const { instance = "", messageId } = options;
const refuse = (problem) => {
throw new build_js_1.MessageError(endpointId, `${namespace}.${name} was not raised: ${problem}`);
};
const capability = this.capability(namespace, instance);
if (!capability) {
const declared = instance ? `the instance ${JSON.stringify(instance)} of ${namespace}` : namespace;
return refuse(`the device did not declare ${declared}. Declare it with add() first${descriptor.instanced && !instance ? ", and pass the instance that raises the event" : ""}`);
}
const events = capability.descriptor.events ?? {};
const event = events[name];
if (!event)
return refuse(`it is not an event of the interface, which has ${Object.keys(events).join(", ") || "none"}`);
if (event.topic !== "proactive")
return refuse("it answers a directive. Send it with respond() in the handler of the directive");
let checked;
try {
checked = event.payload.parse(payload, "payload");
}
catch (err) {
if (err instanceof schema_js_1.SchemaError)
return refuse(err.message);
throw err;
}
const message = (0, build_js_1.proactiveEvent)({
endpointId,
messageId,
namespace: event.namespace ?? namespace,
name,
instance,
payloadVersion: event.payloadVersion ?? capability.descriptor.version,
payload: checked,
});
const bytes = Buffer.byteLength(JSON.stringify(message));
if (bytes > topics.EVENT_BYTES)
return refuse(`it is ${bytes} bytes as JSON, Alex2MQTT takes ${topics.EVENT_BYTES}`);
const topic = topics.event(this.rootTopic);
const unpublished = new Error(`nothing was published to ${topic}: the device is on no bridge, register it with addDevice() or registerDevice()`);
const published = this.publisher
? this.publisher.publish(topic, message)
: Promise.resolve({ ok: false, topic, error: unpublished });
return published.then((result) => {
if (!result.ok)
this.onPublishError?.(result.error);
return result;
});
}
/**
* How the device reports its state, every retrievable property of it:
*
* blinds.state((s) => s.set(lift, "rangeValue", motor.position).health("OK"));
*
* ReportState is answered with it, and it is the context of every ctx.respond(), where the handler sets only what
* the directive changed. Alexa wants the whole state in both (alexa-response.html, "Synchronous response").
*/
state(fill) {
this.stateProvider = fill;
return this;
}
/** Answer ReportState in a handler of its own, for a state that has to be read from the device first. */
onReportState(handler) {
this.reportStateHandler = handler;
return this;
}
/** The handler for the directives of declared capabilities that have no handler of their own. */
onDirective(handler) {
this.directiveHandler = handler;
return this;
}
/** The capability declared for the interface, under the instance for a generic controller. */
capability(namespace, instance = "") {
return this.capabilities.find((capability) => capability.namespace === namespace && capability.instance === instance);
}
/** The capabilities the device declared, in the order it declared them. */
getCapabilities() {
return this.capabilities.slice();
}
setManufacturerName(name) {
this.manufacturerName = name;
}
getManufacturerName() {
return this.manufacturerName;
}
setManufacturer(manufacturer) {
this.manufacturer = manufacturer;
}
getManufacturer() {
return this.manufacturer;
}
setModel(model) {
this.model = model;
}
getModel() {
return this.model;
}
getSoftwareVersion() {
return this.softwareVersion;
}
/**
* Declare an interface of the device:
*
* lamp.add(PowerController, { proactivelyReported: true });
* blinds.add(RangeController, { instance: "Blind.Lift", friendlyNames: [asset("Alexa.Setting.Opening")],
* range: { min: 0, max: 100, precision: 1 } });
*
* Throws a DeclarationError that names the endpoint, the interface and the instance when Alexa would reject the
* capability; the device is then as it was before the call.
*/
add(descriptor, ...[options]) {
const where = { endpointId: this.endpointId, namespace: descriptor.namespace, instance: "" };
if (options !== undefined && (typeof options !== "object" || options === null)) {
throw new types_js_1.DeclarationError(where, "the options of a declaration are an object");
}
try {
const { instance, friendlyNames, retrievable, proactivelyReported, nonControllable, ...given } = Capability_js_1.commonOptions.parse(options ?? {});
where.instance = instance ?? "";
const common = { instance, friendlyNames: friendlyNames, retrievable, proactivelyReported, nonControllable };
const capability = new Capability_js_1.Capability(descriptor, { ...common, options: interfaceOptions(descriptor, given) });
capability.endpointId = this.endpointId;
(0, validate_js_1.checkCapability)(capability, descriptor, this.view(this.capabilities));
(0, validate_js_1.checkCapabilityCount)(this.endpointId, this.announced([...this.capabilities, capability]).length);
this.capabilities.push(capability);
return capability;
}
catch (err) {
throw err instanceof schema_js_1.SchemaError ? new types_js_1.DeclarationError(where, err.message) : err;
}
}
/**
* Declare an interface the 1.x way, by its name: options.retrievable / proactivelyReported / instance, then the
* add and set methods of the capability that is returned. Throws for a name that is not an interface. Anything
* else Alexa would reject is not refused here: check() lists it.
*/
addCapability(type, options = {}) {
let capability;
try {
capability = new AlexaInterface_js_1.AlexaInterface(type, options.retrievable ?? true, options.proactivelyReported ?? false, options.instance ?? "");
}
catch (err) {
throw err instanceof types_js_1.DeclarationError ? new types_js_1.DeclarationError({ endpointId: this.endpointId }, err.problem) : err;
}
capability.endpointId = this.endpointId;
this.capabilities.push(capability);
return capability;
}
/**
* What Alexa would reject in the device as it is declared now, one line for each: a name with punctuation, an
* interface declared twice. Also what a capability noted about its declaration. Empty when there is nothing.
* The bridge logs the lines when it answers a discovery.
*/
check() {
const lines = [];
const attempt = (rule) => {
try {
rule();
}
catch (err) {
if (!(err instanceof types_js_1.DeclarationError))
throw err;
lines.push(err.message);
}
};
attempt(() => (0, validate_js_1.checkEndpoint)(this.fields()));
this.capabilities.forEach((capability, i) => {
capability.endpointId = this.endpointId;
attempt(() => (0, validate_js_1.checkCapability)(capability, capability.descriptor, this.view(this.capabilities.slice(0, i))));
const notes = [...capability.notes];
const { tier, properties } = capability.descriptor;
if (tier === 3 && Object.keys(properties).length === 0 && capability.toJSON().properties) {
notes.push("no property list known, discovery lists it with no supported properties");
}
for (const note of notes)
lines.push(new types_js_1.DeclarationError(capability, note).message);
});
attempt(() => (0, validate_js_1.checkCapabilityCount)(this.endpointId, this.announced(this.capabilities).length));
return lines;
}
/** The endpoint object for discovery. */
getJSON() {
return { ...this.fields(), capabilities: this.announced(this.capabilities).map((capability) => capability.toJSON()) };
}
fields() {
return {
endpointId: this.endpointId,
friendlyName: this.name,
description: this.description,
manufacturerName: this.manufacturerName,
displayCategories: this.getDisplayCategory(),
additionalAttributes: {
manufacturer: this.manufacturer,
model: this.model,
serialNumber: this.serialNumber,
firmwareVersion: this.firmwareVersion,
softwareVersion: this.softwareVersion,
customIdentifier: this.customIdentifier,
},
...(this.cookie ? { cookie: this.cookie } : {}),
};
}
// The endpoint as the rules of a capability see it
view(declaredBefore) {
return {
endpointId: this.endpointId,
friendlyName: this.name,
description: this.description,
displayCategories: this.getDisplayCategory(),
capabilities: declaredBefore,
};
}
// What discovery lists: the declared capabilities, then the ones the library adds
announced(declared) {
const has = (namespace) => declared.some((capability) => capability.namespace === namespace);
const added = [];
// alexa-scenecontroller.html, "Discovery": a scene is not a physical device and has no Alexa.EndpointHealth
if (this.endpointHealth && !has(EndpointHealth_js_1.EndpointHealth.namespace) && !has(SceneController_js_1.SceneController.namespace)) {
added.push(new Capability_js_1.Capability(EndpointHealth_js_1.EndpointHealth, { options: {} }));
}
if (this.alexaInterface && !has(Alexa_js_1.Alexa.namespace))
added.push(new Capability_js_1.Capability(Alexa_js_1.Alexa, { options: {} }));
return [...declared, ...added];
}
}
// The options of the interface itself, checked by the schema of its descriptor
function interfaceOptions(descriptor, given) {
if (descriptor.options)
return descriptor.options.parse(given);
const [unknown] = Object.keys(given);
if (unknown !== undefined)
throw new schema_js_1.SchemaError(unknown, "not an option of the interface");
return {};
}
exports.default = Device;