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,