Ten recipes: lamp, colour lamp, thermostat, blind, lock (deferred), contact sensor (ChangeReport), scene, doorbell (raise), typed errors, and a plug as an ES module. Each reads ALEX2MQTT_USERNAME, _PASSWORD and _ROOT_TOPIC and names the ones that are missing. The nine examples of 1.5.2 move to examples/legacy unchanged. test/examples.test.js starts every file with node, its broker connection sent to a broker on 127.0.0.1 (test/helpers/loopback.js), discovers the device, sends directives and checks the answers. A legacy file is started and its discovery compared with the answer the 1.5.2 build gave, recorded in test/fixtures/legacy-examples. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
72 lines
3.4 KiB
JavaScript
72 lines
3.4 KiB
JavaScript
// A thermostat with one setpoint and the modes HEAT, COOL and OFF: Alexa.ThermostatController, and
|
|
// Alexa.TemperatureSensor for the temperature of the room. It works in Celsius and takes Fahrenheit and Kelvin.
|
|
//
|
|
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/thermostat.js
|
|
//
|
|
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
|
|
const { Alex2MQTT, AlexaErrors, ThermostatController, TemperatureSensor } = require("alex2node");
|
|
|
|
const missing = ["ALEX2MQTT_USERNAME", "ALEX2MQTT_PASSWORD", "ALEX2MQTT_ROOT_TOPIC"].filter((name) => !process.env[name]);
|
|
if (missing.length > 0) {
|
|
console.error(`${missing.join(", ")} not set. Set the MQTT user name, the password and the root topic of your Alex2MQTT account.`);
|
|
process.exit(1);
|
|
}
|
|
const { ALEX2MQTT_USERNAME, ALEX2MQTT_PASSWORD, ALEX2MQTT_ROOT_TOPIC } = process.env;
|
|
|
|
const bridge = new Alex2MQTT(ALEX2MQTT_USERNAME, ALEX2MQTT_PASSWORD, ALEX2MQTT_ROOT_TOPIC);
|
|
bridge.on("connect", () => console.log("connected, discover the devices in the Alexa app"));
|
|
bridge.on("error", (err) => console.error("bridge:", err.message));
|
|
|
|
const MODES = ["HEAT", "COOL", "OFF"];
|
|
const RANGE = { min: 5, max: 30 };
|
|
const thermostat = { mode: "HEAT", target: 21, room: 19.5 };
|
|
|
|
const celsius = (value) => ({ value, scale: "CELSIUS" });
|
|
// A difference has no offset: 9 degrees Fahrenheit more are 5 degrees Celsius more
|
|
const deltaInCelsius = ({ value, scale }) => (scale === "FAHRENHEIT" ? (value * 5) / 9 : value);
|
|
function inCelsius({ value, scale }) {
|
|
if (scale === "FAHRENHEIT") return ((value - 32) * 5) / 9;
|
|
return scale === "KELVIN" ? value - 273.15 : value;
|
|
}
|
|
|
|
const device = bridge.addDevice({
|
|
endpointId: "hall-thermostat", name: "Hall Thermostat", categories: ["THERMOSTAT", "TEMPERATURE_SENSOR"],
|
|
});
|
|
const control = device.add(ThermostatController, { supportedModes: MODES, properties: ["targetSetpoint", "thermostatMode"] });
|
|
const sensor = device.add(TemperatureSensor);
|
|
|
|
device.state((s) => s
|
|
.set(control, "targetSetpoint", celsius(thermostat.target))
|
|
.set(control, "thermostatMode", thermostat.mode)
|
|
.set(sensor, "temperature", celsius(thermostat.room))
|
|
.health("OK"));
|
|
|
|
function setTarget(ctx, target) {
|
|
if (thermostat.mode === "OFF") throw AlexaErrors.thermostatIsOff("The thermostat is off");
|
|
const rounded = Math.round(target * 2) / 2;
|
|
if (rounded < RANGE.min || rounded > RANGE.max) {
|
|
throw AlexaErrors.temperatureOutOfRange(
|
|
`The thermostat takes ${RANGE.min} to ${RANGE.max} degrees Celsius`,
|
|
{ minimumValue: celsius(RANGE.min), maximumValue: celsius(RANGE.max) }
|
|
);
|
|
}
|
|
thermostat.target = rounded;
|
|
return ctx.respond();
|
|
}
|
|
|
|
control.on("SetTargetTemperature", (ctx) => {
|
|
const { targetSetpoint, lowerSetpoint, upperSetpoint } = ctx.payload;
|
|
if (!targetSetpoint || lowerSetpoint || upperSetpoint) {
|
|
throw AlexaErrors.dualSetpointsUnsupported("The thermostat has one setpoint");
|
|
}
|
|
return setTarget(ctx, inCelsius(targetSetpoint));
|
|
});
|
|
control.on("AdjustTargetTemperature", (ctx) => setTarget(ctx, thermostat.target + deltaInCelsius(ctx.payload.targetSetpointDelta)));
|
|
control.on("SetThermostatMode", (ctx) => {
|
|
const mode = ctx.payload.thermostatMode.value;
|
|
if (!MODES.includes(mode)) throw AlexaErrors.unsupportedThermostatMode(`The thermostat has no mode ${mode}`);
|
|
thermostat.mode = mode;
|
|
return ctx.respond();
|
|
});
|
|
|
|
bridge.connect();
|