Compare commits

..

No commits in common. "7f1cfdbe9ccf3c99edae374eeb0d383ae7b22de1" and "13f0b1811c1cd166048502d39ef7cd505ca4a7b8" have entirely different histories.

20 changed files with 72 additions and 1256 deletions

25
dist/Alex2Node.js vendored
View file

@ -5,11 +5,8 @@ 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"));
const events_1 = require("events");
const DisplayCategory_1 = require("./DisplayCategory");
class Alex2MQTT extends events_1.EventEmitter {
class Alex2MQTT {
constructor(username, password, rootTopic, debugLogging) {
super(); // Initialize EventEmitter
this.username = username;
this.password = password;
this.rootTopic = rootTopic;
@ -33,8 +30,7 @@ class Alex2MQTT extends events_1.EventEmitter {
console.log("[Alex2Node.ts] Subscribed to topics");
}
if (err) {
console.error("[Alex2Node.ts] MQTT connection error:", err);
this.emit("error", err);
throw err;
}
});
});
@ -52,12 +48,11 @@ class Alex2MQTT extends events_1.EventEmitter {
if (deviceArray.length > 0) {
this.client.publish(topic + "_r", JSON.stringify(deviceArray), (err) => {
if (err) {
console.error("[Alex2Node.ts] MQTT connection error:", err);
this.emit("error", err);
throw err;
}
else if (this.debugLogging) {
console.log(`[Alex2Node.ts] Discovery payloads published to ${topic + "_r"}`);
console.log(JSON.stringify(deviceArray, null, 2));
console.log(deviceArray);
}
});
}
@ -99,23 +94,17 @@ class Alex2MQTT extends events_1.EventEmitter {
if (this.debugLogging) {
console.error("[Alex2Node.ts] Connection failed:", err);
}
console.error("[Alex2Node.ts] MQTT connection error:", err);
this.emit("error", err);
throw err;
});
}
registerDevice(name, endpointId, displayCategory) {
registerDevice(name, endpointId) {
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 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);
const device = new Device_1.default(this.client, this.rootTopic, name, endpointId);
this.devices.push(device);
return device;
}

View file

@ -1,125 +0,0 @@
"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;

View file

