# Alex2Node Alex2Node makes a Node.js process a set of Alexa smart home devices. You declare a device and its capabilities, and answer the directives Alexa sends; the library writes the discovery answer, checks what goes in and out against the description of each interface, and publishes the responses. It talks MQTT to the [Alex2MQTT](https://alex2mqtt.stormysdream.club/) service, which is the Alexa skill, so you write no skill and run no public endpoint. ``` Alexa -> Alex2MQTT (the skill) -> MQTT broker -> your process (alex2node) ``` Alex2ESP does the same on an ESP8266, on the same topics. ## What you need - Node.js 18 or later. - An Alex2MQTT account. Log in at https://alex2mqtt.stormysdream.club/ with your Amazon account; the page shows the three values the library takes: the MQTT user name, the MQTT password and the root topic. How Alex2MQTT is set up with Alexa is described there. ## Install ```sh npm install alex2node # from npm npm install git+https://git.stormysdream.club/apps/Alex2Node.git#v2.0.0 # the tag, from the repository ``` npm can be behind the repository: `npm view alex2node version` says what it has. The repository is public, so the `https` form needs no account. `#` or `#` pins a revision, and npm records the resolved commit in `package-lock.json`. The package has a CommonJS and an ES module build, and its type declarations: ```js const { Alex2MQTT, PowerController } = require("alex2node"); ``` ```js import { Alex2MQTT, PowerController } from "alex2node"; ``` ## Quick start A lamp that switches and dims. This is [examples/lamp.js](examples/lamp.js): ```js // 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(); ``` ```sh ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node lamp.js ``` Then discover the devices in the Alexa app. The lamp answers while the process runs. ## Concepts **Bridge.** One `Alex2MQTT` is one broker connection for one root topic. The constructor takes the three credentials, then `debugLogging` and the options: ```js const bridge = new Alex2MQTT(username, password, rootTopic, false, { host: "mqtt://127.0.0.1:1883", // default: the public Alex2MQTT broker mqtt: { clientId: "my-house" }, // merged over the mqtt.js client options log: (line, detail) => console.debug(line, detail ?? ""), answerWithinMs: 6500, // the default; 0 turns the watchdog off }); bridge.on("error", (err) => console.error(err.message)); bridge.on("unanswered", ({ endpointId, namespace, name }) => console.warn(`${endpointId}: ${namespace}.${name} got no answer`)); bridge.connect(); ``` Its events are `connect`, `offline`, `reconnect`, `close`, `error`, `discover` (the number of devices announced), `directive`, `unknownEndpoint` and `unanswered`. None has to be listened to. **Device.** `bridge.addDevice({ endpointId, name, categories, ... })` declares an endpoint. The `endpointId` is what Alexa knows the device by: keep it the same at every start. A device can be declared before `connect()`. `addDevice()` throws a `DeclarationError` for what Alexa would reject, an `endpointId` with a slash or a name with punctuation. **Capability.** `device.add(Interface, options)` declares an interface of the device and returns the capability that handlers and state are set on. `Alexa.EndpointHealth` and `Alexa` are added to the discovery answer by the library. A generic controller (Range, Mode, Toggle) takes an instance name and friendly names, and a device can have several: ```js const blind = bridge.addDevice({ endpointId: "bedroom-blind", name: "Bedroom Blind", categories: ["INTERIOR_BLIND"] }); const lift = blind.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 }) .state("Closed", 0) .stateRange("Open", 1, 100), }); ``` `add()` throws a `DeclarationError` that names the endpoint, the interface and the instance. `device.check()` returns what Alexa would reject in the device as lines of text, and the bridge logs each line once when it answers a discovery. **State.** `device.state(fill)` says how to read the whole state of the device. It answers `ReportState`, and it is the context of every response, which Alexa wants complete. Values are checked against the interface: a `powerState` of `"on"` throws a `SchemaError`. ```js blind.state((s) => s.set(lift, "rangeValue", motor.position).health("OK")); ``` **Handlers.** `capability.on("", handler)` answers one directive, `capability.on("*", handler)` the others of the capability, `device.onDirective(handler)` what is left on the device, and `device.onReportState(handler)` answers `ReportState` in place of `device.state()`. A handler gets one argument: ```js lift.on("SetRangeValue", async (ctx) => { ctx.payload.rangeValue; // the payload, checked against the interface ctx.instance; // "Blind.Lift" if (motor.blocked) return ctx.error("ENDPOINT_BUSY", "The blind is moving"); await motor.moveTo(ctx.payload.rangeValue); return ctx.respond(); // a Response with the state of device.state() }); ``` | Call | Sends | |---|---| | `ctx.respond(fill?, { payload }?)` | `Response`, or the response the interface has of its own (`Arm.Response`, `ActivationStarted`). `fill` sets properties over those of `device.state()` | | `ctx.report(fill?)` | `StateReport`, in a handler of `onReportState()` | | `ctx.defer(seconds?)` | `DeferredResponse`; the `ctx.respond()` or `ctx.error()` after it goes to the deferred topic | | `ctx.error(type, message, extra?)`, `ctx.error(err)` | `ErrorResponse` | Each resolves with `{ ok, topic, error? }` and none rejects. What a handler returns is awaited; what it throws answers the directive, an `AlexaError` as itself and anything else as `INTERNAL_ERROR`. **Change reports.** When the device changes by itself, say so. The capability has to be declared with `proactivelyReported: true`: ```js const contact = door.add(ContactSensor, { proactivelyReported: true }); const result = await door.changeReport("PHYSICAL_INTERACTION", (s) => s.set(contact, "detectionState", "DETECTED")); if (!result.ok) console.error(result.error.message); ``` `fill` sets what changed; after `s.unchanged()` it sets what did not, and the rest of the context is `device.state()`. `changeReport()` throws a `MessageError` when `fill` sets no property that changed. **Events.** `device.raise(Interface, "", payload?, { instance }?)` sends an event nobody asked for: ```js const result = await bell.raise(DoorbellEventSource, "DoorbellPress"); ``` It throws a `MessageError` for an event the device did not declare. See [Limits](#limits). ## Capabilities A row is an interface with a descriptor: its version, properties, directives and events as the page of the interface gives them. Tier 1 and 2 are described in full: the declaration, the property values and the directive payloads are checked, and the discovery JSON is written from the descriptor. Tier 3 is named only: the interface is announced with its version, nothing about it is checked, and a handler gets the payload as it arrived. | Interface | Version | Properties | Directives | Events | Tier | Through Alexa | | --- | --- | --- | --- | --- | --- | --- | | [Alexa](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-interface.html) | 3 | - | `ReportState` | - | 1 | ReportState | | [Alexa.BrightnessController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-brightnesscontroller.html) | 3 | `brightness` | `SetBrightness`, `AdjustBrightness` | - | 1 | directives | | [Alexa.ChannelController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-channelcontroller.html) | 3 | `channel` | `ChangeChannel`, `SkipChannels` | - | 2 | - | | [Alexa.ColorController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-colorcontroller.html) | 3 | `color` | `SetColor` | - | 1 | directives | | [Alexa.ColorTemperatureController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-colortemperaturecontroller.html) | 3 | `colorTemperatureInKelvin` | `SetColorTemperature`, `IncreaseColorTemperature`, `DecreaseColorTemperature` | - | 1 | directives | | [Alexa.ContactSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-contactsensor.html) | 3 | `detectionState` | - | - | 1 | change report | | [Alexa.DoorbellEventSource](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-doorbelleventsource.html) | 3 | - | - | `DoorbellPress` | 2 | - | | [Alexa.EndpointHealth](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-endpointhealth.html) | 3.1 | `connectivity` | - | - | 1 | - | | [Alexa.EqualizerController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-equalizercontroller.html) | 3 | `bands`, `mode` | `SetMode`, `SetBands`, `AdjustBands`, `ResetBands` | - | 2 | - | | [Alexa.HumiditySensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-humiditysensor.html) | 3 | `relativeHumidity` | - | - | 1 | - | | [Alexa.InputController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-inputcontroller.html) | 3 | `input` | `SelectInput` | - | 2 | - | | [Alexa.InventoryLevelSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-inventorylevelsensor.html) (instances) | 3 | `level` | - | - | 2 | - | | [Alexa.LockController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-lockcontroller.html) | 3 | `lockState` | `Lock`, `Unlock` | - | 1 | directives, change report | | [Alexa.ModeController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-modecontroller.html) (instances) | 3 | `mode` | `SetMode`, `AdjustMode` | - | 1 | directives | | [Alexa.MotionSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-motionsensor.html) | 3 | `detectionState` | - | - | 1 | change report | | [Alexa.PercentageController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-percentagecontroller.html) | 3 | `percentage` | `SetPercentage`, `AdjustPercentage` | - | 1 | directives | | [Alexa.PlaybackController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-playbackcontroller.html) | 3 | - | `Play`, `Pause`, `Stop`, `Next`, `Previous`, `FastForward`, `Rewind`, `StartOver` | - | 2 | - | | [Alexa.PlaybackStateReporter](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-playbackcontroller.html) | 3 | `playbackState` | - | - | 2 | - | | [Alexa.PowerController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-powercontroller.html) | 3 | `powerState` | `TurnOn`, `TurnOff` | - | 1 | directives, change report | | [Alexa.PowerLevelController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-powerlevelcontroller.html) | 3 | `powerLevel` | `SetPowerLevel`, `AdjustPowerLevel` | - | 1 | directives | | [Alexa.RangeController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-rangecontroller.html) (instances) | 3 | `rangeValue` | `SetRangeValue`, `AdjustRangeValue` | - | 1 | directives | | [Alexa.SceneController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-scenecontroller.html) | 3 | - | `Activate`, `Deactivate` | `ActivationStarted`, `DeactivationStarted` | 1 | directives | | [Alexa.SecurityPanelController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-securitypanelcontroller.html) | 3 | `armState`, `burglaryAlarm`, `fireAlarm`, `carbonMonoxideAlarm`, `waterAlarm` | `Arm`, `Disarm` | - | 2 | - | | [Alexa.SimpleEventSource](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-simpleeventsource.html) (instances) | 1.0 | - | - | `Event` | 2 | - | | [Alexa.Speaker](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-speaker.html) | 3 | `volume`, `muted` | `SetVolume`, `AdjustVolume`, `SetMute` | - | 2 | - | | [Alexa.StepSpeaker](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-stepspeaker.html) | 3 | - | `AdjustVolume`, `SetMute` | - | 2 | - | | [Alexa.TemperatureSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-temperaturesensor.html) | 3 | `temperature` | - | - | 1 | change report | | [Alexa.ThermostatController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-thermostatcontroller.html) | 3.2 | `targetSetpoint`, `lowerSetpoint`, `upperSetpoint`, `thermostatMode`, `adaptiveRecoveryStatus` | `SetTargetTemperature`, `AdjustTargetTemperature`, `SetThermostatMode`, `ResumeSchedule` | - | 1 | directives | | [Alexa.ThermostatController.Schedule](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-thermostatcontroller-schedule.html) | 3.2 | `adaptiveRecoveryEnabled`, `scheduleEnabled` | `SetWeeklySchedule`, `SetScheduleState`, `SetAdaptiveRecovery` | - | 2 | - | | [Alexa.TimeHoldController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-timeholdcontroller.html) | 3 | `holdStartTime`, `holdEndTime` | `Hold`, `Resume` | - | 2 | - | | [Alexa.ToggleController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-togglecontroller.html) (instances) | 3 | `toggleState` | `TurnOn`, `TurnOff` | - | 1 | directives | | [Alexa.WakeOnLANController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-wakeonlancontroller.html) | 3 | - | - | `WakeUp` | 2 | - | 32 interfaces are described (tier 1 and 2), 39 are named (tier 3). "Through Alexa" is what a real Alexa account drove through the public Alex2MQTT service on 2026-09-28, with alex2node 1.5.2; "-" is an interface that no such run has covered. Tier 3: [Alexa.ApplicationStateReporter](https://developer.amazon.com/docs/alexaplus/alexa-voice-service/alexa-applicationstatereporter.html) 1, [Alexa.Audio.PlayQueue](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-audio-playqueue.html) 1, [Alexa.AuthorizationController](https://developer.amazon.com/en-US/docs/alexa/ask-overviews/deprecated-features.html) 1, [Alexa.AutomationManagement](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-automationmanagement.html) 1, [Alexa.Automotive.VehicleData](https://developer.amazon.com/en-US/docs/alexa/ask-overviews/deprecated-features.html) 1, [Alexa.Camera.LiveViewController](https://developer.amazon.com/docs/alexaplus/device-apis/list-of-interfaces.html) 1.7, [Alexa.CameraStreamController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-camerastreamcontroller.html) 3, [Alexa.Commissionable](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-commissionable.html) 1, [Alexa.ConsentManagement.ConsentRequiredReporter](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-consentrequiredreporter.html) 1, [Alexa.Cooking](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-cooking.html) 1, [Alexa.Cooking.FoodTemperatureController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-cooking-foodtemperaturecontroller.html) 1, [Alexa.Cooking.FoodTemperatureSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-cooking-foodtemperaturesensor.html) 1, [Alexa.Cooking.PresetController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-cooking-presetcontroller.html) 1, [Alexa.Cooking.TemperatureController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-cooking-temperaturecontroller.html) 1, [Alexa.Cooking.TemperatureSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-cooking-temperaturesensor.html) 1, [Alexa.Cooking.TimeController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-cooking-timecontroller.html) 1, [Alexa.DataController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-datacontroller.html) 1, [Alexa.DeviceUsage.Estimation](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-deviceusage-estimation.html) 1, [Alexa.DeviceUsage.Meter](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-deviceusage-meter.html) 1, [Alexa.InventoryLevelUsageSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-inventorylevelusagesensor.html) 1, [Alexa.InventoryUsageSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-inventoryusagesensor.html) 1, [Alexa.KeypadController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-keypadcontroller.html) 1, [Alexa.Launcher](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-launcher.html) 1.1, [Alexa.Media.PlayQueue](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-media-playqueue.html) 1, [Alexa.Media.Playback](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-media-playback.html) 1, [Alexa.Media.Search](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-media-search.html) 1, [Alexa.ProactiveNotificationSource](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-proactivenotificationsource.html) 1, [Alexa.RTCSessionController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-rtcsessioncontroller.html) 1, [Alexa.RecordController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-recordcontroller.html) 3, [Alexa.RemoteVideoPlayer](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-remotevideoplayer.html) 1, [Alexa.SecurityPanelController.Alert](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-securitypanelcontroller-alert.html) 1, [Alexa.SeekController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-seekcontroller.html) 3, [Alexa.SmartVision.ObjectDetectionSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-smartvision-objectdetectionsensor.html) 1, [Alexa.SmartVision.SnapshotProvider](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-smartvision-snapshotprovider.html) 1, [Alexa.ThermostatController.Configuration](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-thermostatcontroller-configuration.html) 1, [Alexa.ThermostatController.HVAC.Components](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-thermostatcontroller-hvac-components.html) 1, [Alexa.UIController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-uicontroller.html) 1, [Alexa.UserPreference](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-userpreference.html) 1, [Alexa.VideoRecorder](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-videorecorder.html) 1. `registry.list()` returns the descriptors and `registry.get("Alexa.RangeController")` one of them. A tier 3 interface has no export of its own: declare it with `device.add(registry.get("Alexa.KeypadController"))`. ## Recipes Each file is a complete program: it declares one device, answers its directives and connects. The test suite starts every one of them against a broker on 127.0.0.1, discovers the device, sends it directives and checks the answers. | File | Device | Interfaces | Shows | |---|---|---|---| | [lamp.js](examples/lamp.js) | lamp | PowerController, BrightnessController | handlers, `device.state()`, `ctx.respond()` | | [color-lamp.js](examples/color-lamp.js) | colour lamp | + ColorController, ColorTemperatureController | a state that depends on the mode of the device | | [thermostat.js](examples/thermostat.js) | thermostat | ThermostatController, TemperatureSensor | one setpoint, three modes, temperature scales | | [blind.js](examples/blind.js) | roller blind | RangeController | an instance, friendly names, semantics for open and close | | [lock.js](examples/lock.js) | lock | LockController | `ctx.defer()` for an answer that takes longer than 7 seconds | | [sensor.js](examples/sensor.js) | contact sensor | ContactSensor | `device.changeReport()` | | [scene.js](examples/scene.js) | scene | SceneController | ActivationStarted and DeactivationStarted | | [doorbell.js](examples/doorbell.js) | doorbell | DoorbellEventSource | `device.raise()` | | [errors.js](examples/errors.js) | fan | PowerController, RangeController | `AlexaErrors`, `ctx.error()` | | [plug.mjs](examples/plug.mjs) | plug | PowerController | the library as an ES module | [examples/testing/lamp.test.js](examples/testing/lamp.test.js) tests a lamp without a broker, see [below](#testing-your-device-without-alexa). [examples/legacy](examples/legacy) has the nine examples of 1.5.2 on the 1.x API. ## The bridge contract What the library and Alex2MQTT agree on. You need it to debug with an MQTT client, not to use the library. | Topic | Direction | Message | |---|---|---| | `/discover` | to the bridge | a discovery request | | `/discover_r` | from the bridge | the endpoints, as one JSON array | | `//alexaDirective` | to the bridge | a directive: `{ header, endpoint, payload }` | | `//alexaResponce` | from the bridge | `Response`, `StateReport`, `ErrorResponse` or `DeferredResponse` | | `//deferredResponse` | from the bridge | the answer that follows a `DeferredResponse` | | `/changeReport` | from the bridge | a `ChangeReport` | | `/event` | from the bridge | an event of `device.raise()` | `alexaResponce` is how the service spells it. The bridge subscribes to `/discover` and `/+/alexaDirective`. **The answer window.** Alex2MQTT waits 7 seconds for the answer to a directive. A directive that nothing has answered after 6.5 seconds is answered by the bridge with `INTERNAL_ERROR`, and the bridge emits `unanswered` and `error`. `answerWithinMs` sets the time, and `0` turns this off. **Deferred answers.** When the device needs longer, call `ctx.defer()` within the window. The answer then goes to `deferredResponse`, and Alex2MQTT waits 10 seconds for it. The bridge sets no limit of its own after a `DeferredResponse`. [examples/lock.js](examples/lock.js) does this. **Change reports.** A `ChangeReport` goes to `/changeReport`, and Alex2MQTT posts it to Alexa with the token of your account. A report without a changed property is not published. **Proactive events.** An event of `device.raise()` goes to `/event`. The library refuses an event that is longer than 16000 bytes as JSON. **Duplicates.** A directive with the `messageId` of one that came for the endpoint in the last 60 seconds is dropped, and so is a discovery request with the `messageId` of an earlier one. A second answer for one `correlationToken` is refused: one `DeferredResponse` and one answer after it pass. **The connection.** mqtt.js reconnects by itself, 1 second after the connection is lost, and gives a connection attempt 10 seconds; `options.mqtt` changes both. The bridge emits `error` only when there is a listener, so a broker that is away does not end the process. `bridge.connected` says where you stand. **Endpoints of other bridges.** A directive for an endpoint the bridge does not have is left alone, because the endpoint may belong to another process on the same root topic. The bridge emits `unknownEndpoint`; `answerUnknownEndpoints: true` answers with `NO_SUCH_ENDPOINT`. ## Errors An `ErrorResponse` is an `AlexaError`: thrown in a handler, or sent with `ctx.error()`. `AlexaErrors` has a function for each of the 73 error types of Amazon's [table](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-errorresponse.html), named as the type in camel case. The function of a type whose payload has fields of its own takes them: ```js throw AlexaErrors.endpointUnreachable("The lamp does not answer"); throw AlexaErrors.valueOutOfRange("The fan has the speeds 1 to 3", { minimumValue: 1, maximumValue: 3 }); throw AlexaErrors.notSupportedInCurrentMode("The lamp shows a colour", "COLOR"); throw AlexaErrors.of("ENDPOINT_BUSY", "The blind is moving"); ``` The header of the `ErrorResponse` carries the namespace the type is documented under: `THERMOSTAT_IS_OFF` goes under `Alexa.ThermostatController`, `UNAUTHORIZED` under `Alexa.SecurityPanelController`, `OBSTACLE_DETECTED` under `Alexa.Safety`, and the 31 general types under `Alexa`. The library answers by itself in three cases: `INVALID_DIRECTIVE` for a directive without a handler, `INTERNAL_ERROR` for a handler that throws something else than an `AlexaError`, and `INTERNAL_ERROR` for a directive that nothing answered in time. [examples/errors.js](examples/errors.js) has a device that refuses. The other errors of the library are thrown where the mistake is made: `DeclarationError` by `addDevice()` and `add()`, `SchemaError` for a value that does not fit the interface, `MessageError` by `changeReport()` and `raise()`. ## Testing your device without Alexa A bridge with the `publisher` option publishes into it and not to the broker. With a `MemoryPublisher`, and `bridge.receive(topic, text)` in place of the broker, a test needs no connection: `receive()` resolves when the handlers are done. This is [examples/testing/lamp.test.js](examples/testing/lamp.test.js): ```js // A lamp tested without a broker and without Alexa. The bridge publishes into a MemoryPublisher, and // bridge.receive() gives it a message as the broker would. connect() is never called. // // node examples/testing/lamp.test.js // // "alex2node" is the installed package; inside this checkout it resolves to the built dist/. const { test } = require("node:test"); const assert = require("node:assert/strict"); const { Alex2MQTT, MemoryPublisher, PowerController } = require("alex2node"); // The device under test. In your program this is the module that declares your devices on a bridge. function declareLamp(bridge, lamp) { const device = bridge.addDevice({ endpointId: "desk-lamp", name: "Desk Lamp", categories: ["LIGHT"] }); const power = device.add(PowerController); device.state((s) => s.set(power, "powerState", lamp.on ? "ON" : "OFF").health("OK")); power.on("TurnOn", (ctx) => { if (lamp.broken) throw new Error("the lamp does not answer"); lamp.on = true; return ctx.respond(); }); return device; } // A bridge that publishes into sent, and what Alex2MQTT would send it function setup(lamp) { const sent = new MemoryPublisher(); const bridge = new Alex2MQTT("user", "password", "root", false, { publisher: sent }); declareLamp(bridge, lamp); let count = 0; const directive = (namespace, name, payload = {}) => { count += 1; return JSON.stringify({ header: { namespace, name, payloadVersion: "3", messageId: `message-${count}`, correlationToken: `token-${count}` }, endpoint: { endpointId: "desk-lamp" }, payload, }); }; return { sent, bridge, directive }; } test("discovery announces the lamp", async () => { const { sent, bridge } = setup({ on: false }); await bridge.receive("root/discover", "{}"); const [{ topic, message }] = sent.published; assert.equal(topic, "root/discover_r"); assert.equal(message[0].friendlyName, "Desk Lamp"); assert.deepEqual(message[0].capabilities.map((capability) => capability.interface), ["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"]); }); test("TurnOn switches the lamp and answers with its state", async () => { const lamp = { on: false }; const { sent, bridge, directive } = setup(lamp); await bridge.receive("root/desk-lamp/alexaDirective", directive("Alexa.PowerController", "TurnOn")); assert.equal(lamp.on, true); const [{ topic, message }] = sent.published; assert.equal(topic, "root/desk-lamp/alexaResponce"); assert.equal(message.event.header.name, "Response"); assert.equal(message.event.header.correlationToken, "token-1"); const power = message.context.properties.find((property) => property.name === "powerState"); assert.equal(power.value, "ON"); }); test("a handler that throws is answered with INTERNAL_ERROR", async () => { const { sent, bridge, directive } = setup({ on: false, broken: true }); const errors = []; bridge.on("error", (err) => errors.push(err)); await bridge.receive("root/desk-lamp/alexaDirective", directive("Alexa.PowerController", "TurnOn")); const [{ message }] = sent.published; assert.equal(message.event.header.name, "ErrorResponse"); assert.equal(message.event.payload.type, "INTERNAL_ERROR"); assert.equal(errors.length, 1); }); test("a directive without a handler is answered with INVALID_DIRECTIVE", async () => { const { sent, bridge, directive } = setup({ on: true }); await bridge.receive("root/desk-lamp/alexaDirective", directive("Alexa.PowerController", "TurnOff")); assert.equal(sent.published[0].message.event.payload.type, "INVALID_DIRECTIVE"); }); ``` To see the traffic of a running process, subscribe to `/#` with any MQTT client, or pass `log` in the options. ## Migrating from 1.x Every call of 1.5.2 is kept: `registerDevice()`, `addCapability()`, the `Event` and `ReportState` listeners, `getStatusMessage()`, `getErrorMessage()`, `getChangeReport()`, `sendSceneResponse()` and the enums. The nine examples of 1.5.2 run on 2.0 as they were. The topics are unchanged. What a 1.x program can observe of 2.0: | What | 1.5.2 | 2.0 | What to do | |---|---|---|---| | The `Alexa` interface in discovery | not listed | the last capability of every endpoint | a test that compares the capability list gets one more entry. `{ alexaInterface: false }` leaves it out | | Version of `Alexa.EndpointHealth` | `3.3` | `3.1` | nothing | | Speaker, StepSpeaker, EqualizerController, PlaybackController, PlaybackStateReporter, InputController, ChannelController, LockController, MotionSensor, PowerLevelController in discovery | version `1` or no property | the version and the properties of the interface | nothing | | `addSupportedModes(["a", "b"])` | announced as strings | announced as `{ value }` objects, with a warning | pass `{ value, modeResources }` | | `new ActionMapping(actions, name, "")` | the string was announced | a JSON string is parsed, with a warning; another string throws | pass the object | | A directive or a discovery request that arrives twice | handled twice | the second is dropped | nothing | | A second answer for one `correlationToken` | published | refused, `send()` resolves `""` | answer once | | A directive nothing answers | no answer | `INTERNAL_ERROR` after 6.5 s | answer, or defer; `answerWithinMs: 0` turns it off | | A listener that throws | the exception left the library | `INTERNAL_ERROR`, and the `error` event | nothing | | A directive for a device without a listener or handler | no answer | `INVALID_DIRECTIVE` | nothing | | `getChangeReport()` without a changed property | published | not published, `send()` resolves `""` | send no report when nothing changed | | Namespace of an `ErrorResponse` | always `Alexa` | the namespace of the error type | `setErrorMessage(type, message, extra, { namespace })` names another | | `getErrorMessage(token).send()` without `setErrorMessage()` | an empty payload | `INTERNAL_ERROR` | call `setErrorMessage()` | | `DeferredResponse` | `"context": null` | no `context` | nothing | | Subscriptions | `/#` | `/discover`, `/+/alexaDirective` | nothing | | `registerDevice()` before `connect()` | threw | returns the device | nothing | | `new Device(client, ...)`, `device.setMqttClient(client)` | used the client | the client is ignored, with a warning once | remove it. Both go in 3.0 | [docs/wire-changes.md](docs/wire-changes.md) has every row, with the test that pins it. The calls of the two APIs, side by side: | 1.x | 2.0 | |---|---| | `bridge.registerDevice(name, endpointId, category)` | `bridge.addDevice({ endpointId, name, categories })` | | `device.addCapability(AlexaInterfaceType.POWER_CONTROLLER)` | `const power = device.add(PowerController)` | | `device.on("Event", ...)` with `getStatusMessage(token, true)...send()` | `power.on("TurnOn", (ctx) => ctx.respond())` | | `device.on("ReportState", ...)` with `getStatusMessage(token)...send()` | `device.state((s) => s.set(power, "powerState", "ON"))` | | `getErrorMessage(token).setErrorMessage(type, message).send()` | `ctx.error(type, message)`, or `throw AlexaErrors...` | | `getChangeReport(cause).add...Prop(value).send()` | `device.changeReport(cause, (s) => s.set(...))` | A device of `addDevice()` is announced with `Alexa.EndpointHealth`; one of `registerDevice()` is announced as 1.x announced it. `send()` of 1.x resolves `""` where the 2.0 calls resolve `{ ok: false, error }` or throw. `PowerController` is both the 1.x enum (`PowerController.ON`) and the interface for `device.add()`.
The light of the 1.x readme: examples/legacy/ExamplePowerController.js ```js // Import the library ("alex2node" is the installed package; inside this checkout it resolves to the built dist/ via package.json "exports") const { Alex2MQTT, AlexaInterfaceType, PowerController, EndpointHealth } = require("alex2node"); require("dotenv").config(); // Load environment variables from .env file // Simulated in-memory device state (used for example/demo purposes) let outputState = PowerController.OFF; // Initialize the power state to OFF // 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 (bedroom light) const deviceName = "Bedroom Light"; // Register the device with a unique endpoint ID (unique per root topic, e.g. "bedroom-light-1") const bedroomLight = alex2NodeClient.registerDevice(deviceName, "bedroom-light-1"); // Add the PowerController capability (for turning on/off the device) bedroomLight.addCapability(AlexaInterfaceType.POWER_CONTROLLER); // Log the device name to verify registration console.log(bedroomLight.getName()); /** * Handle Alexa's ReportState directive. * This occurs when Alexa queries the current state of the device (e.g., during routines or device status checks). */ bedroomLight.on("ReportState", (payload) => { console.log("ReportState received!", payload); const { correlationToken } = payload.header; const status = bedroomLight.getStatusMessage(correlationToken); status .addHealthProp(EndpointHealth.OK) // Device is healthy .addPowerControllerProp(outputState); // Report the current power state (ON/OFF) status.send(); // Send the state report back to Alexa }); /** * Handle incoming control directives (e.g., TurnOn, TurnOff). * These directives come from Alexa when a user issues a command. */ bedroomLight.on("Event", (directive, interfaceType) => { console.log("Event received", { directive, interfaceType }); // Ensure the interfaceType is either PowerController or other valid interfaces if (interfaceType === AlexaInterfaceType.POWER_CONTROLLER) { const name = directive.header.name; const token = directive.header.correlationToken; // Update the internal state based on the command (TurnOn / TurnOff) if (name === "TurnOn") { outputState = PowerController.ON; console.log("Turning ON the Bedroom Light"); } else if (name === "TurnOff") { outputState = PowerController.OFF; console.log("Turning OFF the Bedroom Light"); } // Respond to the directive with the updated device status const status = bedroomLight.getStatusMessage(token, true); status .addHealthProp(EndpointHealth.OK) .addPowerControllerProp(outputState) .send(); } }); ```
## Limits - **What was run against Alexa.** On 2026-09-28 alex2node 1.5.2 was driven from a real Alexa account through the public Alex2MQTT service: Power, Brightness, Color, ColorTemperature, Percentage, PowerLevel, Thermostat, Range, Mode, Toggle, Lock and Scene, deferred responses, `ErrorResponse`, `ReportState`, and change reports for contact, motion, lock, temperature and power. Voice commands were not tested yet. The 2.0 API was not run against an Alexa account yet: the test suite runs it against a broker on 127.0.0.1. - **Proactive events.** `device.raise()` publishes to `/event`. Whether a `DoorbellPress` reaches Alexa depends on the Alex2MQTT service relaying that topic, which was not tested. - **Alexa.WakeOnLANController** needs three messages for one `TurnOn`. The descriptor is there; the exchange was not run through Alex2MQTT. - **Tier 3 interfaces** are named, not described: no check of the declaration, the state or the payload. - **One message for discovery.** The bridge answers a discovery request with all its endpoints in one message. - **The 1.x helpers `addThermostatControllerProp()` and `addTemperatureSensorProp()`** convert Fahrenheit to Celsius without rounding: 70 °F is sent as `21.11111111111111`, as in 1.5.2. - **The constructor** takes its arguments by position. There is no form that takes one object. - **No skill of your own.** The library does not talk to Alexa: it needs the Alex2MQTT service, or a broker and a backend that speak its topics. ## Development ```sh npm install # builds dist/ npm run build # after a change in src/; dist/ is committed with it npm test # against a broker in the test process npm run check # the type checks npm run docs # writes the table of interfaces and the example files into this readme ``` ## Changelog [CHANGELOG.md](CHANGELOG.md). ## License MIT, see [LICENSE](LICENSE).