add support and examples for deferred state report responses
This commit is contained in:
parent
f4d07b589c
commit
61b032c889
8 changed files with 147 additions and 25 deletions
28
dist/AlexaStatusMessage.js
vendored
28
dist/AlexaStatusMessage.js
vendored
|
|
@ -28,15 +28,20 @@ var TemperatureSensorScale;
|
||||||
TemperatureSensorScale["FAHRENHEIT"] = "FAHRENHEIT";
|
TemperatureSensorScale["FAHRENHEIT"] = "FAHRENHEIT";
|
||||||
})(TemperatureSensorScale || (exports.TemperatureSensorScale = TemperatureSensorScale = {}));
|
})(TemperatureSensorScale || (exports.TemperatureSensorScale = TemperatureSensorScale = {}));
|
||||||
class AlexaStatusMessage {
|
class AlexaStatusMessage {
|
||||||
constructor(correlationToken, rootTopic, endpointId, mqttClient, isResponse = false) {
|
constructor(correlationToken, rootTopic, endpointId, mqttClient, isResponse = false, isDeferred = false) {
|
||||||
this.context = { properties: [] };
|
this.context = { properties: [] };
|
||||||
this.rootTopic = rootTopic;
|
this.rootTopic = rootTopic;
|
||||||
this.endpointId = endpointId;
|
this.endpointId = endpointId;
|
||||||
this.mqttClient = mqttClient;
|
this.mqttClient = mqttClient;
|
||||||
|
this.isDeferred = isDeferred;
|
||||||
this.event = {
|
this.event = {
|
||||||
header: {
|
header: {
|
||||||
namespace: "Alexa",
|
namespace: "Alexa",
|
||||||
name: isResponse ? "Response" : "StateReport",
|
name: isDeferred
|
||||||
|
? "DeferredResponse"
|
||||||
|
: isResponse
|
||||||
|
? "Response"
|
||||||
|
: "StateReport",
|
||||||
payloadVersion: "3",
|
payloadVersion: "3",
|
||||||
messageId: this.generateMessageId(),
|
messageId: this.generateMessageId(),
|
||||||
correlationToken,
|
correlationToken,
|
||||||
|
|
@ -69,6 +74,17 @@ class AlexaStatusMessage {
|
||||||
addThermostatModeProp(mode, uncertaintyInMs = 0) {
|
addThermostatModeProp(mode, uncertaintyInMs = 0) {
|
||||||
return this.addProperty(AlexaInterface_1.AlexaInterfaceType.THERMOSTAT_CONTROLLER, "thermostatMode", mode, uncertaintyInMs);
|
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) {
|
addThermostatControllerProp(name, scale, value, uncertaintyInMs = 0) {
|
||||||
const tempValue = {
|
const tempValue = {
|
||||||
value: scale === TemperatureSensorScale.FAHRENHEIT
|
value: scale === TemperatureSensorScale.FAHRENHEIT
|
||||||
|
|
@ -106,19 +122,19 @@ class AlexaStatusMessage {
|
||||||
this.context.properties.push(prop);
|
this.context.properties.push(prop);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
send() {
|
send(sendAsync) {
|
||||||
const payload = {
|
const payload = {
|
||||||
context: this.context,
|
context: this.isDeferred ? null : this.context,
|
||||||
event: this.event,
|
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);
|
const payloadStr = JSON.stringify(payload);
|
||||||
this.mqttClient.publish(topic, payloadStr, (err) => {
|
this.mqttClient.publish(topic, payloadStr, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("[AlexaStatusMessage] Failed to publish status message:", err);
|
console.error("[AlexaStatusMessage] Failed to publish status message:", err);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// console.log(`[AlexaStatusMessage] Sent message to ${topic}`);
|
console.log(`[AlexaStatusMessage] Sent message to ${topic}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
dist/Device.js
vendored
4
dist/Device.js
vendored
|
|
@ -40,8 +40,8 @@ class Device extends events_1.EventEmitter {
|
||||||
getDescription() {
|
getDescription() {
|
||||||
return this.description;
|
return this.description;
|
||||||
}
|
}
|
||||||
getStatusMessage(correlationToken, isResponse = false) {
|
getStatusMessage(correlationToken, isResponse = false, isDeferred = false) {
|
||||||
return new AlexaStatusMessage_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.mqttClient, isResponse);
|
return new AlexaStatusMessage_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.mqttClient, isResponse, isDeferred);
|
||||||
}
|
}
|
||||||
setManufacturerName(name) {
|
setManufacturerName(name) {
|
||||||
this.manufacturerName = name;
|
this.manufacturerName = name;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ console.log(bedroomLight.getName());
|
||||||
bedroomLight.on("ReportState", (payload) => {
|
bedroomLight.on("ReportState", (payload) => {
|
||||||
console.log("ReportState received!", payload);
|
console.log("ReportState received!", payload);
|
||||||
|
|
||||||
const { correlationToken } = payload;
|
const { correlationToken } = payload.header;
|
||||||
|
|
||||||
const status = bedroomLight.getStatusMessage(correlationToken);
|
const status = bedroomLight.getStatusMessage(correlationToken);
|
||||||
|
|
||||||
|
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -54,7 +54,7 @@ const deviceData = {
|
||||||
thermostat.on("ReportState", (payload) => {
|
thermostat.on("ReportState", (payload) => {
|
||||||
// console.log("ReportState received!", payload);
|
// console.log("ReportState received!", payload);
|
||||||
|
|
||||||
const { correlationToken } = payload;
|
const { correlationToken } = payload.header;
|
||||||
|
|
||||||
let status = thermostat.getStatusMessage(correlationToken);
|
let status = thermostat.getStatusMessage(correlationToken);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "alex2node",
|
"name": "alex2node",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"description": "A Node.js library for creating Alexa-compatible devices using MQTT, based on Alex2MQTT.",
|
"description": "A Node.js library for creating Alexa-compatible devices using MQTT, based on Alex2MQTT.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
||||||
|
|
@ -57,22 +57,28 @@ export class AlexaStatusMessage {
|
||||||
private rootTopic: string;
|
private rootTopic: string;
|
||||||
private endpointId: string;
|
private endpointId: string;
|
||||||
private mqttClient: MqttClient;
|
private mqttClient: MqttClient;
|
||||||
|
private isDeferred: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
correlationToken: string,
|
correlationToken: string,
|
||||||
rootTopic: string,
|
rootTopic: string,
|
||||||
endpointId: string,
|
endpointId: string,
|
||||||
mqttClient: MqttClient,
|
mqttClient: MqttClient,
|
||||||
isResponse: boolean = false
|
isResponse: boolean = false,
|
||||||
|
isDeferred: boolean = false
|
||||||
) {
|
) {
|
||||||
this.rootTopic = rootTopic;
|
this.rootTopic = rootTopic;
|
||||||
this.endpointId = endpointId;
|
this.endpointId = endpointId;
|
||||||
this.mqttClient = mqttClient;
|
this.mqttClient = mqttClient;
|
||||||
|
this.isDeferred = isDeferred;
|
||||||
this.event = {
|
this.event = {
|
||||||
header: {
|
header: {
|
||||||
namespace: "Alexa",
|
namespace: "Alexa",
|
||||||
name: isResponse ? "Response" : "StateReport",
|
name: isDeferred
|
||||||
|
? "DeferredResponse"
|
||||||
|
: isResponse
|
||||||
|
? "Response"
|
||||||
|
: "StateReport",
|
||||||
payloadVersion: "3",
|
payloadVersion: "3",
|
||||||
messageId: this.generateMessageId(),
|
messageId: this.generateMessageId(),
|
||||||
correlationToken,
|
correlationToken,
|
||||||
|
|
@ -91,7 +97,6 @@ export class AlexaStatusMessage {
|
||||||
private getTimestamp(): string {
|
private getTimestamp(): string {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private addProperty(
|
private addProperty(
|
||||||
namespace: AlexaInterfaceType,
|
namespace: AlexaInterfaceType,
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -110,10 +115,7 @@ export class AlexaStatusMessage {
|
||||||
this.context.properties.push(prop);
|
this.context.properties.push(prop);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
public addThermostatModeProp(
|
public addThermostatModeProp(mode: string, uncertaintyInMs = 0): this {
|
||||||
mode: string,
|
|
||||||
uncertaintyInMs = 0
|
|
||||||
): this {
|
|
||||||
return this.addProperty(
|
return this.addProperty(
|
||||||
AlexaInterfaceType.THERMOSTAT_CONTROLLER,
|
AlexaInterfaceType.THERMOSTAT_CONTROLLER,
|
||||||
"thermostatMode",
|
"thermostatMode",
|
||||||
|
|
@ -121,6 +123,18 @@ export class AlexaStatusMessage {
|
||||||
uncertaintyInMs
|
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(
|
public addThermostatControllerProp(
|
||||||
name: "lowerSetpoint" | "upperSetpoint" | "targetSetpoint",
|
name: "lowerSetpoint" | "upperSetpoint" | "targetSetpoint",
|
||||||
|
|
@ -227,12 +241,12 @@ export class AlexaStatusMessage {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public send(): void {
|
public send(sendAsync: boolean): void {
|
||||||
const payload = {
|
const payload = {
|
||||||
context: this.context,
|
context: this.isDeferred ? null : this.context,
|
||||||
event: this.event,
|
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);
|
const payloadStr = JSON.stringify(payload);
|
||||||
|
|
||||||
this.mqttClient.publish(topic, payloadStr, (err) => {
|
this.mqttClient.publish(topic, payloadStr, (err) => {
|
||||||
|
|
@ -242,7 +256,7 @@ export class AlexaStatusMessage {
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// console.log(`[AlexaStatusMessage] Sent message to ${topic}`);
|
console.log(`[AlexaStatusMessage] Sent message to ${topic}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,14 +51,16 @@ class Device extends EventEmitter {
|
||||||
}
|
}
|
||||||
getStatusMessage(
|
getStatusMessage(
|
||||||
correlationToken: string,
|
correlationToken: string,
|
||||||
isResponse = false
|
isResponse = false,
|
||||||
|
isDeferred = false,
|
||||||
): AlexaStatusMessage {
|
): AlexaStatusMessage {
|
||||||
return new AlexaStatusMessage(
|
return new AlexaStatusMessage(
|
||||||
correlationToken,
|
correlationToken,
|
||||||
this.rootTopic,
|
this.rootTopic,
|
||||||
this.endpointId,
|
this.endpointId,
|
||||||
this.mqttClient,
|
this.mqttClient,
|
||||||
isResponse
|
isResponse,
|
||||||
|
isDeferred
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setManufacturerName(name: string): void {
|
setManufacturerName(name: string): void {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue