Compare commits
10 commits
13f0b1811c
...
7f1cfdbe9c
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f1cfdbe9c | |||
|
|
f09d03636d | ||
|
|
c7f0c37aa1 | ||
|
|
3bcd767127 | ||
|
|
e7a2079ff7 | ||
|
|
689a357df0 | ||
|
|
4af45ff726 | ||
|
|
61b032c889 | ||
|
|
f4d07b589c | ||
|
|
7f051dfdcb |
20 changed files with 1256 additions and 72 deletions
25
dist/Alex2Node.js
vendored
25
dist/Alex2Node.js
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
125
dist/AlexaErrorResponse.js
vendored
Normal file
125
dist/AlexaErrorResponse.js
vendored
Normal file
|
|
@ -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;
|
||||
33
dist/AlexaInterface.js
vendored
33
dist/AlexaInterface.js
vendored
|
|
@ -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) {
|
||||
|
|
|
|||
54
dist/AlexaStatusMessage.js
vendored
54
dist/AlexaStatusMessage.js
vendored
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
26
dist/Device.js
vendored
26
dist/Device.js
vendored
|
|
@ -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,
|
||||
|
|
|
|||
10
dist/index.js
vendored
10
dist/index.js
vendored
|
|
@ -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; } });
|
||||
|
|
|
|||
118
examples/ExampleModeController.js
Normal file
118
examples/ExampleModeController.js
Normal file
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
90
examples/ExamplePowerControllerDeferred.js
Normal file
90
examples/ExamplePowerControllerDeferred.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
92
examples/ExamplePowerControllerWithErrors.js
Normal file
92
examples/ExamplePowerControllerWithErrors.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
109
examples/ExamplePowerControllerWithMomentary.js
Normal file
109
examples/ExamplePowerControllerWithMomentary.js
Normal file
|
|
@ -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();
|
||||
});
|
||||
206
examples/ExampleThermostat.js
Normal file
206
examples/ExampleThermostat.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
77
examples/ExampleToggleController.js
Normal file
77
examples/ExampleToggleController.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
148
src/AlexaErrorResponse.ts
Normal file
148
src/AlexaErrorResponse.ts
Normal file
|
|
@ -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<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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DisplayCategory> | 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<DisplayCategory> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue