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

42
examples/README.md Normal file
View file

@ -0,0 +1,42 @@
# Examples
One file per recipe, on the 2.0 API. Each file is complete: it declares one device, answers its directives and
connects. The state of the device is an object in the file, which you replace with what drives your device.
| File | Device | Interfaces | Shows |
|---|---|---|---|
| [lamp.js](lamp.js) | lamp | PowerController, BrightnessController | handlers, `device.state()`, `ctx.respond()` |
| [color-lamp.js](color-lamp.js) | colour lamp | + ColorController, ColorTemperatureController | a state that depends on the mode of the device |
| [thermostat.js](thermostat.js) | thermostat | ThermostatController, TemperatureSensor | one setpoint, three modes, temperature scales |
| [blind.js](blind.js) | roller blind | RangeController | an instance, friendly names, semantics for open and close |
| [lock.js](lock.js) | lock | LockController | `ctx.defer()` for an answer that takes longer than 7 seconds |
| [sensor.js](sensor.js) | contact sensor | ContactSensor | `device.changeReport()` |
| [scene.js](scene.js) | scene | SceneController | ActivationStarted and DeactivationStarted |
| [doorbell.js](doorbell.js) | doorbell | DoorbellEventSource | `device.raise()` |
| [errors.js](errors.js) | fan | PowerController, RangeController | `AlexaErrors`, `ctx.error()` |
| [plug.mjs](plug.mjs) | plug | PowerController | the library as an ES module |
## Run one
The examples read the MQTT user name, the password and the root topic of your Alex2MQTT account from the
environment, and say which variable is missing:
```sh
ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/lamp.js
```
In a checkout of this repository `npm install` comes first: it builds `dist/`, which `require("alex2node")` resolves
to. Then discover the devices in the Alexa app.
`sensor.js` and `doorbell.js` wait for the Enter key: it opens and closes the door, and rings the bell.
`lock.js` takes `BOLT_SECONDS`, the time its bolt needs, 8 by default.
## What was tested
`test/examples.test.js` starts every file with `node`, against a broker on 127.0.0.1, discovers the device, sends
it directives and checks the answers. The examples were not run against an Alexa account. `doorbell.js` publishes
to `<root>/event`, and whether a press reaches Alexa depends on the Alex2MQTT service relaying that topic.
## 1.x
[legacy/](legacy) has the nine examples of 1.5.2, on the 1.x API.

54
examples/blind.js Normal file
View file

@ -0,0 +1,54 @@
// A roller blind: Alexa.RangeController with the position in percent, 0 closed and 100 open. The semantics say
// what "open", "close", "raise" and "lower" do, and which positions count as open and as closed.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/blind.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { Alex2MQTT, RangeController, asset, text, semantics } = 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 blind = { position: 0 };
const device = bridge.addDevice({ endpointId: "bedroom-blind", name: "Bedroom Blind", categories: ["INTERIOR_BLIND"] });
const lift = device.add(RangeController, {
instance: "Blind.Lift",
friendlyNames: [asset("Alexa.Setting.Opening"), text("Position", "en-US")],
range: { min: 0, max: 100, precision: 1 },
unit: "Alexa.Unit.Percent",
semantics: semantics()
.action("Close", "SetRangeValue", { rangeValue: 0 })
.action("Open", "SetRangeValue", { rangeValue: 100 })
.action("Lower", "AdjustRangeValue", { rangeValueDelta: -10, rangeValueDeltaDefault: false })
.action("Raise", "AdjustRangeValue", { rangeValueDelta: 10, rangeValueDeltaDefault: false })
.state("Closed", 0)
.stateRange("Open", 1, 100),
});
// lift carries the instance: the property is reported under "Blind.Lift"
device.state((s) => s.set(lift, "rangeValue", blind.position).health("OK"));
const within = (position) => Math.min(100, Math.max(0, Math.round(position)));
lift.on("SetRangeValue", (ctx) => {
blind.position = within(ctx.payload.rangeValue);
return ctx.respond();
});
// rangeValueDeltaDefault: the user named no amount, and the blind picks its own step
lift.on("AdjustRangeValue", (ctx) => {
const { rangeValueDelta, rangeValueDeltaDefault } = ctx.payload;
const delta = rangeValueDeltaDefault ? Math.sign(rangeValueDelta) * 10 : rangeValueDelta;
blind.position = within(blind.position + delta);
return ctx.respond();
});
bridge.connect();