@ -82,7 +82,6 @@ class AlexaInterface {
this.instance = instance;
this.friendlyNames = [];
this.actionMappings = [];
this.supportedModes = [];
}
addActionMapping(mapping) {
this.actionMappings.push(mapping);
@ -90,9 +89,6 @@ class AlexaInterface {
addFriendlyName(name, locale) {
this.friendlyNames.push({ text: name, locale });
}
addSupportedModes(modes) {
this.supportedModes = modes;
}
setInstance(name) {
this.instance = name;
}
@ -168,12 +164,11 @@ 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:
@ -200,17 +195,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:
return [
"lowerSetpoint",
"upperSetpoint",
"thermostatMode",
"adaptiveRecoveryStatus",
];
case AlexaInterfaceType.APPLICATION_STATE_REPORTER:
case AlexaInterfaceType.AUDIO_PLAY_QUEUE:
case AlexaInterfaceType.AUTHORIZATION_CONTROLLER:
@ -234,6 +220,7 @@ 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:
@ -251,6 +238,7 @@ 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:
@ -273,21 +261,9 @@ class AlexaInterface {
properties: {
retrievable: this.retrievable,
proactivelyReported: this.proactivelyReported,
supported: this.getProps().map(name => ({ name })),
supported: [{ name: this.getProps()[0] }],
},
};
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;
}
@ -302,7 +278,6 @@ class AlexaInterface {
};
friendlyNamesArray.push(friendlyNameObj);
});
capabilityResources.friendlyNames = friendlyNamesArray;
doc["capabilityResources"] = capabilityResources;
}
if (this.actionMappings.length > 0) {

View file

@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AlexaStatusMessage = exports.TemperatureSensorScale = exports.PowerController = exports.ThermostatMode = exports.EndpointHealth = void 0;
exports.AlexaStatusMessage = exports.TemperatureSensorScale = exports.PowerController = exports.EndpointHealth = void 0;
const uuid_1 = require("uuid");
const AlexaInterface_1 = require("./AlexaInterface");
var EndpointHealth;
@ -8,15 +8,6 @@ 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";
@ -28,20 +19,15 @@ var TemperatureSensorScale;
TemperatureSensorScale["FAHRENHEIT"] = "FAHRENHEIT";
})(TemperatureSensorScale || (exports.TemperatureSensorScale = TemperatureSensorScale = {}));
class AlexaStatusMessage {
constructor(correlationToken, rootTopic, endpointId, mqttClient, isResponse = false, isDeferred = false) {
constructor(correlationToken, rootTopic, endpointId, mqttClient, isResponse = false) {
this.context = { properties: [] };
this.rootTopic = rootTopic;
this.endpointId = endpointId;
this.mqttClient = mqttClient;
this.isDeferred = isDeferred;
this.event = {
header: {
namespace: "Alexa",
name: isDeferred
? "DeferredResponse"
: isResponse
? "Response"
: "StateReport",
name: isResponse ? "Response" : "StateReport",
payloadVersion: "3",
messageId: this.generateMessageId(),
correlationToken,
@ -71,32 +57,6 @@ 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);
}
@ -125,19 +85,19 @@ class AlexaStatusMessage {
this.context.properties.push(prop);
return this;
}
send(sendAsync) {
send() {
const payload = {
context: this.isDeferred ? null : this.context,
context: this.context,
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 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) => {
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}`);
}
});
}

26
dist/Device.js vendored
View file

@ -4,9 +4,8 @@ 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") {
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") {
super();
this.mqttClient = mqttClient;
this.rootTopic = rootTopic;
@ -30,10 +29,10 @@ class Device extends events_1.EventEmitter {
return this.endpointId;
}
setDisplayCategory(category) {
this.displayCategory = Array.isArray(category) ? category : [category];
this.displayCategory = category;
}
getDisplayCategory() {
return this.displayCategory || [DisplayCategory_1.DisplayCategory.LIGHT];
return this.displayCategory;
}
setDescription(description) {
this.description = description;
@ -41,11 +40,8 @@ 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);
getStatusMessage(correlationToken, isResponse = false) {
return new AlexaStatusMessage_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.mqttClient, isResponse);
}
setManufacturerName(name) {
this.manufacturerName = name;
@ -69,12 +65,10 @@ 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(
@ -88,7 +82,7 @@ class Device extends events_1.EventEmitter {
friendlyName: this.name,
description: this.description,
manufacturerName: this.manufacturerName,
displayCategories: this.getDisplayCategory(),
displayCategories: [this.displayCategory],
additionalAttributes: {
manufacturer: this.manufacturer,
model: this.model,

10
dist/index.js vendored
View file

@ -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.AlexaErrorResponse = exports.AlexaErrorType = exports.ThermostatMode = exports.TemperatureSensorScale = exports.EndpointHealth = exports.PowerController = exports.DisplayCategory = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterfaceType = exports.Alex2MQTT = void 0;
exports.PowerController = 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,13 +11,5 @@ 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; } });

View file

@ -1,118 +0,0 @@
// 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
}
});

View file

@ -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, "2E3AE9");
const bedroomLight = alex2NodeClient.registerDevice(deviceName, "endpoint2");
// Add the PowerController capability (for turning on/off the device)
bedroomLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
@ -31,9 +31,6 @@ 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).
@ -41,15 +38,13 @@ const defaultMomentaryMode = "mode.signal.beep.Normal";
bedroomLight.on("ReportState", (payload) => {
console.log("ReportState received!", payload);
const { correlationToken } = payload.header;
const { correlationToken } = payload;
const status = bedroomLight.getStatusMessage(correlationToken);
status
.addHealthProp("OK") // Device is healthy
.addPowerControllerProp(outputState) // Report the current power state (ON/OFF)
.addBrightnessControllerProp(100)
.addModeControllerProp(momentaryModeInstance, defaultMomentaryMode);
.addPowerControllerProp(outputState); // Report the current power state (ON/OFF)
status.send(); // Send the state report back to Alexa
});
@ -80,7 +75,6 @@ bedroomLight.on("Event", (directive, interfaceType) => {
status
.addHealthProp("OK")
.addPowerControllerProp(outputState)
.addBrightnessControllerProp(100)
.send();
}
});

View file

@ -1,90 +0,0 @@
// 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();
}
});

View file

@ -1,92 +0,0 @@
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();
}
});

View file

@ -1,109 +0,0 @@
// 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();
});

View file

@ -1,206 +0,0 @@
// 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;
}

View file

@ -1,77 +0,0 @@
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();
}
}
});

View file

@ -1,6 +1,6 @@
{
"name": "alex2node",
"version": "1.5.0",
"version": "1.0.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",

View file

@ -1,9 +1,7 @@
import mqtt, { MqttClient } from "mqtt";
import Device from "./Device";
import { EventEmitter } from "events";
import { DisplayCategory } from "./DisplayCategory";
class Alex2MQTT extends EventEmitter {
class Alex2MQTT {
private client: MqttClient | null = null;
private devices: Device[] = [];
private MqttHost = "mqtt://Alex2MQTT.stormysdream.club";
@ -13,9 +11,7 @@ class Alex2MQTT extends EventEmitter {
private password: string,
private rootTopic: string,
private debugLogging: boolean
) {
super(); // Initialize EventEmitter
}
) {}
connect(): void {
const options = {
@ -35,8 +31,7 @@ class Alex2MQTT extends EventEmitter {
console.log("[Alex2Node.ts] Subscribed to topics");
}
if (err) {
console.error("[Alex2Node.ts] MQTT connection error:", err);
this.emit("error", err);
throw err;
}
});
});
@ -61,15 +56,14 @@ class Alex2MQTT extends EventEmitter {
JSON.stringify(deviceArray),
(err) => {
if (err) {
console.error("[Alex2Node.ts] MQTT connection error:", err);
this.emit("error", err);
throw err;
} else if (this.debugLogging) {
console.log(
`[Alex2Node.ts] Discovery payloads published to ${
topic + "_r"
}`
);
console.log(JSON.stringify(deviceArray, null, 2));
console.log(deviceArray);
}
}
);
@ -106,8 +100,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);
}
}
});
@ -115,16 +109,11 @@ class Alex2MQTT extends EventEmitter {
if (this.debugLogging) {
console.error("[Alex2Node.ts] Connection failed:", err);
}
console.error("[Alex2Node.ts] MQTT connection error:", err);
this.emit("error", err);
throw err;
});
}
registerDevice(
name: string,
endpointId: string,
displayCategory: DisplayCategory | DisplayCategory[] | null
): Device {
registerDevice(name: string, endpointId: string): Device {
if (!this.client) {
throw new Error("Must call connect before creating devices");
}
@ -133,20 +122,7 @@ class Alex2MQTT extends EventEmitter {
`[Alex2Node.ts] Creating new device with endpoint: ${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
);
const device = new Device(this.client!, this.rootTopic, name, endpointId);
this.devices.push(device);
return device;
}

View file

@ -1,148 +0,0 @@
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<string, any> = {}
) {
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}`);
}
});
}
}

View file

@ -81,7 +81,6 @@ interface FriendlyName {
export class AlexaInterface {
private friendlyNames: FriendlyName[] = [];
private actionMappings: ActionMapping[] = [];
private supportedModes: string[] = [];
constructor(
public type: AlexaInterfaceType,
@ -97,9 +96,7 @@ 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;
}
@ -179,12 +176,11 @@ 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:
@ -212,17 +208,9 @@ 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:
@ -246,6 +234,7 @@ 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:
@ -263,6 +252,7 @@ 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:
@ -287,20 +277,9 @@ export class AlexaInterface {
properties: {
retrievable: this.retrievable,
proactivelyReported: this.proactivelyReported,
supported: this.getProps().map(name => ({ name })),
supported: [{ name: this.getProps()[0] }],
},
};
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;
}
@ -315,7 +294,6 @@ export class AlexaInterface {
};
friendlyNamesArray.push(friendlyNameObj);
});
capabilityResources.friendlyNames=friendlyNamesArray
doc["capabilityResources"] = capabilityResources;
}
if (this.actionMappings.length > 0) {

View file

@ -7,15 +7,6 @@ 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",
@ -57,28 +48,22 @@ 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,
isDeferred: boolean = false
isResponse: boolean = false
) {
this.rootTopic = rootTopic;
this.endpointId = endpointId;
this.mqttClient = mqttClient;
this.isDeferred = isDeferred;
this.event = {
header: {
namespace: "Alexa",
name: isDeferred
? "DeferredResponse"
: isResponse
? "Response"
: "StateReport",
name: isResponse ? "Response" : "StateReport",
payloadVersion: "3",
messageId: this.generateMessageId(),
correlationToken,
@ -97,6 +82,7 @@ export class AlexaStatusMessage {
private getTimestamp(): string {
return new Date().toISOString();
}
private addProperty(
namespace: AlexaInterfaceType,
name: string,
@ -115,56 +101,6 @@ 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(
@ -250,12 +186,12 @@ export class AlexaStatusMessage {
return this;
}
public send(sendAsync: boolean): void {
public send(): void {
const payload = {
context: this.isDeferred ? null : this.context,
context: this.context,
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 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) => {
@ -265,7 +201,7 @@ export class AlexaStatusMessage {
err
);
} else {
console.log(`[AlexaStatusMessage] Sent message to ${topic}`);
// console.log(`[AlexaStatusMessage] Sent message to ${topic}`);
}
});
}

View file

@ -3,7 +3,6 @@ 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";
@ -14,7 +13,7 @@ class Device extends EventEmitter {
private rootTopic: string,
public name: string,
public endpointId: string,
public displayCategory: Array<DisplayCategory> | null,
public displayCategory: DisplayCategory = DisplayCategory.LIGHT,
public description: string = "Alexa to Node.js bridge",
public manufacturerName: string = "Alex2Node",
public manufacturer: string = "Alex2Node",
@ -35,12 +34,12 @@ class Device extends EventEmitter {
return this.endpointId;
}
setDisplayCategory(category: DisplayCategory | DisplayCategory[]): void {
this.displayCategory = Array.isArray(category) ? category : [category];
setDisplayCategory(category: DisplayCategory): void {
this.displayCategory = category;
}
getDisplayCategory(): Array<DisplayCategory> {
return this.displayCategory || [DisplayCategory.LIGHT];
getDisplayCategory(): DisplayCategory {
return this.displayCategory;
}
setDescription(description: string): void {
@ -50,26 +49,16 @@ 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
isResponse = false
): AlexaStatusMessage {
return new AlexaStatusMessage(
correlationToken,
this.rootTopic,
this.endpointId,
this.mqttClient,
isResponse,
isDeferred
isResponse
);
}
setManufacturerName(name: string): void {
@ -101,12 +90,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);
@ -122,7 +111,7 @@ class Device extends EventEmitter {
friendlyName: this.name,
description: this.description,
manufacturerName: this.manufacturerName,
displayCategories: this.getDisplayCategory(),
displayCategories: [this.displayCategory],
additionalAttributes: {
manufacturer: this.manufacturer,
model: this.model,

View file

@ -1,6 +1,4 @@
export { default as Alex2MQTT } from './Alex2Node';
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';
export { PowerController } from './AlexaStatusMessage';