303 lines
8 KiB
TypeScript
303 lines
8 KiB
TypeScript
import { v4 as uuidv4 } from "uuid";
|
|
import { AlexaInterface, AlexaInterfaceType } from "./AlexaInterface";
|
|
import { MqttClient } from "mqtt";
|
|
|
|
export enum EndpointHealth {
|
|
OK = "OK",
|
|
UNREACHABLE = "UNREACHABLE",
|
|
}
|
|
|
|
export enum ThermostatMode {
|
|
OFF = "OFF",
|
|
HEAT = "HEAT",
|
|
COOL = "COOL",
|
|
AUTO = "AUTO",
|
|
ECO = "ECO",
|
|
CUSTOM = "CUSTOM",
|
|
}
|
|
|
|
export enum PowerController {
|
|
ON = "ON",
|
|
OFF = "OFF",
|
|
}
|
|
|
|
export enum TemperatureSensorScale {
|
|
CELSIUS = "CELSIUS",
|
|
FAHRENHEIT = "FAHRENHEIT",
|
|
}
|
|
|
|
interface DirectiveHeader {
|
|
namespace: string;
|
|
name: string;
|
|
payloadVersion: string;
|
|
messageId: string;
|
|
correlationToken: string;
|
|
}
|
|
|
|
interface DirectiveEndpoint {
|
|
endpointId: string;
|
|
}
|
|
|
|
interface ContextProperty {
|
|
namespace: string;
|
|
name: string;
|
|
value: any;
|
|
timeOfSample: string;
|
|
uncertaintyInMilliseconds: number;
|
|
instance?: string;
|
|
}
|
|
|
|
/** Why a ChangeReport is sent (Alexa.ChangeReport payload.change.cause.type). */
|
|
export type ChangeCause = "APP_INTERACTION" | "PHYSICAL_INTERACTION" | "PERIODIC_POLL" | "RULE_TRIGGER" | "VOICE_INTERACTION";
|
|
|
|
export class AlexaStatusMessage {
|
|
private context: { properties: ContextProperty[] } = { properties: [] };
|
|
/** ChangeReport only: the properties that changed (payload.change.properties); the rest go to context. */
|
|
private changeProps: ContextProperty[] = [];
|
|
private changeCause: ChangeCause | null = null;
|
|
private target: "context" | "change" = "context";
|
|
private event: {
|
|
header: DirectiveHeader;
|
|
endpoint: DirectiveEndpoint;
|
|
payload: {};
|
|
};
|
|
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,
|
|
changeCause: ChangeCause | null = null
|
|
) {
|
|
this.rootTopic = rootTopic;
|
|
this.endpointId = endpointId;
|
|
this.mqttClient = mqttClient;
|
|
this.isDeferred = isDeferred;
|
|
this.changeCause = changeCause;
|
|
if (changeCause) this.target = "change";
|
|
this.event = {
|
|
header: {
|
|
namespace: "Alexa",
|
|
name: changeCause
|
|
? "ChangeReport"
|
|
: isDeferred
|
|
? "DeferredResponse"
|
|
: isResponse
|
|
? "Response"
|
|
: "StateReport",
|
|
payloadVersion: "3",
|
|
messageId: this.generateMessageId(),
|
|
correlationToken,
|
|
},
|
|
endpoint: {
|
|
endpointId,
|
|
},
|
|
payload: {},
|
|
};
|
|
}
|
|
|
|
private generateMessageId(): string {
|
|
return uuidv4();
|
|
}
|
|
|
|
private getTimestamp(): string {
|
|
return new Date().toISOString();
|
|
}
|
|
private addProperty(
|
|
namespace: AlexaInterfaceType,
|
|
name: string,
|
|
value: any,
|
|
uncertaintyInMilliseconds = 0,
|
|
instance?: string
|
|
): this {
|
|
const prop: ContextProperty = {
|
|
namespace,
|
|
name,
|
|
value,
|
|
timeOfSample: this.getTimestamp(),
|
|
uncertaintyInMilliseconds,
|
|
};
|
|
if (instance) prop.instance = instance;
|
|
(this.target === "change" ? this.changeProps : this.context.properties).push(prop);
|
|
return this;
|
|
}
|
|
/** ChangeReport: the add*Prop calls that follow describe what CHANGED (the default for a change report). */
|
|
public changed(): this {
|
|
this.target = "change";
|
|
return this;
|
|
}
|
|
/** ChangeReport: the add*Prop calls that follow describe the other, unchanged properties (context). */
|
|
public unchanged(): this {
|
|
this.target = "context";
|
|
return this;
|
|
}
|
|
/** True for a ChangeReport (Device.getChangeReport). */
|
|
public isChangeReport(): boolean {
|
|
return this.changeCause !== null;
|
|
}
|
|
/** The message as it will be published (for tests and logging). */
|
|
public toJSON(): { event: any; context: { properties: ContextProperty[] } | null } {
|
|
if (this.changeCause) {
|
|
return {
|
|
event: { ...this.event, payload: { change: { cause: { type: this.changeCause }, properties: this.changeProps } } },
|
|
context: this.context,
|
|
};
|
|
}
|
|
return { context: this.isDeferred ? null : this.context, event: this.event };
|
|
}
|
|
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(
|
|
AlexaInterfaceType.ENDPOINT_HEALTH,
|
|
"connectivity",
|
|
{ value: health },
|
|
uncertaintyInMs
|
|
);
|
|
}
|
|
|
|
public addPowerControllerProp(
|
|
power: PowerController,
|
|
uncertaintyInMs = 0
|
|
): this {
|
|
return this.addProperty(
|
|
AlexaInterfaceType.POWER_CONTROLLER,
|
|
"powerState",
|
|
power,
|
|
uncertaintyInMs
|
|
);
|
|
}
|
|
|
|
public addTemperatureSensorProp(
|
|
scale: TemperatureSensorScale,
|
|
value: number,
|
|
uncertaintyInMs = 0
|
|
): this {
|
|
const tempValue = {
|
|
value:
|
|
scale === TemperatureSensorScale.FAHRENHEIT
|
|
? (value - 32) * (5 / 9)
|
|
: value,
|
|
scale: "CELSIUS",
|
|
};
|
|
return this.addProperty(
|
|
AlexaInterfaceType.TEMPERATURE_SENSOR,
|
|
"temperature",
|
|
tempValue,
|
|
uncertaintyInMs
|
|
);
|
|
}
|
|
|
|
public addBrightnessControllerProp(
|
|
brightness: number,
|
|
uncertaintyInMs = 0
|
|
): this {
|
|
return this.addProperty(
|
|
AlexaInterfaceType.BRIGHTNESS_CONTROLLER,
|
|
"brightness",
|
|
brightness,
|
|
uncertaintyInMs
|
|
);
|
|
}
|
|
|
|
public addColorTemperatureControllerProp(
|
|
colorTemp: number,
|
|
uncertaintyInMs = 0
|
|
): this {
|
|
return this.addProperty(
|
|
AlexaInterfaceType.COLOR_TEMPERATURE_CONTROLLER,
|
|
"colorTemperatureInKelvin",
|
|
colorTemp,
|
|
uncertaintyInMs
|
|
);
|
|
}
|
|
|
|
public addToggleControllerProp(
|
|
state: PowerController,
|
|
instance: string,
|
|
uncertaintyInMs = 0
|
|
): this {
|
|
return this.addProperty(
|
|
AlexaInterfaceType.TOGGLE_CONTROLLER,
|
|
"toggleState",
|
|
state,
|
|
uncertaintyInMs,
|
|
instance
|
|
);
|
|
}
|
|
|
|
public addContextProp(prop: ContextProperty): this {
|
|
this.context.properties.push(prop);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Publish: a Response/StateReport to <root>/<endpoint>/alexaResponce (sendAsync: deferredResponse), a ChangeReport
|
|
* to <root>/changeReport (Alex2MQTT adds the user's token and posts it to the Alexa event gateway). Resolves with
|
|
* the topic; rejects on a publish error (1.4.0 only logged).
|
|
*/
|
|
public send(sendAsync: boolean = false): Promise<string> {
|
|
const payloadStr = JSON.stringify(this.toJSON());
|
|
const topic = this.changeCause
|
|
? `${this.rootTopic}/changeReport`
|
|
: `${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
|
|
return new Promise((resolve, reject) => {
|
|
this.mqttClient.publish(topic, payloadStr, (err) => (err ? reject(err) : resolve(topic)));
|
|
});
|
|
}
|
|
}
|