76
examples/color-lamp.js Normal file
View file

@ -0,0 +1,76 @@
// A lamp that shows a colour or a white: Alexa.ColorController and Alexa.ColorTemperatureController, with power
// and brightness. It reports the colour or the colour temperature, whichever it shows.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/color-lamp.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { Alex2MQTT, AlexaErrors, PowerController, BrightnessController, ColorController, ColorTemperatureController } = 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));
// The whites Alexa asks for by name: warm, soft, white, daylight, cool
const WHITES = [2200, 2700, 4000, 5500, 7000];
// color is null while the lamp shows a white
const lamp = { on: false, brightness: 100, color: null, kelvin: 2700 };
const device = bridge.addDevice({ endpointId: "floor-lamp", name: "Floor Lamp", categories: ["LIGHT"] });
const power = device.add(PowerController);
const brightness = device.add(BrightnessController);
const color = device.add(ColorController);
const white = device.add(ColorTemperatureController);
device.state((s) => {
s.set(power, "powerState", lamp.on ? "ON" : "OFF").set(brightness, "brightness", lamp.brightness);
if (lamp.color) s.set(color, "color", { ...lamp.color, brightness: lamp.brightness / 100 });
else s.set(white, "colorTemperatureInKelvin", lamp.kelvin);
s.health("OK");
});
power.on("TurnOn", (ctx) => {
lamp.on = true;
return ctx.respond();
});
power.on("TurnOff", (ctx) => {
lamp.on = false;
return ctx.respond();
});
brightness.on("SetBrightness", (ctx) => {
lamp.brightness = ctx.payload.brightness;
lamp.on = true;
return ctx.respond();
});
// SetColor comes with brightness 1 whatever the lamp shows: the lamp takes hue and saturation and keeps its brightness
color.on("SetColor", (ctx) => {
const { hue, saturation } = ctx.payload.color;
lamp.color = { hue, saturation };
lamp.on = true;
return ctx.respond();
});
white.on("SetColorTemperature", (ctx) => {
lamp.kelvin = Math.min(7000, Math.max(2200, ctx.payload.colorTemperatureInKelvin));
lamp.color = null;
lamp.on = true;
return ctx.respond();
});
// "Warmer" and "cooler" have no meaning while the lamp shows a colour. A handler that throws answers with the error.
function step(by) {
return (ctx) => {
if (lamp.color) throw AlexaErrors.notSupportedInCurrentMode("The lamp shows a colour", "COLOR");
const next = WHITES.indexOf(WHITES.find((kelvin) => kelvin >= lamp.kelvin) ?? 7000) + by;
lamp.kelvin = WHITES[Math.min(WHITES.length - 1, Math.max(0, next))];
return ctx.respond();
};
}
white.on("IncreaseColorTemperature", step(1));
white.on("DecreaseColorTemperature", step(-1));
bridge.connect();

40
examples/doorbell.js Normal file
View file

