// Import the library ("alex2node" is the installed package; inside this checkout it resolves to the built dist/ via package.json "exports") const { Alex2MQTT, AlexaInterfaceType, TemperatureSensorScale, DisplayCategory, ThermostatMode, EndpointHealth, } = require("alex2node"); require("dotenv").config(); // Load environment variables from .env file // 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 alex2NodeClient.on("error", (e) => console.error("broker:", e.message)); // Log broker errors (on 1.5.0 this listener is also what keeps an outage from killing the process) // Define the device name (Thermostat Test) const deviceName = "Thermostat Test"; // Register the thermostat device with a unique endpoint ID const thermostat = alex2NodeClient.registerDevice( deviceName, "thermostat-1", [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: "Auto", locale: "en-US", }, }, ], }, }, { value: "mode.fanmode.On", modeResources: { friendlyNames: [ { "@type": "text", value: { text: "On", 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 = { // all temperatures in Fahrenheit; the library converts to Celsius for Alexa 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(EndpointHealth.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 ); // Single setpoint (HEAT / COOL) } 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(EndpointHealth.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 ); // Single setpoint (HEAT / COOL) } 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; }