From 7f051dfdcbba4be94526b087331766b8ac958d53 Mon Sep 17 00:00:00 2001 From: David <43281665+chaos511@users.noreply.github.com> Date: Thu, 8 May 2025 15:57:14 +0000 Subject: [PATCH 01/10] add internal event emitter for error handling --- dist/Alex2Node.js | 13 +++++++++---- package.json | 2 +- src/Alex2Node.ts | 17 ++++++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/dist/Alex2Node.js b/dist/Alex2Node.js index 7e63d2e..7ef4af6 100644 --- a/dist/Alex2Node.js +++ b/dist/Alex2Node.js @@ -5,8 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); const mqtt_1 = __importDefault(require("mqtt")); const Device_1 = __importDefault(require("./Device")); -class Alex2MQTT { +const events_1 = require("events"); +class Alex2MQTT extends events_1.EventEmitter { constructor(username, password, rootTopic, debugLogging) { + super(); // Initialize EventEmitter this.username = username; this.password = password; this.rootTopic = rootTopic; @@ -30,7 +32,8 @@ class Alex2MQTT { console.log("[Alex2Node.ts] Subscribed to topics"); } if (err) { - throw err; + console.error("[Alex2Node.ts] MQTT connection error:", err); + this.emit("error", err); } }); }); @@ -48,7 +51,8 @@ class Alex2MQTT { if (deviceArray.length > 0) { this.client.publish(topic + "_r", JSON.stringify(deviceArray), (err) => { if (err) { - throw err; + console.error("[Alex2Node.ts] MQTT connection error:", err); + this.emit("error", err); } else if (this.debugLogging) { console.log(`[Alex2Node.ts] Discovery payloads published to ${topic + "_r"}`); @@ -94,7 +98,8 @@ class Alex2MQTT { if (this.debugLogging) { console.error("[Alex2Node.ts] Connection failed:", err); } - throw err; + console.error("[Alex2Node.ts] MQTT connection error:", err); + this.emit("error", err); }); } registerDevice(name, endpointId) { diff --git a/package.json b/package.json index 92dc81d..7a7ea67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alex2node", - "version": "1.0.0", + "version": "1.0.1", "description": "A Node.js library for creating Alexa-compatible devices using MQTT, based on Alex2MQTT.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/Alex2Node.ts b/src/Alex2Node.ts index bf18271..d0682ca 100644 --- a/src/Alex2Node.ts +++ b/src/Alex2Node.ts @@ -1,7 +1,8 @@ import mqtt, { MqttClient } from "mqtt"; import Device from "./Device"; +import { EventEmitter } from "events"; -class Alex2MQTT { +class Alex2MQTT extends EventEmitter { private client: MqttClient | null = null; private devices: Device[] = []; private MqttHost = "mqtt://Alex2MQTT.stormysdream.club"; @@ -11,7 +12,10 @@ class Alex2MQTT { private password: string, private rootTopic: string, private debugLogging: boolean - ) {} + ) { + super(); // Initialize EventEmitter + } + connect(): void { const options = { @@ -31,7 +35,8 @@ class Alex2MQTT { console.log("[Alex2Node.ts] Subscribed to topics"); } if (err) { - throw err; + console.error("[Alex2Node.ts] MQTT connection error:", err); + this.emit("error", err); } }); }); @@ -56,7 +61,8 @@ class Alex2MQTT { JSON.stringify(deviceArray), (err) => { if (err) { - throw err; + console.error("[Alex2Node.ts] MQTT connection error:", err); + this.emit("error", err); } else if (this.debugLogging) { console.log( `[Alex2Node.ts] Discovery payloads published to ${ @@ -109,7 +115,8 @@ class Alex2MQTT { if (this.debugLogging) { console.error("[Alex2Node.ts] Connection failed:", err); } - throw err; + console.error("[Alex2Node.ts] MQTT connection error:", err); + this.emit("error", err); }); } From f4d07b589c2dd0a0b1a7b5497d99e7d51f07771a Mon Sep 17 00:00:00 2001 From: David <43281665+chaos511@users.noreply.github.com> Date: Sat, 10 May 2025 12:21:51 +0000 Subject: [PATCH 02/10] Add full support and examples for thermostat --- dist/Alex2Node.js | 11 ++- dist/AlexaInterface.js | 19 +++- dist/AlexaStatusMessage.js | 23 ++++- dist/Device.js | 8 +- dist/index.js | 7 +- examples/ExampleThermostat.js | 160 ++++++++++++++++++++++++++++++++++ package.json | 2 +- src/Alex2Node.ts | 29 ++++-- src/AlexaInterface.ts | 20 ++++- src/AlexaStatusMessage.ts | 43 ++++++++- src/Device.ts | 14 +-- src/index.ts | 3 +- 12 files changed, 307 insertions(+), 32 deletions(-) create mode 100644 examples/ExampleThermostat.js diff --git a/dist/Alex2Node.js b/dist/Alex2Node.js index 7ef4af6..d44c7ce 100644 --- a/dist/Alex2Node.js +++ b/dist/Alex2Node.js @@ -56,7 +56,7 @@ class Alex2MQTT extends events_1.EventEmitter { } else if (this.debugLogging) { console.log(`[Alex2Node.ts] Discovery payloads published to ${topic + "_r"}`); - console.log(deviceArray); + console.log(JSON.stringify(deviceArray, null, 2)); } }); } @@ -102,14 +102,19 @@ class Alex2MQTT extends events_1.EventEmitter { this.emit("error", err); }); } - registerDevice(name, endpointId) { + registerDevice(name, endpointId, displayCategory) { if (!this.client) { throw new Error("Must call connect before creating devices"); } if (this.debugLogging) { console.log(`[Alex2Node.ts] Creating new device with endpoint: ${endpointId}`); } - const device = new Device_1.default(this.client, this.rootTopic, name, endpointId); + const normalizedCategory = displayCategory === null + ? null + : Array.isArray(displayCategory) + ? displayCategory + : [displayCategory]; + const device = new Device_1.default(this.client, this.rootTopic, name, endpointId, normalizedCategory); this.devices.push(device); return device; } diff --git a/dist/AlexaInterface.js b/dist/AlexaInterface.js index 452ed28..826263c 100644 --- a/dist/AlexaInterface.js +++ b/dist/AlexaInterface.js @@ -164,11 +164,12 @@ class AlexaInterface { case AlexaInterfaceType.SCENE_CONTROLLER: case AlexaInterfaceType.SEEK_CONTROLLER: case AlexaInterfaceType.TEMPERATURE_SENSOR: - case AlexaInterfaceType.THERMOSTAT_CONTROLLER: case AlexaInterfaceType.TOGGLE_CONTROLLER: return "3"; case AlexaInterfaceType.LAUNCHER: return "1.1"; + case AlexaInterfaceType.THERMOSTAT_CONTROLLER: + return "3.2"; case AlexaInterfaceType.ENDPOINT_HEALTH: return "3.3"; default: @@ -197,6 +198,13 @@ class AlexaInterface { return ["toggleState"]; case AlexaInterfaceType.TEMPERATURE_SENSOR: return ["temperature"]; + case AlexaInterfaceType.THERMOSTAT_CONTROLLER: + return [ + "lowerSetpoint", + "upperSetpoint", + "thermostatMode", + "adaptiveRecoveryStatus", + ]; case AlexaInterfaceType.APPLICATION_STATE_REPORTER: case AlexaInterfaceType.AUDIO_PLAY_QUEUE: case AlexaInterfaceType.AUTHORIZATION_CONTROLLER: @@ -238,7 +246,6 @@ class AlexaInterface { case AlexaInterfaceType.SMART_VISION_SNAPSHOT_PROVIDER: case AlexaInterfaceType.SPEAKER: case AlexaInterfaceType.STEP_SPEAKER: - case AlexaInterfaceType.THERMOSTAT_CONTROLLER: case AlexaInterfaceType.THERMOSTAT_CONTROLLER_CONFIGURATION: case AlexaInterfaceType.THERMOSTAT_CONTROLLER_HVAC_COMPONENTS: case AlexaInterfaceType.THERMOSTAT_CONTROLLER_SCHEDULE: @@ -261,9 +268,15 @@ class AlexaInterface { properties: { retrievable: this.retrievable, proactivelyReported: this.proactivelyReported, - supported: [{ name: this.getProps()[0] }], + supported: this.getProps().map(name => ({ name })), }, }; + if (this.type == AlexaInterfaceType.THERMOSTAT_CONTROLLER) { + doc.configuration = { + "supportedModes": ["HEAT", "COOL", "AUTO", "OFF"], + "supportsScheduling": false + }; + } if (this.instance) { doc.instance = this.instance; } diff --git a/dist/AlexaStatusMessage.js b/dist/AlexaStatusMessage.js index 6bfd1fb..a076673 100644 --- a/dist/AlexaStatusMessage.js +++ b/dist/AlexaStatusMessage.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.AlexaStatusMessage = exports.TemperatureSensorScale = exports.PowerController = exports.EndpointHealth = void 0; +exports.AlexaStatusMessage = exports.TemperatureSensorScale = exports.PowerController = exports.ThermostatMode = exports.EndpointHealth = void 0; const uuid_1 = require("uuid"); const AlexaInterface_1 = require("./AlexaInterface"); var EndpointHealth; @@ -8,6 +8,15 @@ var EndpointHealth; EndpointHealth["OK"] = "OK"; EndpointHealth["UNREACHABLE"] = "UNREACHABLE"; })(EndpointHealth || (exports.EndpointHealth = EndpointHealth = {})); +var ThermostatMode; +(function (ThermostatMode) { + ThermostatMode["OFF"] = "OFF"; + ThermostatMode["HEAT"] = "HEAT"; + ThermostatMode["COOL"] = "COOL"; + ThermostatMode["AUTO"] = "AUTO"; + ThermostatMode["ECO"] = "ECO"; + ThermostatMode["CUSTOM"] = "CUSTOM"; +})(ThermostatMode || (exports.ThermostatMode = ThermostatMode = {})); var PowerController; (function (PowerController) { PowerController["ON"] = "ON"; @@ -57,6 +66,18 @@ class AlexaStatusMessage { this.context.properties.push(prop); return this; } + addThermostatModeProp(mode, uncertaintyInMs = 0) { + return this.addProperty(AlexaInterface_1.AlexaInterfaceType.THERMOSTAT_CONTROLLER, "thermostatMode", mode, uncertaintyInMs); + } + addThermostatControllerProp(name, scale, value, uncertaintyInMs = 0) { + const tempValue = { + value: scale === TemperatureSensorScale.FAHRENHEIT + ? (value - 32) * (5 / 9) + : value, + scale: "CELSIUS", + }; + return this.addProperty(AlexaInterface_1.AlexaInterfaceType.THERMOSTAT_CONTROLLER, name, tempValue, uncertaintyInMs); + } addHealthProp(health, uncertaintyInMs = 0) { return this.addProperty(AlexaInterface_1.AlexaInterfaceType.ENDPOINT_HEALTH, "connectivity", { value: health }, uncertaintyInMs); } diff --git a/dist/Device.js b/dist/Device.js index 7decf86..0a4ae97 100644 --- a/dist/Device.js +++ b/dist/Device.js @@ -5,7 +5,7 @@ const DisplayCategory_1 = require("./DisplayCategory"); const events_1 = require("events"); const AlexaStatusMessage_1 = require("./AlexaStatusMessage"); class Device extends events_1.EventEmitter { - constructor(mqttClient, rootTopic, name, endpointId, displayCategory = DisplayCategory_1.DisplayCategory.LIGHT, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") { + constructor(mqttClient, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") { super(); this.mqttClient = mqttClient; this.rootTopic = rootTopic; @@ -29,10 +29,10 @@ class Device extends events_1.EventEmitter { return this.endpointId; } setDisplayCategory(category) { - this.displayCategory = category; + this.displayCategory = Array.isArray(category) ? category : [category]; } getDisplayCategory() { - return this.displayCategory; + return this.displayCategory || [DisplayCategory_1.DisplayCategory.LIGHT]; } setDescription(description) { this.description = description; @@ -82,7 +82,7 @@ class Device extends events_1.EventEmitter { friendlyName: this.name, description: this.description, manufacturerName: this.manufacturerName, - displayCategories: [this.displayCategory], + displayCategories: this.getDisplayCategory(), additionalAttributes: { manufacturer: this.manufacturer, model: this.model, diff --git a/dist/index.js b/dist/index.js index 77b248c..6a32109 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.PowerController = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterfaceType = exports.Alex2MQTT = void 0; +exports.ThermostatMode = exports.TemperatureSensorScale = exports.EndpointHealth = exports.PowerController = exports.DisplayCategory = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterfaceType = exports.Alex2MQTT = void 0; var Alex2Node_1 = require("./Alex2Node"); Object.defineProperty(exports, "Alex2MQTT", { enumerable: true, get: function () { return __importDefault(Alex2Node_1).default; } }); var AlexaInterface_1 = require("./AlexaInterface"); @@ -11,5 +11,10 @@ Object.defineProperty(exports, "AlexaInterfaceType", { enumerable: true, get: fu var ActionMapping_1 = require("./ActionMapping"); Object.defineProperty(exports, "ActionMapping", { enumerable: true, get: function () { return ActionMapping_1.ActionMapping; } }); Object.defineProperty(exports, "AlexaActions", { enumerable: true, get: function () { return ActionMapping_1.AlexaActions; } }); +var DisplayCategory_1 = require("./DisplayCategory"); +Object.defineProperty(exports, "DisplayCategory", { enumerable: true, get: function () { return DisplayCategory_1.DisplayCategory; } }); var AlexaStatusMessage_1 = require("./AlexaStatusMessage"); Object.defineProperty(exports, "PowerController", { enumerable: true, get: function () { return AlexaStatusMessage_1.PowerController; } }); +Object.defineProperty(exports, "EndpointHealth", { enumerable: true, get: function () { return AlexaStatusMessage_1.EndpointHealth; } }); +Object.defineProperty(exports, "TemperatureSensorScale", { enumerable: true, get: function () { return AlexaStatusMessage_1.TemperatureSensorScale; } }); +Object.defineProperty(exports, "ThermostatMode", { enumerable: true, get: function () { return AlexaStatusMessage_1.ThermostatMode; } }); diff --git a/examples/ExampleThermostat.js b/examples/ExampleThermostat.js new file mode 100644 index 0000000..b4839e6 --- /dev/null +++ b/examples/ExampleThermostat.js @@ -0,0 +1,160 @@ +// Import required modules and types from the compiled TypeScript distribution (via dist/index.js) +const { + Alex2MQTT, + AlexaInterfaceType, + PowerController, + TemperatureSensorScale, + DisplayCategory, + ThermostatMode, +} = require("../dist/index.js"); + +require("dotenv").config(); // Load environment variables from .env file + +// Simulated in-memory device state (used for example/demo purposes) +let outputState = PowerController.OFF; // Initialize the power state to OFF (if thermostat supports ON/OFF modes) + +// Load MQTT connection credentials and root topic from environment variables +const username = process.env.MQTT_USERNAME; +const password = process.env.MQTT_PASSWORD; +const rootTopic = process.env.MQTT_ROOT_TOPIC; + +// Create and initialize a new Alexa-to-MQTT client +const alex2NodeClient = new Alex2MQTT(username, password, rootTopic, false); +alex2NodeClient.connect(); // Connect to the MQTT broker + +// Define the device name (Thermostat Test) +const deviceName = "Thermostat Test"; + +// Register the thermostat device with a unique endpoint ID +const thermostat = alex2NodeClient.registerDevice( + deviceName, + "thermostatEndpoint1", + [DisplayCategory.TEMPERATURE_SENSOR, DisplayCategory.THERMOSTAT] +); + +// Add thermostat-related capabilities +thermostat.addCapability(AlexaInterfaceType.THERMOSTAT_CONTROLLER); // Allows Alexa to control thermostat modes and setpoints +thermostat.addCapability(AlexaInterfaceType.TEMPERATURE_SENSOR); // Allows Alexa to read current temperature from the sensor + +// Log the device name to verify registration +console.log(thermostat.getName()); + +/** + * Handle Alexa's ReportState directive. + * This occurs when Alexa queries the current state of the thermostat (e.g., during routines or device status checks). + */ + +const deviceData = { + lowerSetpoint: 20, + upperSetpoint: 20, + targetSetpoint: 20, + current: 20, + thermostatMode: ThermostatMode.AUTO, +}; +thermostat.on("ReportState", (payload) => { + // console.log("ReportState received!", payload); + + const { correlationToken } = payload; + + let status = thermostat.getStatusMessage(correlationToken); + + status + .addHealthProp("OK") // Device is healthy and reachable + .addTemperatureSensorProp( + TemperatureSensorScale.CELSIUS, + deviceData.current + ) // Report current temperature + .addThermostatModeProp(deviceData.thermostatMode); // Report current thermostat mode + + if (deviceData.thermostatMode == "AUTO") { + status = status + .addThermostatControllerProp( + "lowerSetpoint", + TemperatureSensorScale.CELSIUS, + deviceData.lowerSetpoint + ) // Lower bound of temperature range + .addThermostatControllerProp( + "upperSetpoint", + TemperatureSensorScale.CELSIUS, + deviceData.upperSetpoint + ); // Upper bound of temperature range + } else { + status = status.addThermostatControllerProp( + "targetSetpoint", + TemperatureSensorScale.CELSIUS, + deviceData.targetSetpoint + ); // Lower bound of temperature range + } + status.send(); // Send the full state report back to Alexa +}); + +/** + * Handle incoming control directives for the thermostat. + * These directives come from Alexa when a user issues a command (e.g., to change temperature or mode). + */ +thermostat.on("Event", (directive, interfaceType) => { + console.log("Event received", { directive, interfaceType }); + if (interfaceType === AlexaInterfaceType.THERMOSTAT_CONTROLLER) { + const name = directive.header.name; + const token = directive.header.correlationToken; + + if (name == "SetTargetTemperature") { + console.log({ name: directive.payload }); + if (directive.payload.upperSetpoint) { + deviceData.upperSetpoint = praseTempValue( + directive.payload.upperSetpoint + ); + } + if (directive.payload.lowerSetpoint) { + deviceData.lowerSetpoint = praseTempValue( + directive.payload.lowerSetpoint + ); + } + if (directive.payload.targetSetpoint) { + deviceData.targetSetpoint = praseTempValue( + directive.payload.targetSetpoint + ); + } + } + if (name == "SetThermostatMode") { + console.log(directive.payload.thermostatMode); + if (directive.payload.thermostatMode) { + deviceData.thermostatMode = directive.payload.thermostatMode.value; + } + } + + let status = thermostat.getStatusMessage(token, true); + + status + .addHealthProp("OK") // Device is healthy and reachable + .addTemperatureSensorProp( + TemperatureSensorScale.CELSIUS, + deviceData.current + ) // Report current temperature + .addThermostatModeProp(deviceData.thermostatMode); // Report current thermostat mode + if (deviceData.thermostatMode == "AUTO") { + status = status + .addThermostatControllerProp( + "lowerSetpoint", + TemperatureSensorScale.CELSIUS, + deviceData.lowerSetpoint + ) // Lower bound of temperature range + .addThermostatControllerProp( + "upperSetpoint", + TemperatureSensorScale.CELSIUS, + deviceData.upperSetpoint + ); // Upper bound of temperature range + } else { + status = status.addThermostatControllerProp( + "targetSetpoint", + TemperatureSensorScale.CELSIUS, + deviceData.targetSetpoint + ); // Lower bound of temperature range + } + status.send(); // Send the full state report back to Alexa + } +}); + +function praseTempValue(data) { + return data.value; +} diff --git a/package.json b/package.json index 7a7ea67..db43f84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alex2node", - "version": "1.0.1", + "version": "1.1.0", "description": "A Node.js library for creating Alexa-compatible devices using MQTT, based on Alex2MQTT.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/Alex2Node.ts b/src/Alex2Node.ts index d0682ca..a9f3c1d 100644 --- a/src/Alex2Node.ts +++ b/src/Alex2Node.ts @@ -1,6 +1,7 @@ import mqtt, { MqttClient } from "mqtt"; import Device from "./Device"; import { EventEmitter } from "events"; +import { DisplayCategory } from "./DisplayCategory"; class Alex2MQTT extends EventEmitter { private client: MqttClient | null = null; @@ -15,7 +16,6 @@ class Alex2MQTT extends EventEmitter { ) { super(); // Initialize EventEmitter } - connect(): void { const options = { @@ -69,7 +69,7 @@ class Alex2MQTT extends EventEmitter { topic + "_r" }` ); - console.log(deviceArray); + console.log(JSON.stringify(deviceArray, null, 2)); } } ); @@ -106,8 +106,8 @@ class Alex2MQTT extends EventEmitter { ); } device.emit("ReportState", payload); - }else{ - device.emit("Event", payload,payload.header.namespace); + } else { + device.emit("Event", payload, payload.header.namespace); } } }); @@ -120,7 +120,11 @@ class Alex2MQTT extends EventEmitter { }); } - registerDevice(name: string, endpointId: string): Device { + registerDevice( + name: string, + endpointId: string, + displayCategory: DisplayCategory | DisplayCategory[] | null + ): Device { if (!this.client) { throw new Error("Must call connect before creating devices"); } @@ -129,7 +133,20 @@ class Alex2MQTT extends EventEmitter { `[Alex2Node.ts] Creating new device with endpoint: ${endpointId}` ); } - const device = new Device(this.client!, this.rootTopic, name, endpointId); + const normalizedCategory = + displayCategory === null + ? null + : Array.isArray(displayCategory) + ? displayCategory + : [displayCategory]; + + const device = new Device( + this.client!, + this.rootTopic, + name, + endpointId, + normalizedCategory + ); this.devices.push(device); return device; } diff --git a/src/AlexaInterface.ts b/src/AlexaInterface.ts index 9cdc58d..1f9bcf3 100644 --- a/src/AlexaInterface.ts +++ b/src/AlexaInterface.ts @@ -176,11 +176,12 @@ export class AlexaInterface { case AlexaInterfaceType.SCENE_CONTROLLER: case AlexaInterfaceType.SEEK_CONTROLLER: case AlexaInterfaceType.TEMPERATURE_SENSOR: - case AlexaInterfaceType.THERMOSTAT_CONTROLLER: case AlexaInterfaceType.TOGGLE_CONTROLLER: return "3"; case AlexaInterfaceType.LAUNCHER: return "1.1"; + case AlexaInterfaceType.THERMOSTAT_CONTROLLER: + return "3.2"; case AlexaInterfaceType.ENDPOINT_HEALTH: return "3.3"; default: @@ -210,7 +211,13 @@ export class AlexaInterface { return ["toggleState"]; case AlexaInterfaceType.TEMPERATURE_SENSOR: return ["temperature"]; - + case AlexaInterfaceType.THERMOSTAT_CONTROLLER: + return [ + "lowerSetpoint", + "upperSetpoint", + "thermostatMode", + "adaptiveRecoveryStatus", + ]; case AlexaInterfaceType.APPLICATION_STATE_REPORTER: case AlexaInterfaceType.AUDIO_PLAY_QUEUE: case AlexaInterfaceType.AUTHORIZATION_CONTROLLER: @@ -252,7 +259,6 @@ export class AlexaInterface { case AlexaInterfaceType.SMART_VISION_SNAPSHOT_PROVIDER: case AlexaInterfaceType.SPEAKER: case AlexaInterfaceType.STEP_SPEAKER: - case AlexaInterfaceType.THERMOSTAT_CONTROLLER: case AlexaInterfaceType.THERMOSTAT_CONTROLLER_CONFIGURATION: case AlexaInterfaceType.THERMOSTAT_CONTROLLER_HVAC_COMPONENTS: case AlexaInterfaceType.THERMOSTAT_CONTROLLER_SCHEDULE: @@ -277,9 +283,15 @@ export class AlexaInterface { properties: { retrievable: this.retrievable, proactivelyReported: this.proactivelyReported, - supported: [{ name: this.getProps()[0] }], + supported: this.getProps().map(name => ({ name })), }, }; + if(this.type==AlexaInterfaceType.THERMOSTAT_CONTROLLER){ + doc.configuration={ + "supportedModes": ["HEAT", "COOL", "AUTO", "OFF"], + "supportsScheduling": false + } + } if (this.instance) { doc.instance = this.instance; } diff --git a/src/AlexaStatusMessage.ts b/src/AlexaStatusMessage.ts index e8f4f38..fefa365 100644 --- a/src/AlexaStatusMessage.ts +++ b/src/AlexaStatusMessage.ts @@ -7,6 +7,15 @@ export enum EndpointHealth { UNREACHABLE = "UNREACHABLE", } +export enum ThermostatMode { + OFF = "OFF", + HEAT = "HEAT", + COOL = "COOL", + AUTO = "AUTO", + ECO = "ECO", + CUSTOM = "CUSTOM", +} + export enum PowerController { ON = "ON", OFF = "OFF", @@ -101,6 +110,38 @@ export class AlexaStatusMessage { this.context.properties.push(prop); return this; } + public addThermostatModeProp( + mode: string, + uncertaintyInMs = 0 + ): this { + return this.addProperty( + AlexaInterfaceType.THERMOSTAT_CONTROLLER, + "thermostatMode", + mode, + uncertaintyInMs + ); + } + + public addThermostatControllerProp( + name: "lowerSetpoint" | "upperSetpoint" | "targetSetpoint", + scale: TemperatureSensorScale, + value: number, + uncertaintyInMs = 0 + ): this { + const tempValue = { + value: + scale === TemperatureSensorScale.FAHRENHEIT + ? (value - 32) * (5 / 9) + : value, + scale: "CELSIUS", + }; + return this.addProperty( + AlexaInterfaceType.THERMOSTAT_CONTROLLER, + name, + tempValue, + uncertaintyInMs + ); + } public addHealthProp(health: EndpointHealth, uncertaintyInMs = 0): this { return this.addProperty( @@ -191,7 +232,7 @@ export class AlexaStatusMessage { context: this.context, event: this.event, }; - const topic = `${this.rootTopic}/${this.endpointId}/alexaResponce`;//Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too + const topic = `${this.rootTopic}/${this.endpointId}/alexaResponce`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too const payloadStr = JSON.stringify(payload); this.mqttClient.publish(topic, payloadStr, (err) => { diff --git a/src/Device.ts b/src/Device.ts index 80fa436..82d294a 100644 --- a/src/Device.ts +++ b/src/Device.ts @@ -13,7 +13,7 @@ class Device extends EventEmitter { private rootTopic: string, public name: string, public endpointId: string, - public displayCategory: DisplayCategory = DisplayCategory.LIGHT, + public displayCategory: Array | null, public description: string = "Alexa to Node.js bridge", public manufacturerName: string = "Alex2Node", public manufacturer: string = "Alex2Node", @@ -34,12 +34,12 @@ class Device extends EventEmitter { return this.endpointId; } - setDisplayCategory(category: DisplayCategory): void { - this.displayCategory = category; + setDisplayCategory(category: DisplayCategory | DisplayCategory[]): void { + this.displayCategory = Array.isArray(category) ? category : [category]; } - - getDisplayCategory(): DisplayCategory { - return this.displayCategory; + + getDisplayCategory(): Array { + return this.displayCategory ||[DisplayCategory.LIGHT]; } setDescription(description: string): void { @@ -111,7 +111,7 @@ class Device extends EventEmitter { friendlyName: this.name, description: this.description, manufacturerName: this.manufacturerName, - displayCategories: [this.displayCategory], + displayCategories: this.getDisplayCategory(), additionalAttributes: { manufacturer: this.manufacturer, model: this.model, diff --git a/src/index.ts b/src/index.ts index 77b9e07..4dde791 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export { default as Alex2MQTT } from './Alex2Node'; export { AlexaInterfaceType } from './AlexaInterface'; export { ActionMapping, AlexaActions } from './ActionMapping'; -export { PowerController } from './AlexaStatusMessage'; +export {DisplayCategory} from "./DisplayCategory"; +export { PowerController,EndpointHealth,TemperatureSensorScale,ThermostatMode } from './AlexaStatusMessage'; From 61b032c8896dfeb4abb9153ed29d36b2fa893259 Mon Sep 17 00:00:00 2001 From: David <43281665+chaos511@users.noreply.github.com> Date: Sun, 11 May 2025 23:12:47 +0000 Subject: [PATCH 03/10] add support and examples for deferred state report responses --- dist/AlexaStatusMessage.js | 28 +++++-- dist/Device.js | 4 +- examples/ExamplePowerController.js | 2 +- examples/ExamplePowerControllerDeferred.js | 90 ++++++++++++++++++++++ examples/ExampleThermostat.js | 2 +- package.json | 2 +- src/AlexaStatusMessage.ts | 38 ++++++--- src/Device.ts | 6 +- 8 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 examples/ExamplePowerControllerDeferred.js diff --git a/dist/AlexaStatusMessage.js b/dist/AlexaStatusMessage.js index a076673..0d8eeb3 100644 --- a/dist/AlexaStatusMessage.js +++ b/dist/AlexaStatusMessage.js @@ -28,15 +28,20 @@ var TemperatureSensorScale; TemperatureSensorScale["FAHRENHEIT"] = "FAHRENHEIT"; })(TemperatureSensorScale || (exports.TemperatureSensorScale = TemperatureSensorScale = {})); class AlexaStatusMessage { - constructor(correlationToken, rootTopic, endpointId, mqttClient, isResponse = false) { + constructor(correlationToken, rootTopic, endpointId, mqttClient, isResponse = false, isDeferred = false) { this.context = { properties: [] }; this.rootTopic = rootTopic; this.endpointId = endpointId; this.mqttClient = mqttClient; + this.isDeferred = isDeferred; this.event = { header: { namespace: "Alexa", - name: isResponse ? "Response" : "StateReport", + name: isDeferred + ? "DeferredResponse" + : isResponse + ? "Response" + : "StateReport", payloadVersion: "3", messageId: this.generateMessageId(), correlationToken, @@ -69,6 +74,17 @@ class AlexaStatusMessage { addThermostatModeProp(mode, uncertaintyInMs = 0) { return this.addProperty(AlexaInterface_1.AlexaInterfaceType.THERMOSTAT_CONTROLLER, "thermostatMode", mode, uncertaintyInMs); } + addEstimatedDeferralTime(seconds) { + if (this.isDeferred) { + this.event.payload = { + estimatedDeferralInSeconds: seconds, + }; + } + else { + console.warn("[AlexaStatusMessage.ts] Attempted to add estimated deferral time, but message is not marked as DeferredResponse."); + } + return this; + } addThermostatControllerProp(name, scale, value, uncertaintyInMs = 0) { const tempValue = { value: scale === TemperatureSensorScale.FAHRENHEIT @@ -106,19 +122,19 @@ class AlexaStatusMessage { this.context.properties.push(prop); return this; } - send() { + send(sendAsync) { const payload = { - context: this.context, + context: this.isDeferred ? null : this.context, event: this.event, }; - const topic = `${this.rootTopic}/${this.endpointId}/alexaResponce`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too + const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too const payloadStr = JSON.stringify(payload); this.mqttClient.publish(topic, payloadStr, (err) => { if (err) { console.error("[AlexaStatusMessage] Failed to publish status message:", err); } else { - // console.log(`[AlexaStatusMessage] Sent message to ${topic}`); + console.log(`[AlexaStatusMessage] Sent message to ${topic}`); } }); } diff --git a/dist/Device.js b/dist/Device.js index 0a4ae97..94084cf 100644 --- a/dist/Device.js +++ b/dist/Device.js @@ -40,8 +40,8 @@ class Device extends events_1.EventEmitter { getDescription() { return this.description; } - getStatusMessage(correlationToken, isResponse = false) { - return new AlexaStatusMessage_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.mqttClient, isResponse); + getStatusMessage(correlationToken, isResponse = false, isDeferred = false) { + return new AlexaStatusMessage_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.mqttClient, isResponse, isDeferred); } setManufacturerName(name) { this.manufacturerName = name; diff --git a/examples/ExamplePowerController.js b/examples/ExamplePowerController.js index 5658a55..ae0a35b 100644 --- a/examples/ExamplePowerController.js +++ b/examples/ExamplePowerController.js @@ -38,7 +38,7 @@ console.log(bedroomLight.getName()); bedroomLight.on("ReportState", (payload) => { console.log("ReportState received!", payload); - const { correlationToken } = payload; + const { correlationToken } = payload.header; const status = bedroomLight.getStatusMessage(correlationToken); diff --git a/examples/ExamplePowerControllerDeferred.js b/examples/ExamplePowerControllerDeferred.js new file mode 100644 index 0000000..1661318 --- /dev/null +++ b/examples/ExamplePowerControllerDeferred.js @@ -0,0 +1,90 @@ +// Import required modules and types from the compiled TypeScript distribution (via dist/index.js) +const { + Alex2MQTT, + AlexaInterfaceType, + PowerController +} = require("../dist/index.js"); + +require("dotenv").config(); // Load environment variables from .env file + +// Simulated in-memory device state (used for example/demo purposes) +let outputState = PowerController.OFF; // Initialize the power state to OFF + +// Load MQTT connection credentials and root topic from environment variables +const username = process.env.MQTT_USERNAME; +const password = process.env.MQTT_PASSWORD; +const rootTopic = process.env.MQTT_ROOT_TOPIC; + +// Create and initialize a new Alexa-to-MQTT client +const alex2NodeClient = new Alex2MQTT(username, password, rootTopic, false); +alex2NodeClient.connect(); // Connect to the MQTT broker + +// Define the device name (deferred light) +const deviceName = "Deferred Light"; + +// Register the device with a unique endpoint ID (you can use something like "endpoint1") +const deferredLight = alex2NodeClient.registerDevice(deviceName, "endpoint3"); + +// Add the PowerController capability (for turning on/off the device) +deferredLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER); + +// Log the device name to verify registration +console.log(deferredLight.getName()); + +/** + * Handle Alexa's ReportState directive. + * This occurs when Alexa queries the current state of the device (e.g., during routines or device status checks). + */ +deferredLight.on("ReportState", (payload) => { + console.log("ReportState received!", payload); + + const { correlationToken } = payload.header; + + // Step 1: Send a DeferredResponse after ~1 second + setTimeout(() => { + const deferred = deferredLight.getStatusMessage(correlationToken, false, true); // isDeferred = true + deferred.addEstimatedDeferralTime(20).send(); + console.log("Sent DeferredResponse"); + }, 2000); // simulate slight delay before deferred response + + // Step 2: Send actual StateReport after 10 seconds + setTimeout(() => { + const status = deferredLight.getStatusMessage(correlationToken,false,false); // normal StateReport + status + .addHealthProp("OK") + .addPowerControllerProp(outputState) + .send(true); + console.log("Sent actual StateReport with device status"); + }, 8000); +}); + + +/** + * Handle incoming control directives (e.g., TurnOn, TurnOff). + * These directives come from Alexa when a user issues a command. + */ +deferredLight.on("Event", (directive, interfaceType) => { + console.log("Event received", { directive, interfaceType }); + + // Ensure the interfaceType is either PowerController or other valid interfaces + if (interfaceType === AlexaInterfaceType.POWER_CONTROLLER) { + const name = directive.header.name; + const token = directive.header.correlationToken; + + // Update the internal state based on the command (TurnOn / TurnOff) + if (name === "TurnOn") { + outputState = PowerController.ON; + console.log("Turning ON the Deferred Light"); + } else if (name === "TurnOff") { + outputState = PowerController.OFF; + console.log("Turning OFF the Deferred Light"); + } + + // Respond to the directive with the updated device status + const status = deferredLight.getStatusMessage(token, true); + status + .addHealthProp("OK") + .addPowerControllerProp(outputState) + .send(); + } +}); diff --git a/examples/ExampleThermostat.js b/examples/ExampleThermostat.js index b4839e6..006858d 100644 --- a/examples/ExampleThermostat.js +++ b/examples/ExampleThermostat.js @@ -54,7 +54,7 @@ const deviceData = { thermostat.on("ReportState", (payload) => { // console.log("ReportState received!", payload); - const { correlationToken } = payload; + const { correlationToken } = payload.header; let status = thermostat.getStatusMessage(correlationToken); diff --git a/package.json b/package.json index db43f84..1274a8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alex2node", - "version": "1.1.0", + "version": "1.2.0", "description": "A Node.js library for creating Alexa-compatible devices using MQTT, based on Alex2MQTT.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/AlexaStatusMessage.ts b/src/AlexaStatusMessage.ts index fefa365..0cdb05a 100644 --- a/src/AlexaStatusMessage.ts +++ b/src/AlexaStatusMessage.ts @@ -57,22 +57,28 @@ export class AlexaStatusMessage { private rootTopic: string; private endpointId: string; private mqttClient: MqttClient; + private isDeferred: boolean; constructor( correlationToken: string, rootTopic: string, endpointId: string, mqttClient: MqttClient, - isResponse: boolean = false + isResponse: boolean = false, + isDeferred: boolean = false ) { this.rootTopic = rootTopic; this.endpointId = endpointId; this.mqttClient = mqttClient; - + this.isDeferred = isDeferred; this.event = { header: { namespace: "Alexa", - name: isResponse ? "Response" : "StateReport", + name: isDeferred + ? "DeferredResponse" + : isResponse + ? "Response" + : "StateReport", payloadVersion: "3", messageId: this.generateMessageId(), correlationToken, @@ -91,7 +97,6 @@ export class AlexaStatusMessage { private getTimestamp(): string { return new Date().toISOString(); } - private addProperty( namespace: AlexaInterfaceType, name: string, @@ -110,10 +115,7 @@ export class AlexaStatusMessage { this.context.properties.push(prop); return this; } - public addThermostatModeProp( - mode: string, - uncertaintyInMs = 0 - ): this { + public addThermostatModeProp(mode: string, uncertaintyInMs = 0): this { return this.addProperty( AlexaInterfaceType.THERMOSTAT_CONTROLLER, "thermostatMode", @@ -121,6 +123,18 @@ export class AlexaStatusMessage { uncertaintyInMs ); } + public addEstimatedDeferralTime(seconds: number): this { + if (this.isDeferred) { + this.event.payload = { + estimatedDeferralInSeconds: seconds, + }; + } else { + console.warn( + "[AlexaStatusMessage.ts] Attempted to add estimated deferral time, but message is not marked as DeferredResponse." + ); + } + return this; + } public addThermostatControllerProp( name: "lowerSetpoint" | "upperSetpoint" | "targetSetpoint", @@ -227,12 +241,12 @@ export class AlexaStatusMessage { return this; } - public send(): void { + public send(sendAsync: boolean): void { const payload = { - context: this.context, + context: this.isDeferred ? null : this.context, event: this.event, }; - const topic = `${this.rootTopic}/${this.endpointId}/alexaResponce`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too + const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync?"deferredResponse":"alexaResponce"}`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too const payloadStr = JSON.stringify(payload); this.mqttClient.publish(topic, payloadStr, (err) => { @@ -242,7 +256,7 @@ export class AlexaStatusMessage { err ); } else { - // console.log(`[AlexaStatusMessage] Sent message to ${topic}`); + console.log(`[AlexaStatusMessage] Sent message to ${topic}`); } }); } diff --git a/src/Device.ts b/src/Device.ts index 82d294a..cdcc22e 100644 --- a/src/Device.ts +++ b/src/Device.ts @@ -51,14 +51,16 @@ class Device extends EventEmitter { } getStatusMessage( correlationToken: string, - isResponse = false + isResponse = false, + isDeferred = false, ): AlexaStatusMessage { return new AlexaStatusMessage( correlationToken, this.rootTopic, this.endpointId, this.mqttClient, - isResponse + isResponse, + isDeferred ); } setManufacturerName(name: string): void { From 4af45ff7261146329a209df3d0cd1dcda2f887d0 Mon Sep 17 00:00:00 2001 From: David <43281665+chaos511@users.noreply.github.com> Date: Mon, 12 May 2025 16:03:15 +0000 Subject: [PATCH 04/10] Add support for sending Error Responses --- dist/AlexaErrorResponse.js | 125 ++++++++++++++++ dist/Device.js | 4 + dist/index.js | 5 +- examples/ExamplePowerControllerWithErrors.js | 80 ++++++++++ examples/ExampleThermostat.js | 35 +++-- package.json | 2 +- src/AlexaErrorResponse.ts | 148 +++++++++++++++++++ src/Device.ts | 15 +- src/index.ts | 1 + 9 files changed, 396 insertions(+), 19 deletions(-) create mode 100644 dist/AlexaErrorResponse.js create mode 100644 examples/ExamplePowerControllerWithErrors.js create mode 100644 src/AlexaErrorResponse.ts diff --git a/dist/AlexaErrorResponse.js b/dist/AlexaErrorResponse.js new file mode 100644 index 0000000..40577f5 --- /dev/null +++ b/dist/AlexaErrorResponse.js @@ -0,0 +1,125 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AlexaErrorResponse = exports.AlexaErrorType = void 0; +const uuid_1 = require("uuid"); +var AlexaErrorType; +(function (AlexaErrorType) { + AlexaErrorType["ALREADY_IN_OPERATION"] = "ALREADY_IN_OPERATION"; + AlexaErrorType["AUTHORIZATION_REQUIRED"] = "AUTHORIZATION_REQUIRED"; + AlexaErrorType["BRIDGE_UNREACHABLE"] = "BRIDGE_UNREACHABLE"; + AlexaErrorType["BYPASS_NEEDED"] = "BYPASS_NEEDED"; + AlexaErrorType["CLOUD_CONTROL_DISABLED"] = "CLOUD_CONTROL_DISABLED"; + AlexaErrorType["CHILD_LOCK"] = "CHILD_LOCK"; + AlexaErrorType["CONFIGURATION_UPDATE_NOT_ALLOWED"] = "CONFIGURATION_UPDATE_NOT_ALLOWED"; + AlexaErrorType["COOK_DURATION_TOO_LONG"] = "COOK_DURATION_TOO_LONG"; + AlexaErrorType["COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE"] = "COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE"; + AlexaErrorType["COOLING_STAGES_EXCEEDS_LIMIT"] = "COOLING_STAGES_EXCEEDS_LIMIT"; + AlexaErrorType["DATA_DELETION_NOT_SUPPORTED"] = "DATA_DELETION_NOT_SUPPORTED"; + AlexaErrorType["DATA_RETRIEVAL_NOT_SUPPORTED"] = "DATA_RETRIEVAL_NOT_SUPPORTED"; + AlexaErrorType["DEVICE_STUCK"] = "DEVICE_STUCK"; + AlexaErrorType["DISABLED_BY_USER"] = "DISABLED_BY_USER"; + AlexaErrorType["DO_NOT_DISTURB_MODE"] = "DO_NOT_DISTURB_MODE"; + AlexaErrorType["DOOR_CLOSED_TOO_LONG"] = "DOOR_CLOSED_TOO_LONG"; + AlexaErrorType["DOOR_OPEN"] = "DOOR_OPEN"; + AlexaErrorType["DUAL_SETPOINTS_UNSUPPORTED"] = "DUAL_SETPOINTS_UNSUPPORTED"; + AlexaErrorType["ENDPOINT_BUSY"] = "ENDPOINT_BUSY"; + AlexaErrorType["ENDPOINT_CONTROL_UNAVAILABLE"] = "ENDPOINT_CONTROL_UNAVAILABLE"; + AlexaErrorType["ENDPOINT_LOW_POWER"] = "ENDPOINT_LOW_POWER"; + AlexaErrorType["ENDPOINT_UNREACHABLE"] = "ENDPOINT_UNREACHABLE"; + AlexaErrorType["EXCEEDED_PIN_ATTEMPTS"] = "EXCEEDED_PIN_ATTEMPTS"; + AlexaErrorType["EXPIRED_AUTHORIZATION_CREDENTIAL"] = "EXPIRED_AUTHORIZATION_CREDENTIAL"; + AlexaErrorType["FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS"] = "FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS"; + AlexaErrorType["FIRMWARE_OUT_OF_DATE"] = "FIRMWARE_OUT_OF_DATE"; + AlexaErrorType["HARDWARE_MALFUNCTION"] = "HARDWARE_MALFUNCTION"; + AlexaErrorType["HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE"] = "HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE"; + AlexaErrorType["HEATING_STAGES_EXCEEDS_LIMIT"] = "HEATING_STAGES_EXCEEDS_LIMIT"; + AlexaErrorType["INSUFFICIENT_PERMISSIONS"] = "INSUFFICIENT_PERMISSIONS"; + AlexaErrorType["INSUFFICIENT_RESOURCE"] = "INSUFFICIENT_RESOURCE"; + AlexaErrorType["INSUFFICIENT_SPACE"] = "INSUFFICIENT_SPACE"; + AlexaErrorType["INTERNAL_ERROR"] = "INTERNAL_ERROR"; + AlexaErrorType["INVALID_AUTHORIZATION_CREDENTIAL"] = "INVALID_AUTHORIZATION_CREDENTIAL"; + AlexaErrorType["INVALID_AUXILIARY_HEATING_SYSTEM_TYPE"] = "INVALID_AUXILIARY_HEATING_SYSTEM_TYPE"; + AlexaErrorType["INVALID_DIRECTIVE"] = "INVALID_DIRECTIVE"; + AlexaErrorType["INVALID_SYSTEM_TYPE"] = "INVALID_SYSTEM_TYPE"; + AlexaErrorType["INVALID_TARGET_STATE"] = "INVALID_TARGET_STATE"; + AlexaErrorType["INVALID_TEMPERATURE_SCALE"] = "INVALID_TEMPERATURE_SCALE"; + AlexaErrorType["INVALID_TERMINAL_CONNECTION"] = "INVALID_TERMINAL_CONNECTION"; + AlexaErrorType["INVALID_VALUE"] = "INVALID_VALUE"; + AlexaErrorType["MAINTENANCE_REQUIRED"] = "MAINTENANCE_REQUIRED"; + AlexaErrorType["MAX_COMMISSIONING_LIMIT_REACHED"] = "MAX_COMMISSIONING_LIMIT_REACHED"; + AlexaErrorType["MISSING_SETUP_INFORMATION"] = "MISSING_SETUP_INFORMATION"; + AlexaErrorType["NO_SUCH_ENDPOINT"] = "NO_SUCH_ENDPOINT"; + AlexaErrorType["NOT_CALIBRATED"] = "NOT_CALIBRATED"; + AlexaErrorType["NOT_IN_OPERATION"] = "NOT_IN_OPERATION"; + AlexaErrorType["NOT_READY"] = "NOT_READY"; + AlexaErrorType["NOT_SUPPORTED_IN_CURRENT_MODE"] = "NOT_SUPPORTED_IN_CURRENT_MODE"; + AlexaErrorType["NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE"] = "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE"; + AlexaErrorType["OBSTACLE_DETECTED"] = "OBSTACLE_DETECTED"; + AlexaErrorType["PARTNER_APPLICATION_REDIRECTION"] = "PARTNER_APPLICATION_REDIRECTION"; + AlexaErrorType["PIN_SETUP_REQUIRED"] = "PIN_SETUP_REQUIRED"; + AlexaErrorType["POWER_LEVEL_NOT_SUPPORTED"] = "POWER_LEVEL_NOT_SUPPORTED"; + AlexaErrorType["PREHEAT_REQUIRED"] = "PREHEAT_REQUIRED"; + AlexaErrorType["PROBE_REQUIRED"] = "PROBE_REQUIRED"; + AlexaErrorType["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED"; + AlexaErrorType["REMOTE_START_NOT_SUPPORTED"] = "REMOTE_START_NOT_SUPPORTED"; + AlexaErrorType["REMOVE_PROBE"] = "REMOVE_PROBE"; + AlexaErrorType["REMOTE_START_DISABLED"] = "REMOTE_START_DISABLED"; + AlexaErrorType["REQUESTED_SETPOINTS_TOO_CLOSE"] = "REQUESTED_SETPOINTS_TOO_CLOSE"; + AlexaErrorType["SAFETY_BEAM_BREACHED"] = "SAFETY_BEAM_BREACHED"; + AlexaErrorType["SUBSCRIPTION_REQUIRED"] = "SUBSCRIPTION_REQUIRED"; + AlexaErrorType["TEMPERATURE_VALUE_OUT_OF_RANGE"] = "TEMPERATURE_VALUE_OUT_OF_RANGE"; + AlexaErrorType["THERMOSTAT_IS_OFF"] = "THERMOSTAT_IS_OFF"; + AlexaErrorType["TOO_MANY_FAILED_ATTEMPTS"] = "TOO_MANY_FAILED_ATTEMPTS"; + AlexaErrorType["TRIPLE_SETPOINTS_UNSUPPORTED"] = "TRIPLE_SETPOINTS_UNSUPPORTED"; + AlexaErrorType["UNABLE_TO_CHARGE"] = "UNABLE_TO_CHARGE"; + AlexaErrorType["UNAUTHORIZED"] = "UNAUTHORIZED"; + AlexaErrorType["UNCLEARED_ALARM"] = "UNCLEARED_ALARM"; + AlexaErrorType["UNSUPPORTED_THERMOSTAT_MODE"] = "UNSUPPORTED_THERMOSTAT_MODE"; + AlexaErrorType["UNCLEARED_TROUBLE"] = "UNCLEARED_TROUBLE"; + AlexaErrorType["UNWILLING_TO_SET_SCHEDULE"] = "UNWILLING_TO_SET_SCHEDULE"; + AlexaErrorType["UNWILLING_TO_SET_VALUE"] = "UNWILLING_TO_SET_VALUE"; + AlexaErrorType["VALUE_OUT_OF_RANGE"] = "VALUE_OUT_OF_RANGE"; +})(AlexaErrorType || (exports.AlexaErrorType = AlexaErrorType = {})); +class AlexaErrorResponse { + constructor(correlationToken, rootTopic, endpointId, mqttClient) { + this.rootTopic = rootTopic; + this.endpointId = endpointId; + this.mqttClient = mqttClient; + this.event = { + header: { + namespace: "Alexa", + name: "ErrorResponse", + messageId: this.generateMessageId(), + correlationToken, + payloadVersion: "3", + }, + endpoint: { + endpointId, + }, + payload: {}, + }; + } + generateMessageId() { + return (0, uuid_1.v4)(); + } + setErrorMessage(type, message, otherParams = {}) { + this.event.payload = Object.assign({ type, + message }, otherParams); + } + send(sendAsync = false) { + const payload = { + event: this.event, + }; + const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too + const payloadStr = JSON.stringify(payload); + this.mqttClient.publish(topic, payloadStr, (err) => { + if (err) { + console.error("[AlexaErrorResponse] Failed to publish status message:", err); + } + else { + console.log(`[AlexaErrorResponse] Sent message to ${topic}`); + } + }); + } +} +exports.AlexaErrorResponse = AlexaErrorResponse; diff --git a/dist/Device.js b/dist/Device.js index 94084cf..393310b 100644 --- a/dist/Device.js +++ b/dist/Device.js @@ -4,6 +4,7 @@ const AlexaInterface_1 = require("./AlexaInterface"); const DisplayCategory_1 = require("./DisplayCategory"); const events_1 = require("events"); const AlexaStatusMessage_1 = require("./AlexaStatusMessage"); +const AlexaErrorResponse_1 = require("./AlexaErrorResponse"); class Device extends events_1.EventEmitter { constructor(mqttClient, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") { super(); @@ -40,6 +41,9 @@ class Device extends events_1.EventEmitter { getDescription() { return this.description; } + getErrorMessage(correlationToken) { + return new AlexaErrorResponse_1.AlexaErrorResponse(correlationToken, this.rootTopic, this.endpointId, this.mqttClient); + } getStatusMessage(correlationToken, isResponse = false, isDeferred = false) { return new AlexaStatusMessage_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.mqttClient, isResponse, isDeferred); } diff --git a/dist/index.js b/dist/index.js index 6a32109..12e62bf 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ThermostatMode = exports.TemperatureSensorScale = exports.EndpointHealth = exports.PowerController = exports.DisplayCategory = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterfaceType = exports.Alex2MQTT = void 0; +exports.AlexaErrorResponse = exports.AlexaErrorType = exports.ThermostatMode = exports.TemperatureSensorScale = exports.EndpointHealth = exports.PowerController = exports.DisplayCategory = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterfaceType = exports.Alex2MQTT = void 0; var Alex2Node_1 = require("./Alex2Node"); Object.defineProperty(exports, "Alex2MQTT", { enumerable: true, get: function () { return __importDefault(Alex2Node_1).default; } }); var AlexaInterface_1 = require("./AlexaInterface"); @@ -18,3 +18,6 @@ Object.defineProperty(exports, "PowerController", { enumerable: true, get: funct Object.defineProperty(exports, "EndpointHealth", { enumerable: true, get: function () { return AlexaStatusMessage_1.EndpointHealth; } }); Object.defineProperty(exports, "TemperatureSensorScale", { enumerable: true, get: function () { return AlexaStatusMessage_1.TemperatureSensorScale; } }); Object.defineProperty(exports, "ThermostatMode", { enumerable: true, get: function () { return AlexaStatusMessage_1.ThermostatMode; } }); +var AlexaErrorResponse_1 = require("./AlexaErrorResponse"); +Object.defineProperty(exports, "AlexaErrorType", { enumerable: true, get: function () { return AlexaErrorResponse_1.AlexaErrorType; } }); +Object.defineProperty(exports, "AlexaErrorResponse", { enumerable: true, get: function () { return AlexaErrorResponse_1.AlexaErrorResponse; } }); diff --git a/examples/ExamplePowerControllerWithErrors.js b/examples/ExamplePowerControllerWithErrors.js new file mode 100644 index 0000000..ae0a35b --- /dev/null +++ b/examples/ExamplePowerControllerWithErrors.js @@ -0,0 +1,80 @@ +// Import required modules and types from the compiled TypeScript distribution (via dist/index.js) +const { + Alex2MQTT, + AlexaInterfaceType, + PowerController +} = require("../dist/index.js"); + +require("dotenv").config(); // Load environment variables from .env file + +// Simulated in-memory device state (used for example/demo purposes) +let outputState = PowerController.OFF; // Initialize the power state to OFF + +// Load MQTT connection credentials and root topic from environment variables +const username = process.env.MQTT_USERNAME; +const password = process.env.MQTT_PASSWORD; +const rootTopic = process.env.MQTT_ROOT_TOPIC; + +// Create and initialize a new Alexa-to-MQTT client +const alex2NodeClient = new Alex2MQTT(username, password, rootTopic, false); +alex2NodeClient.connect(); // Connect to the MQTT broker + +// Define the device name (bedroom light) +const deviceName = "Bedroom Light"; + +// Register the device with a unique endpoint ID (you can use something like "endpoint1") +const bedroomLight = alex2NodeClient.registerDevice(deviceName, "endpoint2"); + +// Add the PowerController capability (for turning on/off the device) +bedroomLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER); + +// Log the device name to verify registration +console.log(bedroomLight.getName()); + +/** + * Handle Alexa's ReportState directive. + * This occurs when Alexa queries the current state of the device (e.g., during routines or device status checks). + */ +bedroomLight.on("ReportState", (payload) => { + console.log("ReportState received!", payload); + + const { correlationToken } = payload.header; + + const status = bedroomLight.getStatusMessage(correlationToken); + + status + .addHealthProp("OK") // Device is healthy + .addPowerControllerProp(outputState); // Report the current power state (ON/OFF) + + status.send(); // Send the state report back to Alexa +}); + +/** + * Handle incoming control directives (e.g., TurnOn, TurnOff). + * These directives come from Alexa when a user issues a command. + */ +bedroomLight.on("Event", (directive, interfaceType) => { + console.log("Event received", { directive, interfaceType }); + + // Ensure the interfaceType is either PowerController or other valid interfaces + if (interfaceType === AlexaInterfaceType.POWER_CONTROLLER) { + const name = directive.header.name; + const token = directive.header.correlationToken; + + // Update the internal state based on the command (TurnOn / TurnOff) + if (name === "TurnOn") { + outputState = PowerController.ON; + console.log("Turning ON the Bedroom Light"); + } else if (name === "TurnOff") { + outputState = PowerController.OFF; + console.log("Turning OFF the Bedroom Light"); + } + + // Respond to the directive with the updated device status + const status = bedroomLight.getStatusMessage(token, true); + status + .addHealthProp("OK") + .addPowerControllerProp(outputState) + .send(); + } +}); diff --git a/examples/ExampleThermostat.js b/examples/ExampleThermostat.js index 006858d..ba3caef 100644 --- a/examples/ExampleThermostat.js +++ b/examples/ExampleThermostat.js @@ -45,11 +45,11 @@ console.log(thermostat.getName()); */ const deviceData = { - lowerSetpoint: 20, - upperSetpoint: 20, - targetSetpoint: 20, - current: 20, - thermostatMode: ThermostatMode.AUTO, + lowerSetpoint: 50, + upperSetpoint: 70, + targetSetpoint: 60, + current: 69, + thermostatMode: ThermostatMode.HEAT, }; thermostat.on("ReportState", (payload) => { // console.log("ReportState received!", payload); @@ -101,17 +101,17 @@ thermostat.on("Event", (directive, interfaceType) => { if (name == "SetTargetTemperature") { console.log({ name: directive.payload }); if (directive.payload.upperSetpoint) { - deviceData.upperSetpoint = praseTempValue( + deviceData.upperSetpoint = parseTempValue( directive.payload.upperSetpoint ); } if (directive.payload.lowerSetpoint) { - deviceData.lowerSetpoint = praseTempValue( + deviceData.lowerSetpoint = parseTempValue( directive.payload.lowerSetpoint ); } if (directive.payload.targetSetpoint) { - deviceData.targetSetpoint = praseTempValue( + deviceData.targetSetpoint = parseTempValue( directive.payload.targetSetpoint ); } @@ -128,7 +128,7 @@ thermostat.on("Event", (directive, interfaceType) => { status .addHealthProp("OK") // Device is healthy and reachable .addTemperatureSensorProp( - TemperatureSensorScale.CELSIUS, + TemperatureSensorScale.FAHRENHEIT, deviceData.current ) // Report current temperature .addThermostatModeProp(deviceData.thermostatMode); // Report current thermostat mode @@ -136,18 +136,18 @@ thermostat.on("Event", (directive, interfaceType) => { status = status .addThermostatControllerProp( "lowerSetpoint", - TemperatureSensorScale.CELSIUS, + TemperatureSensorScale.FAHRENHEIT, deviceData.lowerSetpoint ) // Lower bound of temperature range .addThermostatControllerProp( "upperSetpoint", - TemperatureSensorScale.CELSIUS, + TemperatureSensorScale.FAHRENHEIT, deviceData.upperSetpoint ); // Upper bound of temperature range } else { status = status.addThermostatControllerProp( "targetSetpoint", - TemperatureSensorScale.CELSIUS, + TemperatureSensorScale.FAHRENHEIT, deviceData.targetSetpoint ); // Lower bound of temperature range } @@ -155,6 +155,13 @@ thermostat.on("Event", (directive, interfaceType) => { } }); -function praseTempValue(data) { - return data.value; +function parseTempValue(data) { + console.log(data); + + if (data.scale === "FAHRENHEIT") { + return data.value; + } + + const fahrenheit = (data.value * 9) / 5 + 32; + return fahrenheit; } diff --git a/package.json b/package.json index 1274a8a..873d0b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alex2node", - "version": "1.2.0", + "version": "1.3.0", "description": "A Node.js library for creating Alexa-compatible devices using MQTT, based on Alex2MQTT.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/AlexaErrorResponse.ts b/src/AlexaErrorResponse.ts new file mode 100644 index 0000000..ee845df --- /dev/null +++ b/src/AlexaErrorResponse.ts @@ -0,0 +1,148 @@ +import { v4 as uuidv4 } from "uuid"; +import { AlexaInterface, AlexaInterfaceType } from "./AlexaInterface"; +import { MqttClient } from "mqtt"; + +export enum AlexaErrorType { + ALREADY_IN_OPERATION = "ALREADY_IN_OPERATION", + AUTHORIZATION_REQUIRED = "AUTHORIZATION_REQUIRED", + BRIDGE_UNREACHABLE = "BRIDGE_UNREACHABLE", + BYPASS_NEEDED = "BYPASS_NEEDED", + CLOUD_CONTROL_DISABLED = "CLOUD_CONTROL_DISABLED", + CHILD_LOCK = "CHILD_LOCK", + CONFIGURATION_UPDATE_NOT_ALLOWED = "CONFIGURATION_UPDATE_NOT_ALLOWED", + COOK_DURATION_TOO_LONG = "COOK_DURATION_TOO_LONG", + COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE = "COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", + COOLING_STAGES_EXCEEDS_LIMIT = "COOLING_STAGES_EXCEEDS_LIMIT", + DATA_DELETION_NOT_SUPPORTED = "DATA_DELETION_NOT_SUPPORTED", + DATA_RETRIEVAL_NOT_SUPPORTED = "DATA_RETRIEVAL_NOT_SUPPORTED", + DEVICE_STUCK = "DEVICE_STUCK", + DISABLED_BY_USER = "DISABLED_BY_USER", + DO_NOT_DISTURB_MODE = "DO_NOT_DISTURB_MODE", + DOOR_CLOSED_TOO_LONG = "DOOR_CLOSED_TOO_LONG", + DOOR_OPEN = "DOOR_OPEN", + DUAL_SETPOINTS_UNSUPPORTED = "DUAL_SETPOINTS_UNSUPPORTED", + ENDPOINT_BUSY = "ENDPOINT_BUSY", + ENDPOINT_CONTROL_UNAVAILABLE = "ENDPOINT_CONTROL_UNAVAILABLE", + ENDPOINT_LOW_POWER = "ENDPOINT_LOW_POWER", + ENDPOINT_UNREACHABLE = "ENDPOINT_UNREACHABLE", + EXCEEDED_PIN_ATTEMPTS = "EXCEEDED_PIN_ATTEMPTS", + EXPIRED_AUTHORIZATION_CREDENTIAL = "EXPIRED_AUTHORIZATION_CREDENTIAL", + FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS = "FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS", + FIRMWARE_OUT_OF_DATE = "FIRMWARE_OUT_OF_DATE", + HARDWARE_MALFUNCTION = "HARDWARE_MALFUNCTION", + HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE = "HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", + HEATING_STAGES_EXCEEDS_LIMIT = "HEATING_STAGES_EXCEEDS_LIMIT", + INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS", + INSUFFICIENT_RESOURCE = "INSUFFICIENT_RESOURCE", + INSUFFICIENT_SPACE = "INSUFFICIENT_SPACE", + INTERNAL_ERROR = "INTERNAL_ERROR", + INVALID_AUTHORIZATION_CREDENTIAL = "INVALID_AUTHORIZATION_CREDENTIAL", + INVALID_AUXILIARY_HEATING_SYSTEM_TYPE = "INVALID_AUXILIARY_HEATING_SYSTEM_TYPE", + INVALID_DIRECTIVE = "INVALID_DIRECTIVE", + INVALID_SYSTEM_TYPE = "INVALID_SYSTEM_TYPE", + INVALID_TARGET_STATE = "INVALID_TARGET_STATE", + INVALID_TEMPERATURE_SCALE = "INVALID_TEMPERATURE_SCALE", + INVALID_TERMINAL_CONNECTION = "INVALID_TERMINAL_CONNECTION", + INVALID_VALUE = "INVALID_VALUE", + MAINTENANCE_REQUIRED = "MAINTENANCE_REQUIRED", + MAX_COMMISSIONING_LIMIT_REACHED = "MAX_COMMISSIONING_LIMIT_REACHED", + MISSING_SETUP_INFORMATION = "MISSING_SETUP_INFORMATION", + NO_SUCH_ENDPOINT = "NO_SUCH_ENDPOINT", + NOT_CALIBRATED = "NOT_CALIBRATED", + NOT_IN_OPERATION = "NOT_IN_OPERATION", + NOT_READY = "NOT_READY", + NOT_SUPPORTED_IN_CURRENT_MODE = "NOT_SUPPORTED_IN_CURRENT_MODE", + NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE = "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE", + OBSTACLE_DETECTED = "OBSTACLE_DETECTED", + PARTNER_APPLICATION_REDIRECTION = "PARTNER_APPLICATION_REDIRECTION", + PIN_SETUP_REQUIRED = "PIN_SETUP_REQUIRED", + POWER_LEVEL_NOT_SUPPORTED = "POWER_LEVEL_NOT_SUPPORTED", + PREHEAT_REQUIRED = "PREHEAT_REQUIRED", + PROBE_REQUIRED = "PROBE_REQUIRED", + RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED", + REMOTE_START_NOT_SUPPORTED = "REMOTE_START_NOT_SUPPORTED", + REMOVE_PROBE = "REMOVE_PROBE", + REMOTE_START_DISABLED = "REMOTE_START_DISABLED", + REQUESTED_SETPOINTS_TOO_CLOSE = "REQUESTED_SETPOINTS_TOO_CLOSE", + SAFETY_BEAM_BREACHED = "SAFETY_BEAM_BREACHED", + SUBSCRIPTION_REQUIRED = "SUBSCRIPTION_REQUIRED", + TEMPERATURE_VALUE_OUT_OF_RANGE = "TEMPERATURE_VALUE_OUT_OF_RANGE", + THERMOSTAT_IS_OFF = "THERMOSTAT_IS_OFF", + TOO_MANY_FAILED_ATTEMPTS = "TOO_MANY_FAILED_ATTEMPTS", + TRIPLE_SETPOINTS_UNSUPPORTED = "TRIPLE_SETPOINTS_UNSUPPORTED", + UNABLE_TO_CHARGE = "UNABLE_TO_CHARGE", + UNAUTHORIZED = "UNAUTHORIZED", + UNCLEARED_ALARM = "UNCLEARED_ALARM", + UNSUPPORTED_THERMOSTAT_MODE = "UNSUPPORTED_THERMOSTAT_MODE", + UNCLEARED_TROUBLE = "UNCLEARED_TROUBLE", + UNWILLING_TO_SET_SCHEDULE = "UNWILLING_TO_SET_SCHEDULE", + UNWILLING_TO_SET_VALUE = "UNWILLING_TO_SET_VALUE", + VALUE_OUT_OF_RANGE = "VALUE_OUT_OF_RANGE", +} + +export class AlexaErrorResponse { + private event: any; + private rootTopic: string; + private endpointId: string; + private mqttClient: MqttClient; + + constructor( + correlationToken: string, + rootTopic: string, + endpointId: string, + mqttClient: MqttClient + ) { + this.rootTopic = rootTopic; + this.endpointId = endpointId; + this.mqttClient = mqttClient; + + this.event = { + header: { + namespace: "Alexa", + name: "ErrorResponse", + messageId: this.generateMessageId(), + correlationToken, + payloadVersion: "3", + }, + endpoint: { + endpointId, + }, + payload: {}, + }; + } + private generateMessageId(): string { + return uuidv4(); + } + setErrorMessage( + type: string, + message: string, + otherParams: Record = {} + ) { + this.event.payload = { + type, + message, + ...otherParams, + }; + } + + send(sendAsync = false) { + const payload = { + event: this.event, + }; + const topic = `${this.rootTopic}/${this.endpointId}/${ + sendAsync ? "deferredResponse" : "alexaResponce" + }`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too + const payloadStr = JSON.stringify(payload); + + this.mqttClient.publish(topic, payloadStr, (err) => { + if (err) { + console.error( + "[AlexaErrorResponse] Failed to publish status message:", + err + ); + } else { + console.log(`[AlexaErrorResponse] Sent message to ${topic}`); + } + }); + } +} diff --git a/src/Device.ts b/src/Device.ts index cdcc22e..74cb6e5 100644 --- a/src/Device.ts +++ b/src/Device.ts @@ -3,6 +3,7 @@ import { DisplayCategory } from "./DisplayCategory"; import { EventEmitter } from "events"; import { MqttClient } from "mqtt"; import { AlexaStatusMessage } from "./AlexaStatusMessage"; +import { AlexaErrorResponse } from "./AlexaErrorResponse"; class Device extends EventEmitter { protected softwareVersion: string = "1.0.0"; @@ -37,9 +38,9 @@ class Device extends EventEmitter { setDisplayCategory(category: DisplayCategory | DisplayCategory[]): void { this.displayCategory = Array.isArray(category) ? category : [category]; } - + getDisplayCategory(): Array { - return this.displayCategory ||[DisplayCategory.LIGHT]; + return this.displayCategory || [DisplayCategory.LIGHT]; } setDescription(description: string): void { @@ -49,10 +50,18 @@ class Device extends EventEmitter { getDescription(): string { return this.description; } + getErrorMessage(correlationToken: string): AlexaErrorResponse { + return new AlexaErrorResponse( + correlationToken, + this.rootTopic, + this.endpointId, + this.mqttClient + ); + } getStatusMessage( correlationToken: string, isResponse = false, - isDeferred = false, + isDeferred = false ): AlexaStatusMessage { return new AlexaStatusMessage( correlationToken, diff --git a/src/index.ts b/src/index.ts index 4dde791..f381872 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export { AlexaInterfaceType } from './AlexaInterface'; export { ActionMapping, AlexaActions } from './ActionMapping'; export {DisplayCategory} from "./DisplayCategory"; export { PowerController,EndpointHealth,TemperatureSensorScale,ThermostatMode } from './AlexaStatusMessage'; +export { AlexaErrorType,AlexaErrorResponse} from './AlexaErrorResponse'; From 689a357df0a7a23d15885f9b1b19bfc6f0faaf58 Mon Sep 17 00:00:00 2001 From: David <43281665+chaos511@users.noreply.github.com> Date: Mon, 12 May 2025 16:33:49 +0000 Subject: [PATCH 05/10] fix example with errors --- examples/ExamplePowerControllerWithErrors.js | 102 +++++++++++-------- 1 file changed, 57 insertions(+), 45 deletions(-) diff --git a/examples/ExamplePowerControllerWithErrors.js b/examples/ExamplePowerControllerWithErrors.js index ae0a35b..3b1a59e 100644 --- a/examples/ExamplePowerControllerWithErrors.js +++ b/examples/ExamplePowerControllerWithErrors.js @@ -1,77 +1,89 @@ -// Import required modules and types from the compiled TypeScript distribution (via dist/index.js) -const { - Alex2MQTT, - AlexaInterfaceType, - PowerController +const { + Alex2MQTT, + AlexaInterfaceType, + PowerController, + AlexaErrorType, } = require("../dist/index.js"); -require("dotenv").config(); // Load environment variables from .env file +require("dotenv").config(); -// Simulated in-memory device state (used for example/demo purposes) -let outputState = PowerController.OFF; // Initialize the power state to OFF +let outputState = PowerController.OFF; -// Load MQTT connection credentials and root topic from environment variables const username = process.env.MQTT_USERNAME; const password = process.env.MQTT_PASSWORD; const rootTopic = process.env.MQTT_ROOT_TOPIC; -// Create and initialize a new Alexa-to-MQTT client const alex2NodeClient = new Alex2MQTT(username, password, rootTopic, false); -alex2NodeClient.connect(); // Connect to the MQTT broker +alex2NodeClient.connect(); -// Define the device name (bedroom light) -const deviceName = "Bedroom Light"; +const errorLight = alex2NodeClient.registerDevice("bad light", "endpoint4"); +errorLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER); -// Register the device with a unique endpoint ID (you can use something like "endpoint1") -const bedroomLight = alex2NodeClient.registerDevice(deviceName, "endpoint2"); +console.log(errorLight.getName()); -// Add the PowerController capability (for turning on/off the device) -bedroomLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER); - -// Log the device name to verify registration -console.log(bedroomLight.getName()); +// Error cycling setup +const errorCycle = [ + AlexaErrorType.ENDPOINT_LOW_POWER, + AlexaErrorType.VALUE_OUT_OF_RANGE, + AlexaErrorType.HARDWARE_MALFUNCTION, + AlexaErrorType.BRIDGE_UNREACHABLE, + AlexaErrorType.FIRMWARE_OUT_OF_DATE, + AlexaErrorType.NOT_CALIBRATED, + AlexaErrorType.RATE_LIMIT_EXCEEDED, +]; +let currentErrorIndex = 0; /** - * Handle Alexa's ReportState directive. - * This occurs when Alexa queries the current state of the device (e.g., during routines or device status checks). + * Always return ENDPOINT_BUSY error on state queries. */ -bedroomLight.on("ReportState", (payload) => { +errorLight.on("ReportState", (payload) => { console.log("ReportState received!", payload); const { correlationToken } = payload.header; + const errorResponse = errorLight.getErrorMessage(correlationToken); - const status = bedroomLight.getStatusMessage(correlationToken); - - status - .addHealthProp("OK") // Device is healthy - .addPowerControllerProp(outputState); // Report the current power state (ON/OFF) - - status.send(); // Send the state report back to Alexa + errorResponse.setErrorMessage( + AlexaErrorType.HARDWARE_MALFUNCTION, + "The device is currently busy and cannot report its state." + ); + console.log("Sending error: HARDWARE_MALFUNCTION"); + errorResponse.send(); }); /** - * Handle incoming control directives (e.g., TurnOn, TurnOff). - * These directives come from Alexa when a user issues a command. + * Handle control directives with 90% error chance, cycling through fixed errors. */ -bedroomLight.on("Event", (directive, interfaceType) => { - console.log("Event received", { directive, interfaceType }); - - // Ensure the interfaceType is either PowerController or other valid interfaces +errorLight.on("Event", (directive, interfaceType) => { if (interfaceType === AlexaInterfaceType.POWER_CONTROLLER) { const name = directive.header.name; const token = directive.header.correlationToken; - // Update the internal state based on the command (TurnOn / TurnOff) - if (name === "TurnOn") { - outputState = PowerController.ON; - console.log("Turning ON the Bedroom Light"); - } else if (name === "TurnOff") { - outputState = PowerController.OFF; - console.log("Turning OFF the Bedroom Light"); + const shouldError = Math.random() < 0.9; + + if (shouldError) { + const errorType = errorCycle[currentErrorIndex]; + currentErrorIndex = (currentErrorIndex + 1) % errorCycle.length; + + const errorResponse = errorLight.getErrorMessage(token); + errorResponse.setErrorMessage( + errorType, + `Failed to process ${name} due to ${errorType.replace(/_/g, " ").toLowerCase()}.` + ); + console.log(`Sending error: ${errorType}`); + errorResponse.send(); + return; } - // Respond to the directive with the updated device status - const status = bedroomLight.getStatusMessage(token, true); + // Success path + if (name === "TurnOn") { + outputState = PowerController.ON; + console.log("Turning ON the Light"); + } else if (name === "TurnOff") { + outputState = PowerController.OFF; + console.log("Turning OFF the Light"); + } + + const status = errorLight.getStatusMessage(token, true); status .addHealthProp("OK") .addPowerControllerProp(outputState) From e7a2079ff7be89b6a05f70af8d140562e374a414 Mon Sep 17 00:00:00 2001 From: David <43281665+chaos511@users.noreply.github.com> Date: Mon, 12 May 2025 16:46:33 +0000 Subject: [PATCH 06/10] Fix typo in comments --- examples/ExamplePowerControllerWithErrors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ExamplePowerControllerWithErrors.js b/examples/ExamplePowerControllerWithErrors.js index 3b1a59e..de20d68 100644 --- a/examples/ExamplePowerControllerWithErrors.js +++ b/examples/ExamplePowerControllerWithErrors.js @@ -34,7 +34,7 @@ const errorCycle = [ let currentErrorIndex = 0; /** - * Always return ENDPOINT_BUSY error on state queries. + * Always return HARDWARE_MALFUNCTION error on state queries. */ errorLight.on("ReportState", (payload) => { console.log("ReportState received!", payload); From 3bcd767127526d4130d0f32acd4f2d4d3e6daac3 Mon Sep 17 00:00:00 2001 From: David <43281665+chaos511@users.noreply.github.com> Date: Mon, 12 May 2025 18:23:58 +0000 Subject: [PATCH 07/10] Add full support for mode controller --- dist/Alex2Node.js | 3 +- dist/AlexaInterface.js | 14 +++- dist/AlexaStatusMessage.js | 3 + examples/ExampleModeController.js | 118 ++++++++++++++++++++++++++++++ examples/ExampleThermostat.js | 41 ++++++++++- src/Alex2Node.ts | 2 +- src/AlexaInterface.ts | 14 +++- src/AlexaStatusMessage.ts | 9 +++ 8 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 examples/ExampleModeController.js diff --git a/dist/Alex2Node.js b/dist/Alex2Node.js index d44c7ce..58758af 100644 --- a/dist/Alex2Node.js +++ b/dist/Alex2Node.js @@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const mqtt_1 = __importDefault(require("mqtt")); const Device_1 = __importDefault(require("./Device")); const events_1 = require("events"); +const DisplayCategory_1 = require("./DisplayCategory"); class Alex2MQTT extends events_1.EventEmitter { constructor(username, password, rootTopic, debugLogging) { super(); // Initialize EventEmitter @@ -114,7 +115,7 @@ class Alex2MQTT extends events_1.EventEmitter { : Array.isArray(displayCategory) ? displayCategory : [displayCategory]; - const device = new Device_1.default(this.client, this.rootTopic, name, endpointId, normalizedCategory); + const device = new Device_1.default(this.client, this.rootTopic, name, endpointId, normalizedCategory || [DisplayCategory_1.DisplayCategory.LIGHT]); this.devices.push(device); return device; } diff --git a/dist/AlexaInterface.js b/dist/AlexaInterface.js index 826263c..0febeab 100644 --- a/dist/AlexaInterface.js +++ b/dist/AlexaInterface.js @@ -82,6 +82,7 @@ class AlexaInterface { this.instance = instance; this.friendlyNames = []; this.actionMappings = []; + this.supportedModes = []; } addActionMapping(mapping) { this.actionMappings.push(mapping); @@ -89,6 +90,9 @@ class AlexaInterface { addFriendlyName(name, locale) { this.friendlyNames.push({ text: name, locale }); } + addSupportedModes(modes) { + this.supportedModes = modes; + } setInstance(name) { this.instance = name; } @@ -196,6 +200,8 @@ class AlexaInterface { return ["connectivity"]; case AlexaInterfaceType.TOGGLE_CONTROLLER: return ["toggleState"]; + case AlexaInterfaceType.MODE_CONTROLLER: + return ["mode"]; case AlexaInterfaceType.TEMPERATURE_SENSOR: return ["temperature"]; case AlexaInterfaceType.THERMOSTAT_CONTROLLER: @@ -228,7 +234,6 @@ class AlexaInterface { case AlexaInterfaceType.LOCK_CONTROLLER: case AlexaInterfaceType.MEDIA_PLAYBACK: case AlexaInterfaceType.MEDIA_SEARCH: - case AlexaInterfaceType.MODE_CONTROLLER: case AlexaInterfaceType.MOTION_SENSOR: case AlexaInterfaceType.PERCENTAGE_CONTROLLER: case AlexaInterfaceType.PLAYBACK_CONTROLLER: @@ -277,6 +282,12 @@ class AlexaInterface { "supportsScheduling": false }; } + else if (this.supportedModes.length > 0) { + doc["configuration"] = { + ordered: true, + supportedModes: this.supportedModes, + }; + } if (this.instance) { doc.instance = this.instance; } @@ -291,6 +302,7 @@ class AlexaInterface { }; friendlyNamesArray.push(friendlyNameObj); }); + capabilityResources.friendlyNames = friendlyNamesArray; doc["capabilityResources"] = capabilityResources; } if (this.actionMappings.length > 0) { diff --git a/dist/AlexaStatusMessage.js b/dist/AlexaStatusMessage.js index 0d8eeb3..64dbeba 100644 --- a/dist/AlexaStatusMessage.js +++ b/dist/AlexaStatusMessage.js @@ -71,6 +71,9 @@ class AlexaStatusMessage { this.context.properties.push(prop); return this; } + addModeControllerProp(instance, value, uncertaintyInMs = 0) { + return this.addProperty(AlexaInterface_1.AlexaInterfaceType.MODE_CONTROLLER, "mode", value, uncertaintyInMs, instance); + } addThermostatModeProp(mode, uncertaintyInMs = 0) { return this.addProperty(AlexaInterface_1.AlexaInterfaceType.THERMOSTAT_CONTROLLER, "thermostatMode", mode, uncertaintyInMs); } diff --git a/examples/ExampleModeController.js b/examples/ExampleModeController.js new file mode 100644 index 0000000..0313d5e --- /dev/null +++ b/examples/ExampleModeController.js @@ -0,0 +1,118 @@ +// Import required modules and types from the compiled TypeScript distribution (via dist/index.js) +const { + Alex2MQTT, + AlexaInterfaceType, + PowerController, + DisplayCategory, +} = require("../dist/index.js"); + +require("dotenv").config(); // Load environment variables from .env file + +// Simulated in-memory device state (used for example/demo purposes) +let currentMode = "mode.fanmode.Normal"; + +// Load MQTT connection credentials and root topic from environment variables +const username = process.env.MQTT_USERNAME; +const password = process.env.MQTT_PASSWORD; +const rootTopic = process.env.MQTT_ROOT_TOPIC; + +// Create and initialize a new Alexa-to-MQTT client +const alex2NodeClient = new Alex2MQTT(username, password, rootTopic, false); +alex2NodeClient.connect(); // Connect to the MQTT broker + +// Define the device name (Test mode controller) +const deviceName = "Test mode controller"; + +// Register the device with a unique endpoint ID (you can use something like "endpoint1") +const modeControllerDevice = alex2NodeClient.registerDevice( + deviceName, + "endpoint6", + [DisplayCategory.OTHER] +); + +// Add the 'MODE_CONTROLLER' capability for controlling device modes +const modeController = modeControllerDevice.addCapability( + AlexaInterfaceType.MODE_CONTROLLER +); + +modeController.setInstance("mode.fanmode"); +modeController.addFriendlyName("Fan Mode", "en-US"); +modeController.addSupportedModes([ + { + value: "mode.fanmode.Normal", + modeResources: { + friendlyNames: [ + { + "@type": "text", + value: { + text: "Normal", + locale: "en-US", + }, + }, + ], + }, + }, + { + value: "mode.fanmode.Low", + modeResources: { + friendlyNames: [ + { + "@type": "text", + value: { + text: "Low", + locale: "en-US", + }, + }, + ], + }, + }, +]); + +// Log the device name to verify registration +console.log(modeControllerDevice.getName()); + +/** + * Handle Alexa's ReportState directive. + * This occurs when Alexa queries the current state of the device (e.g., during routines or device status checks). + */ +modeControllerDevice.on("ReportState", (payload) => { + console.log("ReportState received!", payload); + + const { correlationToken } = payload.header; + + const status = modeControllerDevice.getStatusMessage(correlationToken); + + status + .addHealthProp("OK") // Device is healthy + .addModeControllerProp("mode.fanmode", currentMode); + + status.send(); // Send the state report back to Alexa +}); + +/** + * Handle incoming control directives (e.g., SetMode). + * These directives come from Alexa when a user issues a command. + */ +modeControllerDevice.on("Event", (directive, interfaceType) => { + console.log("Event received", { directive, interfaceType }); + // Ensure the interfaceType is either PowerController or other valid interfaces + if (interfaceType === AlexaInterfaceType.MODE_CONTROLLER) { + const name = directive.header.name; + const token = directive.header.correlationToken; + const instance = directive.header.instance; + + if (instance == "mode.fanmode") { + if (name === "SetMode") { + currentMode = directive.payload.mode; + } + } + + const status = modeControllerDevice.getStatusMessage(token, true); + + status + .addHealthProp("OK") // Device is healthy + .addModeControllerProp("mode.fanmode", currentMode); + + status.send(); // Send the state report back to Alexa + } +}); diff --git a/examples/ExampleThermostat.js b/examples/ExampleThermostat.js index ba3caef..a8737b9 100644 --- a/examples/ExampleThermostat.js +++ b/examples/ExampleThermostat.js @@ -29,12 +29,48 @@ const deviceName = "Thermostat Test"; const thermostat = alex2NodeClient.registerDevice( deviceName, "thermostatEndpoint1", - [DisplayCategory.TEMPERATURE_SENSOR, DisplayCategory.THERMOSTAT] + [DisplayCategory.TEMPERATURE_SENSOR, DisplayCategory.THERMOSTAT,DisplayCategory.OTHER] ); // Add thermostat-related capabilities thermostat.addCapability(AlexaInterfaceType.THERMOSTAT_CONTROLLER); // Allows Alexa to control thermostat modes and setpoints thermostat.addCapability(AlexaInterfaceType.TEMPERATURE_SENSOR); // Allows Alexa to read current temperature from the sensor +const modeController = thermostat.addCapability( + AlexaInterfaceType.MODE_CONTROLLER +); + +modeController.setInstance("mode.fanmode"); +modeController.addFriendlyName("Fan", "en-US"); +modeController.addSupportedModes([ + { + value: "mode.fanmode.Auto", + modeResources: { + friendlyNames: [ + { + "@type": "text", + value: { + text: "Bannana", + locale: "en-US", + }, + }, + ], + }, + }, + { + value: "mode.fanmode.On", + modeResources: { + friendlyNames: [ + { + "@type": "text", + value: { + text: "Apple", + locale: "en-US", + }, + }, + ], + }, + }, +]); // Log the device name to verify registration console.log(thermostat.getName()); @@ -50,6 +86,7 @@ const deviceData = { targetSetpoint: 60, current: 69, thermostatMode: ThermostatMode.HEAT, + fanMode:"mode.fanmode.Auto" }; thermostat.on("ReportState", (payload) => { // console.log("ReportState received!", payload); @@ -60,6 +97,7 @@ thermostat.on("ReportState", (payload) => { status .addHealthProp("OK") // Device is healthy and reachable + .addModeControllerProp("mode.fanmode", deviceData.fanMode) .addTemperatureSensorProp( TemperatureSensorScale.CELSIUS, deviceData.current @@ -127,6 +165,7 @@ thermostat.on("Event", (directive, interfaceType) => { status .addHealthProp("OK") // Device is healthy and reachable + .addModeControllerProp("mode.fanmode", deviceData.fanMode) .addTemperatureSensorProp( TemperatureSensorScale.FAHRENHEIT, deviceData.current diff --git a/src/Alex2Node.ts b/src/Alex2Node.ts index a9f3c1d..4fd3793 100644 --- a/src/Alex2Node.ts +++ b/src/Alex2Node.ts @@ -145,7 +145,7 @@ class Alex2MQTT extends EventEmitter { this.rootTopic, name, endpointId, - normalizedCategory + normalizedCategory|| [DisplayCategory.LIGHT] ); this.devices.push(device); return device; diff --git a/src/AlexaInterface.ts b/src/AlexaInterface.ts index 1f9bcf3..31fac2d 100644 --- a/src/AlexaInterface.ts +++ b/src/AlexaInterface.ts @@ -81,6 +81,7 @@ interface FriendlyName { export class AlexaInterface { private friendlyNames: FriendlyName[] = []; private actionMappings: ActionMapping[] = []; + private supportedModes: string[] = []; constructor( public type: AlexaInterfaceType, @@ -96,7 +97,9 @@ export class AlexaInterface { addFriendlyName(name: string, locale: string): void { this.friendlyNames.push({ text: name, locale }); } - + addSupportedModes(modes: string[]): void { + this.supportedModes = modes; + } setInstance(name: string): void { this.instance = name; } @@ -209,6 +212,8 @@ export class AlexaInterface { return ["connectivity"]; case AlexaInterfaceType.TOGGLE_CONTROLLER: return ["toggleState"]; + case AlexaInterfaceType.MODE_CONTROLLER: + return ["mode"]; case AlexaInterfaceType.TEMPERATURE_SENSOR: return ["temperature"]; case AlexaInterfaceType.THERMOSTAT_CONTROLLER: @@ -241,7 +246,6 @@ export class AlexaInterface { case AlexaInterfaceType.LOCK_CONTROLLER: case AlexaInterfaceType.MEDIA_PLAYBACK: case AlexaInterfaceType.MEDIA_SEARCH: - case AlexaInterfaceType.MODE_CONTROLLER: case AlexaInterfaceType.MOTION_SENSOR: case AlexaInterfaceType.PERCENTAGE_CONTROLLER: case AlexaInterfaceType.PLAYBACK_CONTROLLER: @@ -291,6 +295,11 @@ export class AlexaInterface { "supportedModes": ["HEAT", "COOL", "AUTO", "OFF"], "supportsScheduling": false } + }else if (this.supportedModes.length > 0) { + doc["configuration"] = { + ordered:true, + supportedModes: this.supportedModes, + }; } if (this.instance) { doc.instance = this.instance; @@ -306,6 +315,7 @@ export class AlexaInterface { }; friendlyNamesArray.push(friendlyNameObj); }); + capabilityResources.friendlyNames=friendlyNamesArray doc["capabilityResources"] = capabilityResources; } if (this.actionMappings.length > 0) { diff --git a/src/AlexaStatusMessage.ts b/src/AlexaStatusMessage.ts index 0cdb05a..7855730 100644 --- a/src/AlexaStatusMessage.ts +++ b/src/AlexaStatusMessage.ts @@ -115,6 +115,15 @@ export class AlexaStatusMessage { this.context.properties.push(prop); return this; } + public addModeControllerProp(instance: string, value: string, uncertaintyInMs = 0): this { + return this.addProperty( + AlexaInterfaceType.MODE_CONTROLLER, + "mode", + value, + uncertaintyInMs, + instance + ); + } public addThermostatModeProp(mode: string, uncertaintyInMs = 0): this { return this.addProperty( AlexaInterfaceType.THERMOSTAT_CONTROLLER, From c7f0c37aa1c7700bc514920f4e5945c501f33ce8 Mon Sep 17 00:00:00 2001 From: David <43281665+chaos511@users.noreply.github.com> Date: Mon, 12 May 2025 18:38:14 +0000 Subject: [PATCH 08/10] Add momentary example --- .../ExamplePowerControllerWithMomentary.js | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 examples/ExamplePowerControllerWithMomentary.js diff --git a/examples/ExamplePowerControllerWithMomentary.js b/examples/ExamplePowerControllerWithMomentary.js new file mode 100644 index 0000000..eb0558b --- /dev/null +++ b/examples/ExamplePowerControllerWithMomentary.js @@ -0,0 +1,109 @@ +// Import required modules and types from the compiled TypeScript distribution (via dist/index.js) +const { + Alex2MQTT, + AlexaInterfaceType, + PowerController, + DisplayCategory +} = require("../dist/index.js"); + +require("dotenv").config(); // Load environment variables from .env file + +// Simulated in-memory device state +let outputState = PowerController.OFF; +const momentaryModeInstance = "mode.signal.beep"; +const defaultMomentaryMode = "mode.signal.beep.Normal"; + +// Load MQTT connection credentials and root topic from environment variables +const username = process.env.MQTT_USERNAME; +const password = process.env.MQTT_PASSWORD; +const rootTopic = process.env.MQTT_ROOT_TOPIC; + +// Create and initialize a new Alexa-to-MQTT client +const alex2NodeClient = new Alex2MQTT(username, password, rootTopic, false); +alex2NodeClient.connect(); // Connect to the MQTT broker + +// Define the device name and register it +const deviceName = "Kitchen Light"; +const kitchenLight = alex2NodeClient.registerDevice( + deviceName, + "endpoint7", + [DisplayCategory.LIGHT] +); + +// Add PowerController capability +kitchenLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER); + +// Add ModeController for momentary "beep" functionality +const momentaryController = kitchenLight.addCapability(AlexaInterfaceType.MODE_CONTROLLER); +momentaryController.setInstance(momentaryModeInstance); +momentaryController.addFriendlyName("Signal", "en-US"); +momentaryController.addSupportedModes([ + { + value: "mode.signal.beep.Normal", + modeResources: { + friendlyNames: [{ + "@type": "text", + value: { text: "Normal", locale: "en-US" } + }] + } + }, + { + value: "mode.signal.beep.Beep", + modeResources: { + friendlyNames: [{ + "@type": "text", + value: { text: "Beep", locale: "en-US" } + }] + } + } +]); + +// Log the device name to verify registration +console.log(kitchenLight.getName()); + +/** + * Handle Alexa's ReportState directive + */ +kitchenLight.on("ReportState", (payload) => { + console.log("ReportState received!", payload); + const { correlationToken } = payload.header; + const status = kitchenLight.getStatusMessage(correlationToken); + status + .addHealthProp("OK") + .addPowerControllerProp(outputState) + .addModeControllerProp(momentaryModeInstance, defaultMomentaryMode); + status.send(); +}); + +/** + * Handle incoming directives (PowerController and ModeController) + */ +kitchenLight.on("Event", (directive, interfaceType) => { + console.log("Event received", { directive, interfaceType }); + const { name, correlationToken, instance } = directive.header; + + const status = kitchenLight.getStatusMessage(correlationToken, true).addHealthProp("OK"); + + // PowerController logic + if (interfaceType === AlexaInterfaceType.POWER_CONTROLLER) { + if (name === "TurnOn") { + outputState = PowerController.ON; + console.log("Turning ON the Kitchen Light"); + } else if (name === "TurnOff") { + outputState = PowerController.OFF; + console.log("Turning OFF the Kitchen Light"); + } + status.addPowerControllerProp(outputState); + } + + // Momentary ModeController logic + else if (interfaceType === AlexaInterfaceType.MODE_CONTROLLER && instance === momentaryModeInstance) { + if (name === "SetMode") { + const modeValue = directive.payload.mode; + console.log(`[Momentary Trigger] Received mode: ${modeValue}`); + } + status.addModeControllerProp(momentaryModeInstance, defaultMomentaryMode); + } + + status.send(); +}); From f09d03636d00da9af34f9cf8b42c22c135fb0707 Mon Sep 17 00:00:00 2001 From: David <43281665+chaos511@users.noreply.github.com> Date: Tue, 13 May 2025 17:37:00 +0000 Subject: [PATCH 09/10] add full support and examples for toggle controller --- dist/Alex2Node.js | 6 +-- dist/Device.js | 10 ++-- examples/ExampleToggleController.js | 77 +++++++++++++++++++++++++++++ package.json | 2 +- src/Alex2Node.ts | 6 +-- src/Device.ts | 12 ++--- 6 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 examples/ExampleToggleController.js diff --git a/dist/Alex2Node.js b/dist/Alex2Node.js index 58758af..f32cff7 100644 --- a/dist/Alex2Node.js +++ b/dist/Alex2Node.js @@ -111,11 +111,11 @@ class Alex2MQTT extends events_1.EventEmitter { console.log(`[Alex2Node.ts] Creating new device with endpoint: ${endpointId}`); } const normalizedCategory = displayCategory === null - ? null + ? [DisplayCategory_1.DisplayCategory.LIGHT] : Array.isArray(displayCategory) ? displayCategory - : [displayCategory]; - const device = new Device_1.default(this.client, this.rootTopic, name, endpointId, normalizedCategory || [DisplayCategory_1.DisplayCategory.LIGHT]); + : [displayCategory || DisplayCategory_1.DisplayCategory.LIGHT]; + const device = new Device_1.default(this.client, this.rootTopic, name, endpointId, normalizedCategory); this.devices.push(device); return device; } diff --git a/dist/Device.js b/dist/Device.js index 393310b..e2abf6b 100644 --- a/dist/Device.js +++ b/dist/Device.js @@ -69,10 +69,12 @@ class Device extends events_1.EventEmitter { return this.softwareVersion; } addCapability(type) { - const existing = this.capabilities.find((iface) => iface.getType() === type); - if (existing) { - return existing; - } + // const existing = this.capabilities.find( + // (iface) => iface.getType() === type + // ); + // if (existing) { + // return existing; + // } const newCapability = new AlexaInterface_1.AlexaInterface(type); this.capabilities.push(newCapability); // console.log( diff --git a/examples/ExampleToggleController.js b/examples/ExampleToggleController.js new file mode 100644 index 0000000..0d6deca --- /dev/null +++ b/examples/ExampleToggleController.js @@ -0,0 +1,77 @@ +const { + Alex2MQTT, + AlexaInterfaceType, + PowerController, + } = require("../dist/index.js"); + + require("dotenv").config(); + + const username = process.env.MQTT_USERNAME; + const password = process.env.MQTT_PASSWORD; + const rootTopic = process.env.MQTT_ROOT_TOPIC; + + const alex2NodeClient = new Alex2MQTT(username, password, rootTopic, true); + alex2NodeClient.connect(); + + const deviceName = "Custom Device"; + const customToggle = alex2NodeClient.registerDevice(deviceName, "endpoint8"); + + // Define 8 toggle instances and friendly names + const switchCount = 8; + const instanceNames = Array.from({ length: switchCount }, (_, i) => `nodejs.toggle.switch${i + 1}`); + const friendlyNames = Array.from({ length: switchCount }, (_, i) => `Switch ${i + 1}`); + + // Add toggle capabilities and store toggle objects + const toggles = []; + for (let i = 0; i < switchCount; i++) { + const toggle = customToggle.addCapability(AlexaInterfaceType.TOGGLE_CONTROLLER); + toggle.setInstance(instanceNames[i]); + toggle.addFriendlyName(friendlyNames[i], "en-US"); + toggles.push(toggle); + } + + // Output states for all switches + const toggleStates = Array(switchCount).fill(PowerController.OFF); + + console.log(customToggle.getName()); + + customToggle.on("ReportState", (payload) => { + console.log("ReportState received!", payload); + const { correlationToken } = payload.header; + const status = customToggle.getStatusMessage(correlationToken); + + status.addHealthProp("OK"); + + // Report all toggle states + for (let i = 0; i < switchCount; i++) { + status.addToggleControllerProp(toggleStates[i], instanceNames[i]); + } + + status.send(); + }); + + customToggle.on("Event", (directive, interfaceType) => { + console.log("Event received", { directive, interfaceType }); + + if (interfaceType === AlexaInterfaceType.TOGGLE_CONTROLLER) { + const { name, correlationToken, instance } = directive.header; + + const index = instanceNames.indexOf(instance); + if (index !== -1) { + if (name === "TurnOn") { + toggleStates[index] = PowerController.ON; + console.log(`Turning ON ${friendlyNames[index]}`); + } else if (name === "TurnOff") { + toggleStates[index] = PowerController.OFF; + console.log(`Turning OFF ${friendlyNames[index]}`); + } + + const status = customToggle.getStatusMessage(correlationToken, true); + status + .addHealthProp("OK") + .addToggleControllerProp(toggleStates[index], instance) + .send(); + } + } + }); + \ No newline at end of file diff --git a/package.json b/package.json index 873d0b5..dacc0fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alex2node", - "version": "1.3.0", + "version": "1.4.0", "description": "A Node.js library for creating Alexa-compatible devices using MQTT, based on Alex2MQTT.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/Alex2Node.ts b/src/Alex2Node.ts index 4fd3793..dc3b821 100644 --- a/src/Alex2Node.ts +++ b/src/Alex2Node.ts @@ -135,17 +135,17 @@ class Alex2MQTT extends EventEmitter { } const normalizedCategory = displayCategory === null - ? null + ? [DisplayCategory.LIGHT] : Array.isArray(displayCategory) ? displayCategory - : [displayCategory]; + : [displayCategory||DisplayCategory.LIGHT]; const device = new Device( this.client!, this.rootTopic, name, endpointId, - normalizedCategory|| [DisplayCategory.LIGHT] + normalizedCategory ); this.devices.push(device); return device; diff --git a/src/Device.ts b/src/Device.ts index 74cb6e5..8a1871d 100644 --- a/src/Device.ts +++ b/src/Device.ts @@ -101,12 +101,12 @@ class Device extends EventEmitter { } addCapability(type: AlexaInterfaceType): AlexaInterface { - const existing = this.capabilities.find( - (iface) => iface.getType() === type - ); - if (existing) { - return existing; - } + // const existing = this.capabilities.find( + // (iface) => iface.getType() === type + // ); + // if (existing) { + // return existing; + // } const newCapability = new AlexaInterface(type); this.capabilities.push(newCapability); From 7f1cfdbe9ccf3c99edae374eeb0d383ae7b22de1 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 22 Sep 2026 00:14:51 +0000 Subject: [PATCH 10/10] 1.5.0: brightness + mode props in the power example (as published to npm) --- examples/ExamplePowerController.js | 10 ++++++++-- package.json | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/ExamplePowerController.js b/examples/ExamplePowerController.js index ae0a35b..8e6bfb2 100644 --- a/examples/ExamplePowerController.js +++ b/examples/ExamplePowerController.js @@ -23,7 +23,7 @@ alex2NodeClient.connect(); // Connect to the MQTT broker const deviceName = "Bedroom Light"; // Register the device with a unique endpoint ID (you can use something like "endpoint1") -const bedroomLight = alex2NodeClient.registerDevice(deviceName, "endpoint2"); +const bedroomLight = alex2NodeClient.registerDevice(deviceName, "2E3AE9"); // Add the PowerController capability (for turning on/off the device) bedroomLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER); @@ -31,6 +31,9 @@ bedroomLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER); // Log the device name to verify registration console.log(bedroomLight.getName()); +const momentaryModeInstance = "mode.signal.beep"; +const defaultMomentaryMode = "mode.signal.beep.Normal"; + /** * Handle Alexa's ReportState directive. * This occurs when Alexa queries the current state of the device (e.g., during routines or device status checks). @@ -44,7 +47,9 @@ bedroomLight.on("ReportState", (payload) => { status .addHealthProp("OK") // Device is healthy - .addPowerControllerProp(outputState); // Report the current power state (ON/OFF) + .addPowerControllerProp(outputState) // Report the current power state (ON/OFF) + .addBrightnessControllerProp(100) + .addModeControllerProp(momentaryModeInstance, defaultMomentaryMode); status.send(); // Send the state report back to Alexa }); @@ -75,6 +80,7 @@ bedroomLight.on("Event", (directive, interfaceType) => { status .addHealthProp("OK") .addPowerControllerProp(outputState) + .addBrightnessControllerProp(100) .send(); } }); diff --git a/package.json b/package.json index dacc0fe..9b4779e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alex2node", - "version": "1.4.0", + "version": "1.5.0", "description": "A Node.js library for creating Alexa-compatible devices using MQTT, based on Alex2MQTT.", "main": "dist/index.js", "types": "dist/index.d.ts",