@ -0,0 +1,40 @@
// A doorbell: Alexa.DoorbellEventSource. It takes no directive and reports no state of the button;
// device.raise() says that somebody rang. Press Enter to ring.
//
// The event is published to <root>/event. Not tested with an Alexa account yet: whether the press reaches Alexa
// depends on the Alex2MQTT service relaying that topic. Alexa wants 30 seconds between two presses of one doorbell.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/doorbell.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { Alex2MQTT, DoorbellEventSource } = 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 device = bridge.addDevice({ endpointId: "front-doorbell", name: "Front Doorbell", categories: ["DOORBELL"] });
device.add(DoorbellEventSource);
device.state((s) => s.health("OK"));
let rangAt = 0;
process.stdin.on("data", async () => {
if (Date.now() - rangAt < 30000) {
console.log("not sent: the last press is less than 30 seconds ago");
return;
}
rangAt = Date.now();
// raise() throws for an event the device did not declare, and resolves with what became of the publish
const result = await device.raise(DoorbellEventSource, "DoorbellPress");
console.log(result.ok ? "the press was sent" : `not sent: ${result.error.message}`);
});
bridge.connect();

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

52
examples/lamp.js Normal file
View file

@ -0,0 +1,52 @@
// A lamp that switches and dims: Alexa.PowerController and Alexa.BrightnessController.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/lamp.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { Alex2MQTT, PowerController, BrightnessController } = 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));
// The lamp itself. Replace it with what drives yours.
const lamp = { on: false, brightness: 100 };
const device = bridge.addDevice({ endpointId: "desk-lamp", name: "Desk Lamp", categories: ["LIGHT"] });
const power = device.add(PowerController);
const brightness = device.add(BrightnessController);
// The whole state: the answer to ReportState, and the context of every ctx.respond()
device.state((s) => s
.set(power, "powerState", lamp.on ? "ON" : "OFF")
.set(brightness, "brightness", lamp.brightness)
.health("OK"));
power.on("TurnOn", (ctx) => {
lamp.on = true;
return ctx.respond();
});
power.on("TurnOff", (ctx) => {
lamp.on = false;
return ctx.respond();
});
// Both brightness directives turn a lamp on that is off
brightness.on("SetBrightness", (ctx) => {
lamp.brightness = ctx.payload.brightness;
lamp.on = true;
return ctx.respond();
});
brightness.on("AdjustBrightness", (ctx) => {
lamp.brightness = Math.min(100, Math.max(0, lamp.brightness + ctx.payload.brightnessDelta));
lamp.on = true;
return ctx.respond();
});
bridge.connect();

12
examples/legacy/README.md Normal file
View file

@ -0,0 +1,12 @@
# Examples of 1.5.2
The nine examples of alex2node 1.5.2, as they were. They run on 2.0 through its compat layer, which keeps the 1.x
API: `registerDevice()`, `addCapability()`, the `Event` and `ReportState` listeners, `getStatusMessage()` and
`getErrorMessage()`. `test/examples.test.js` starts each of them and compares its discovery answer with the one
1.5.2 gave; 2.0 adds the `Alexa` interface at the end of the capabilities and changes nothing else in them.
They read `MQTT_USERNAME`, `MQTT_PASSWORD` and `MQTT_ROOT_TOPIC` from the environment or from a `.env` file, with
`dotenv`, which is a development dependency of this repository.
For a new program use the examples one directory up. [docs/wire-changes.md](../../docs/wire-changes.md) lists what
a 1.x program can observe of 2.0.

49
examples/lock.js Normal file
View file

@ -0,0 +1,49 @@
// A lock whose bolt takes longer than the 7 seconds Alex2MQTT waits for an answer: Alexa.LockController. The
// handler sends a DeferredResponse at once and the Response when the bolt has moved. Alex2MQTT waits 10 seconds for
// the Response after a DeferredResponse.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/lock.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { Alex2MQTT, LockController } = 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));
// How long the bolt takes, in seconds
const BOLT_SECONDS = Number(process.env.BOLT_SECONDS ?? 8);
const lock = { state: "LOCKED" };
const moveBolt = (state) => new Promise((resolve) => {
setTimeout(() => {
lock.state = state;
resolve();
}, BOLT_SECONDS * 1000);
});
const device = bridge.addDevice({ endpointId: "front-door-lock", name: "Front Door", categories: ["SMARTLOCK"] });
const bolt = device.add(LockController);
device.state((s) => s.set(bolt, "lockState", lock.state).health("OK"));
function move(state) {
return async (ctx) => {
// The argument is optional: how many seconds the answer will take
await ctx.defer(Math.ceil(BOLT_SECONDS));
await moveBolt(state);
// After defer() the answer goes to <root>/<endpointId>/deferredResponse
return ctx.respond();
};
}
bolt.on("Lock", move("LOCKED"));
bolt.on("Unlock", move("UNLOCKED"));
bridge.connect();

