diff --git a/dist/Alex2Node.js b/dist/Alex2Node.js index 7e63d2e..f32cff7 100644 --- a/dist/Alex2Node.js +++ b/dist/Alex2Node.js @@ -5,8 +5,11 @@ 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"); +const DisplayCategory_1 = require("./DisplayCategory"); +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 +33,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,11 +52,12 @@ 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"}`); - console.log(deviceArray); + console.log(JSON.stringify(deviceArray, null, 2)); } }); } @@ -94,17 +99,23 @@ 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) { + 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 + ? [DisplayCategory_1.DisplayCategory.LIGHT] + : Array.isArray(displayCategory) + ? displayCategory + : [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/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/AlexaInterface.js b/dist/AlexaInterface.js index 452ed28..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; } @@ -164,11 +168,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: @@ -195,8 +200,17 @@ 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: + return [ + "lowerSetpoint", + "upperSetpoint", + "thermostatMode", + "adaptiveRecoveryStatus", + ]; case AlexaInterfaceType.APPLICATION_STATE_REPORTER: case AlexaInterfaceType.AUDIO_PLAY_QUEUE: case AlexaInterfaceType.AUTHORIZATION_CONTROLLER: @@ -220,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: @@ -238,7 +251,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 +273,21 @@ 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 + }; + } + else if (this.supportedModes.length > 0) { + doc["configuration"] = { + ordered: true, + supportedModes: this.supportedModes, + }; + } if (this.instance) { doc.instance = this.instance; } @@ -278,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 6bfd1fb..64dbeba 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"; @@ -19,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, @@ -57,6 +71,32 @@ 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); + } + 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 + ? (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); } @@ -85,19 +125,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 7decf86..e2abf6b 100644 --- a/dist/Device.js +++ b/dist/Device.js @@ -4,8 +4,9 @@ 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 = 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 +30,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; @@ -40,8 +41,11 @@ 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); + 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); } setManufacturerName(name) { this.manufacturerName = name; @@ -65,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( @@ -82,7 +88,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..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.PowerController = 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"); @@ -11,5 +11,13 @@ 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; } }); +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/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/ExamplePowerController.js b/examples/ExamplePowerController.js index 5658a55..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). @@ -38,13 +41,15 @@ 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); 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/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/ExamplePowerControllerWithErrors.js b/examples/ExamplePowerControllerWithErrors.js new file mode 100644 index 0000000..de20d68 --- /dev/null +++ b/examples/ExamplePowerControllerWithErrors.js @@ -0,0 +1,92 @@ +const { + Alex2MQTT, + AlexaInterfaceType, + PowerController, + AlexaErrorType, +} = require("../dist/index.js"); + +require("dotenv").config(); + +let outputState = PowerController.OFF; + +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, false); +alex2NodeClient.connect(); + +const errorLight = alex2NodeClient.registerDevice("bad light", "endpoint4"); +errorLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER); + +console.log(errorLight.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; + +/** + * Always return HARDWARE_MALFUNCTION error on state queries. + */ +errorLight.on("ReportState", (payload) => { + console.log("ReportState received!", payload); + + const { correlationToken } = payload.header; + const errorResponse = errorLight.getErrorMessage(correlationToken); + + errorResponse.setErrorMessage( + AlexaErrorType.HARDWARE_MALFUNCTION, + "The device is currently busy and cannot report its state." + ); + console.log("Sending error: HARDWARE_MALFUNCTION"); + errorResponse.send(); +}); + +/** + * Handle control directives with 90% error chance, cycling through fixed errors. + */ +errorLight.on("Event", (directive, interfaceType) => { + if (interfaceType === AlexaInterfaceType.POWER_CONTROLLER) { + const name = directive.header.name; + const token = directive.header.correlationToken; + + 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; + } + + // 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) + .send(); + } +}); 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(); +}); diff --git a/examples/ExampleThermostat.js b/examples/ExampleThermostat.js new file mode 100644 index 0000000..a8737b9 --- /dev/null +++ b/examples/ExampleThermostat.js @@ -0,0 +1,206 @@ +// 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,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()); + +/** + * 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: 50, + upperSetpoint: 70, + targetSetpoint: 60, + current: 69, + thermostatMode: ThermostatMode.HEAT, + fanMode:"mode.fanmode.Auto" +}; +thermostat.on("ReportState", (payload) => { + // console.log("ReportState received!", payload); + + const { correlationToken } = payload.header; + + let status = thermostat.getStatusMessage(correlationToken); + + status + .addHealthProp("OK") // Device is healthy and reachable + .addModeControllerProp("mode.fanmode", deviceData.fanMode) + .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 = parseTempValue( + directive.payload.upperSetpoint + ); + } + if (directive.payload.lowerSetpoint) { + deviceData.lowerSetpoint = parseTempValue( + directive.payload.lowerSetpoint + ); + } + if (directive.payload.targetSetpoint) { + deviceData.targetSetpoint = parseTempValue( + 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 + .addModeControllerProp("mode.fanmode", deviceData.fanMode) + .addTemperatureSensorProp( + TemperatureSensorScale.FAHRENHEIT, + deviceData.current + ) // Report current temperature + .addThermostatModeProp(deviceData.thermostatMode); // Report current thermostat mode + if (deviceData.thermostatMode == "AUTO") { + status = status + .addThermostatControllerProp( + "lowerSetpoint", + TemperatureSensorScale.FAHRENHEIT, + deviceData.lowerSetpoint + ) // Lower bound of temperature range + .addThermostatControllerProp( + "upperSetpoint", + TemperatureSensorScale.FAHRENHEIT, + deviceData.upperSetpoint + ); // Upper bound of temperature range + } else { + status = status.addThermostatControllerProp( + "targetSetpoint", + TemperatureSensorScale.FAHRENHEIT, + deviceData.targetSetpoint + ); // Lower bound of temperature range + } + status.send(); // Send the full state report back to Alexa + } +}); + +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/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 92dc81d..9b4779e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alex2node", - "version": "1.0.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", diff --git a/src/Alex2Node.ts b/src/Alex2Node.ts index bf18271..dc3b821 100644 --- a/src/Alex2Node.ts +++ b/src/Alex2Node.ts @@ -1,7 +1,9 @@ import mqtt, { MqttClient } from "mqtt"; import Device from "./Device"; +import { EventEmitter } from "events"; +import { DisplayCategory } from "./DisplayCategory"; -class Alex2MQTT { +class Alex2MQTT extends EventEmitter { private client: MqttClient | null = null; private devices: Device[] = []; private MqttHost = "mqtt://Alex2MQTT.stormysdream.club"; @@ -11,7 +13,9 @@ 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,14 +61,15 @@ 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 ${ topic + "_r" }` ); - console.log(deviceArray); + console.log(JSON.stringify(deviceArray, null, 2)); } } ); @@ -100,8 +106,8 @@ class Alex2MQTT { ); } device.emit("ReportState", payload); - }else{ - device.emit("Event", payload,payload.header.namespace); + } else { + device.emit("Event", payload, payload.header.namespace); } } }); @@ -109,11 +115,16 @@ 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: 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"); } @@ -122,7 +133,20 @@ class Alex2MQTT { `[Alex2Node.ts] Creating new device with endpoint: ${endpointId}` ); } - const device = new Device(this.client!, this.rootTopic, name, endpointId); + const normalizedCategory = + displayCategory === null + ? [DisplayCategory.LIGHT] + : Array.isArray(displayCategory) + ? displayCategory + : [displayCategory||DisplayCategory.LIGHT]; + + const device = new Device( + this.client!, + this.rootTopic, + name, + endpointId, + normalizedCategory + ); this.devices.push(device); return device; } 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/AlexaInterface.ts b/src/AlexaInterface.ts index 9cdc58d..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; } @@ -176,11 +179,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: @@ -208,9 +212,17 @@ 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: + return [ + "lowerSetpoint", + "upperSetpoint", + "thermostatMode", + "adaptiveRecoveryStatus", + ]; case AlexaInterfaceType.APPLICATION_STATE_REPORTER: case AlexaInterfaceType.AUDIO_PLAY_QUEUE: case AlexaInterfaceType.AUTHORIZATION_CONTROLLER: @@ -234,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: @@ -252,7 +263,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 +287,20 @@ 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 + } + }else if (this.supportedModes.length > 0) { + doc["configuration"] = { + ordered:true, + supportedModes: this.supportedModes, + }; + } if (this.instance) { doc.instance = this.instance; } @@ -294,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 e8f4f38..7855730 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", @@ -48,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, @@ -82,7 +97,6 @@ export class AlexaStatusMessage { private getTimestamp(): string { return new Date().toISOString(); } - private addProperty( namespace: AlexaInterfaceType, name: string, @@ -101,6 +115,56 @@ 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, + "thermostatMode", + mode, + 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", + 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( @@ -186,12 +250,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) => { @@ -201,7 +265,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 80fa436..8a1871d 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"; @@ -13,7 +14,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 +35,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 { @@ -49,16 +50,26 @@ 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 + isResponse = false, + isDeferred = false ): AlexaStatusMessage { return new AlexaStatusMessage( correlationToken, this.rootTopic, this.endpointId, this.mqttClient, - isResponse + isResponse, + isDeferred ); } setManufacturerName(name: string): void { @@ -90,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); @@ -111,7 +122,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..f381872 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ 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'; +export { AlexaErrorType,AlexaErrorResponse} from './AlexaErrorResponse';