Add full support and examples for thermostat

This commit is contained in:
David 2025-05-10 12:21:51 +00:00
parent 7f051dfdcb
commit f4d07b589c
12 changed files with 307 additions and 32 deletions

View file

@ -1,6 +1,7 @@
import mqtt, { MqttClient } from "mqtt";
import Device from "./Device";
import { EventEmitter } from "events";
import { DisplayCategory } from "./DisplayCategory";
class Alex2MQTT extends EventEmitter {
private client: MqttClient | null = null;
@ -15,7 +16,6 @@ class Alex2MQTT extends EventEmitter {
) {
super(); // Initialize EventEmitter
}
connect(): void {
const options = {
@ -69,7 +69,7 @@ class Alex2MQTT extends EventEmitter {
topic + "_r"
}`
);
console.log(deviceArray);
console.log(JSON.stringify(deviceArray, null, 2));
}
}
);
@ -106,8 +106,8 @@ class Alex2MQTT extends EventEmitter {
);
}
device.emit("ReportState", payload);
}else{
device.emit("Event", payload,payload.header.namespace);
} else {
device.emit("Event", payload, payload.header.namespace);
}
}
});
@ -120,7 +120,11 @@ class Alex2MQTT extends EventEmitter {
});
}
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");
}
@ -129,7 +133,20 @@ class Alex2MQTT extends EventEmitter {
`[Alex2Node.ts] Creating new device with endpoint: ${endpointId}`
);
}
const device = new Device(this.client!, this.rootTopic, name, endpointId);
const normalizedCategory =
displayCategory === null
? null
: Array.isArray(displayCategory)
? displayCategory
: [displayCategory];
const device = new Device(
this.client!,
this.rootTopic,
name,
endpointId,
normalizedCategory
);
this.devices.push(device);
return device;
}

View file

@ -176,11 +176,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:
@ -210,7 +211,13 @@ export class AlexaInterface {
return ["toggleState"];
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:
@ -252,7 +259,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 +283,15 @@ 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
}
}
if (this.instance) {
doc.instance = this.instance;
}

View file

@ -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",
@ -101,6 +110,38 @@ export class AlexaStatusMessage {
this.context.properties.push(prop);
return this;
}
public addThermostatModeProp(
mode: string,
uncertaintyInMs = 0
): this {
return this.addProperty(
AlexaInterfaceType.THERMOSTAT_CONTROLLER,
"thermostatMode",
mode,
uncertaintyInMs
);
}
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(
@ -191,7 +232,7 @@ export class AlexaStatusMessage {
context: 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}/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) => {

View file

@ -13,7 +13,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 +34,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 {
@ -111,7 +111,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,

View file

@ -1,4 +1,5 @@
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';