examples: one runnable file per recipe on the 2.0 API, the 1.x files in examples/legacy

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>
This commit is contained in:
David 2026-09-28 21:38:07 +00:00
parent 2cbcde563e
commit 8379a65d78
25 changed files with 1705 additions and 0 deletions

61
examples/errors.js Normal file
View file

@ -0,0 +1,61 @@
// A fan that says no: the error types of Alexa as answers. A handler throws an AlexaError of AlexaErrors, or
// answers with ctx.error(). Anything else a handler throws is answered with INTERNAL_ERROR, and a directive without
// a handler with INVALID_DIRECTIVE.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/errors.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { Alex2MQTT, AlexaErrors, PowerController, RangeController, text } = 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 SPEEDS = { min: 1, max: 3 };
const fan = { on: false, speed: 1 };
const device = bridge.addDevice({ endpointId: "attic-fan", name: "Attic Fan", categories: ["FAN"] });
const power = device.add(PowerController);
const speed = device.add(RangeController, {
instance: "Fan.Speed",
friendlyNames: [text("Speed", "en-US")],
range: { min: SPEEDS.min, max: SPEEDS.max, precision: 1 },
});
device.state((s) => s
.set(power, "powerState", fan.on ? "ON" : "OFF")
.set(speed, "rangeValue", fan.speed)
.health("OK"));
power.on("TurnOn", (ctx) => {
fan.on = true;
return ctx.respond();
});
power.on("TurnOff", (ctx) => {
fan.on = false;
return ctx.respond();
});
speed.on("SetRangeValue", (ctx) => {
const { rangeValue } = ctx.payload;
// Without a throw: the type and the message
if (!fan.on) return ctx.error("NOT_IN_OPERATION", "The fan is off");
// With a throw: every type has a helper, and the helper of a type with fields of its own takes them
if (!Number.isInteger(rangeValue) || rangeValue < SPEEDS.min || rangeValue > SPEEDS.max) {
throw AlexaErrors.valueOutOfRange(
`The fan has the speeds ${SPEEDS.min} to ${SPEEDS.max}`,
{ minimumValue: SPEEDS.min, maximumValue: SPEEDS.max }
);
}
fan.speed = rangeValue;
return ctx.respond();
});
// AdjustRangeValue has no handler: the bridge answers it with INVALID_DIRECTIVE
bridge.connect();