Alex2Node/src/AlexaStatusMessage.ts

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)));
});
}
}