import { AlexaInterface } from "./AlexaInterface.js"; import type { AlexaInterfaceType } from "./AlexaInterface.js"; import { DisplayCategory } from "./DisplayCategory.js"; import { EventEmitter } from "events"; import { randomUUID } from "crypto"; import type { MqttClient } from "mqtt"; import { AlexaStatusMessage } from "./AlexaStatusMessage.js"; import type { ChangeCause } from "./AlexaStatusMessage.js"; import { AlexaErrorResponse } from "./AlexaErrorResponse.js"; class Device extends EventEmitter { protected softwareVersion: string = "1.0.0"; private capabilities: AlexaInterface[] = []; /** Where a failed publish from this device or a message it built is reported; Alex2MQTT sets it (1.5.2). */ public onPublishError?: (err: Error) => void; constructor( private mqttClient: MqttClient, private rootTopic: string, public name: string, public endpointId: string, public displayCategory: Array | null, public description: string = "Alexa to Node.js bridge", public manufacturerName: string = "Alex2Node", public manufacturer: string = "Alex2Node", public model: string = "Alex2Node_v1.0.0" ) { super(); } /** Internal (1.5.2): Alex2MQTT.connect() re-binds every registered device to its new broker client after a disconnect(). */ setMqttClient(client: MqttClient): void { this.mqttClient = client; } getName(): string { return this.name; } setName(name: string): void { this.name = name; } getEndpointId(): string { return this.endpointId; } setDisplayCategory(category: DisplayCategory | DisplayCategory[]): void { this.displayCategory = Array.isArray(category) ? category : [category]; } getDisplayCategory(): Array { return this.displayCategory || [DisplayCategory.LIGHT]; } setDescription(description: string): void { this.description = description; } getDescription(): string { return this.description; } getErrorMessage(correlationToken: string): AlexaErrorResponse { const msg = new AlexaErrorResponse( correlationToken, this.rootTopic, this.endpointId, this.mqttClient ); msg.onPublishError = this.onPublishError; return msg; } getStatusMessage( correlationToken: string, isResponse = false, isDeferred = false ): AlexaStatusMessage { const msg = new AlexaStatusMessage( correlationToken, this.rootTopic, this.endpointId, this.mqttClient, 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 /changeReport, which Alex2MQTT forwards to the Alexa * event gateway with the user's token. Alexa only accepts it for capabilities registered with proactivelyReported. */ getChangeReport(cause: ChangeCause = "PHYSICAL_INTERACTION"): AlexaStatusMessage { const msg = new AlexaStatusMessage("", this.rootTopic, this.endpointId, this.mqttClient, 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: string, activated: boolean, cause: ChangeCause = "VOICE_INTERACTION", sendAsync = false): Promise { const payload = { context: {}, event: { header: { namespace: "Alexa.SceneController", name: activated ? "ActivationStarted" : "DeactivationStarted", messageId: randomUUID(), correlationToken, payloadVersion: "3" }, endpoint: { endpointId: this.endpointId }, payload: { cause: { type: cause }, timestamp: new Date().toISOString() }, }, }; const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; return new Promise((resolve) => { this.mqttClient.publish(topic, JSON.stringify(payload), (err) => { if (!err) return resolve(topic); if (this.onPublishError) this.onPublishError(err); // -> the bridge's "error" event (when somebody listens) resolve(""); // 1.5.1 rejected here, and an un-caught send() then killed the host on any broker hiccup }); }); } getCapabilities(): AlexaInterface[] { return this.capabilities.slice(); } setManufacturerName(name: string): void { this.manufacturerName = name; } getManufacturerName(): string { return this.manufacturerName; } setManufacturer(manufacturer: string): void { this.manufacturer = manufacturer; } getManufacturer(): string { return this.manufacturer; } setModel(model: string): void { this.model = model; } getModel(): string { return this.model; } getSoftwareVersion(): string { return this.softwareVersion; } /** Add a capability. 1.5.1: options.retrievable / proactivelyReported / instance (a change report needs proactivelyReported). */ addCapability(type: AlexaInterfaceType, options: { retrievable?: boolean; proactivelyReported?: boolean; instance?: string } = {}): AlexaInterface { const newCapability = new AlexaInterface(type, options.retrievable ?? true, options.proactivelyReported ?? false, options.instance ?? ""); this.capabilities.push(newCapability); // console.log( // `Created new interface with type: ${newCapability.getTypeString()}` // ); return newCapability; } getJSON(): Record { return { endpointId: this.endpointId, friendlyName: this.name, description: this.description, manufacturerName: this.manufacturerName, displayCategories: this.getDisplayCategory(), additionalAttributes: { manufacturer: this.manufacturer, model: this.model, serialNumber: "Alex2Node", firmwareVersion: "1.0.0", softwareVersion: this.softwareVersion, customIdentifier: "Alex2Node", }, capabilities: this.capabilities.map((cap) => cap.getJSON()), }; } } export default Device;