35
examples/plug.mjs Normal file
View file

@ -0,0 +1,35 @@
// A plug, as an ES module: the same library with import. Alexa.PowerController.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/plug.mjs
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
import { Alex2MQTT, PowerController } from "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 plug = { on: false };
const device = bridge.addDevice({ endpointId: "coffee-plug", name: "Coffee Machine", categories: ["SMARTPLUG"] });
const power = device.add(PowerController);
device.state((s) => s.set(power, "powerState", plug.on ? "ON" : "OFF").health("OK"));
power.on("TurnOn", (ctx) => {
plug.on = true;
return ctx.respond();
});
power.on("TurnOff", (ctx) => {
plug.on = false;
return ctx.respond();
});
bridge.connect();

41
examples/scene.js Normal file
View file

@ -0,0 +1,41 @@
// A scene: Alexa.SceneController. A scene is an endpoint that is not a device. It reports no state, and Activate
// and Deactivate are answered with ActivationStarted and DeactivationStarted, which ctx.respond() sends.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/scene.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { Alex2MQTT, SceneController } = 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));
// What the scene sets. Replace it with your devices.
const room = { lights: "bright", blind: "open" };
// Alexa wants the word "scene" in the description, and the category SCENE_TRIGGER or ACTIVITY_TRIGGER
const device = bridge.addDevice({
endpointId: "movie-night", name: "Movie Night", categories: ["SCENE_TRIGGER"],
description: "Movie night scene by Alex2Node",
});
const scene = device.add(SceneController, { supportsDeactivation: true });
scene.on("Activate", (ctx) => {
Object.assign(room, { lights: "dim", blind: "closed" });
console.log("movie night on", room);
return ctx.respond();
});
scene.on("Deactivate", (ctx) => {
Object.assign(room, { lights: "bright", blind: "open" });
console.log("movie night off", room);
return ctx.respond();
});
bridge.connect();

40
examples/sensor.js Normal file
View file

@ -0,0 +1,40 @@
// A contact sensor on a door: Alexa.ContactSensor. It takes no directive. It answers ReportState and sends a
// ChangeReport when the door opens or closes. Press Enter to open and close the door.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/sensor.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { Alex2MQTT, ContactSensor } = 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));
// DETECTED is open: the two pieces of the sensor are apart
const door = { contact: "NOT_DETECTED", changedAt: new Date() };
const device = bridge.addDevice({ endpointId: "back-door", name: "Back Door", categories: ["CONTACT_SENSOR"] });
// proactivelyReported: Alexa takes ChangeReports for the capability
const contact = device.add(ContactSensor, { proactivelyReported: true });
device.state((s) => s.set(contact, "detectionState", door.contact, { timeOfSample: door.changedAt }).health("OK"));
async function changed(state) {
door.contact = state;
door.changedAt = new Date();
// What changed is set here. The rest of device.state(), the health, is the context of the report.
const result = await device.changeReport("PHYSICAL_INTERACTION", (s) => s
.set(contact, "detectionState", door.contact, { timeOfSample: door.changedAt }));
console.log(result.ok ? `the door reported ${state}` : `not reported: ${result.error.message}`);
}
process.stdin.on("data", () => changed(door.contact === "DETECTED" ? "NOT_DETECTED" : "DETECTED"));
bridge.connect();

72
examples/thermostat.js Normal file
View file

@ -0,0 +1,72 @@
// 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();