The interface registry, generated and checked discovery, typed dispatch with
automatic error answers, message builders, proactive events and typed
helpers for every error type; the 1.x API is kept and the examples of 1.5.2
run unchanged. docs/wire-changes.md lists what a 1.x caller can observe.

On 2026-09-28 a real Alexa account drove 33 test devices on this API through
the public Alex2MQTT service: Alexa accepted the discovery of all of them,
50 of 58 cases passed, 4 had no action in the control call and 4 failed on
what the check could see, none on an answer of the library. The readme table
names the interfaces that run covered. Voice commands were not tested yet.

307 tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 22:55:23 +00:00
parent e2ed755111
commit dd071548bc
8 changed files with 46 additions and 36 deletions

View file

@ -3,7 +3,7 @@
The format is that of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the versions follow
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.0.0 - unreleased
## [2.0.0] - 2026-09-28
Every call of 1.5.2 is kept. What a 1.x program can observe is under "Changed", and
[docs/wire-changes.md](docs/wire-changes.md) has each row with the test that pins it.

View file

@ -43,8 +43,8 @@ node examples/testing/lamp.test.js
## 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.
it directives and checks the answers. These files were not run against an Alexa account; test devices that make the
same calls were, on 2026-09-28, the doorbell's `raise()` among them (the readme has the list).
## 1.x

View file

@ -1,8 +1,8 @@
// 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.
// The event is published to <root>/event, which the Alex2MQTT service passes on to Alexa. Alexa wants 30 seconds
// between two presses of one doorbell.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/doorbell.js
//

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "alex2node",
"version": "1.5.2",
"version": "2.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "alex2node",
"version": "1.5.2",
"version": "2.0.0",
"license": "MIT",
"dependencies": {
"mqtt": "^5.10.3"

View file

@ -1,6 +1,6 @@
{
"name": "alex2node",
"version": "1.5.2",
"version": "2.0.0",
"description": "Alexa smart home devices in Node.js over MQTT, through the Alex2MQTT service: declare a device, answer its directives, report its state.",
"type": "commonjs",
"main": "./dist/cjs/index.js",

View file

@ -218,18 +218,18 @@ announced with its version, nothing about it is checked, and a handler gets the
| 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.BrightnessController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-brightnesscontroller.html) | 3 | `brightness` | `SetBrightness`, `AdjustBrightness` | - | 1 | directives, change report |
| [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.DoorbellEventSource](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-doorbelleventsource.html) | 3 | - | - | `DoorbellPress` | 2 | event |
| [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.InventoryLevelSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-inventorylevelsensor.html) (instances) | 3 | `level` | - | - | 2 | change report |
| [Alexa.LockController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-lockcontroller.html) | 3 | `lockState` | `Lock`, `Unlock` | - | 1 | directives with a deferred answer, 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 |
@ -237,22 +237,22 @@ announced with its version, nothing about it is checked, and a handler gets the
| [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.RangeController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-rangecontroller.html) (instances) | 3 | `rangeValue` | `SetRangeValue`, `AdjustRangeValue` | - | 1 | directives, change report |
| [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.SecurityPanelController](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-securitypanelcontroller.html) | 3 | `armState`, `burglaryAlarm`, `fireAlarm`, `carbonMonoxideAlarm`, `waterAlarm` | `Arm`, `Disarm` | - | 2 | directives, change report |
| [Alexa.SimpleEventSource](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-simpleeventsource.html) (instances) | 1.0 | - | - | `Event` | 2 | event |
| [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.TemperatureSensor](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-temperaturesensor.html) | 3 | `temperature` | - | - | 1 | state 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.ThermostatController.Schedule](https://developer.amazon.com/docs/alexaplus/device-apis/alexa-thermostatcontroller-schedule.html) | 3.2 | `adaptiveRecoveryEnabled`, `scheduleEnabled` | `SetWeeklySchedule`, `SetScheduleState`, `SetAdaptiveRecovery` | - | 2 | state report |
| [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.
alex2node 2.0.0; "-" 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.
<!-- /capabilities -->
@ -586,13 +586,16 @@ bedroomLight.on("Event", (directive, interfaceType) => {
## 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 `<root>/event`. Whether a `DoorbellPress` reaches Alexa
depends on the Alex2MQTT service relaying that topic, which was not tested.
- **What was run against Alexa.** On 2026-09-28 a real Alexa account drove 33 test devices on the 2.0 API through
the public Alex2MQTT service; the table above has the interfaces. Alexa accepted the discovery of all 33. Covered
beside the directives: deferred answers, `ErrorResponse`, `ReportState`, change reports, a `DoorbellPress` and a
button event through `device.raise()`. The directives came from the control call of the Alexa app, which has no
way to say "raise", "lower" or "warmer": voice commands, and with them the `Adjust...` directives and the
semantics of a blind, were not tested yet.
- **Not covered by that run.** `Alexa.Speaker`, `Alexa.ChannelController` and the other interfaces of a television
or a speaker were discovered, but the control call has no action for them. `Alexa.HumiditySensor` was
discovered and answered `ReportState`; Alexa's own state call does not return a humidity, so nothing shows
whether Alexa took the value.
- **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.

View file

@ -14,26 +14,31 @@ const README = path.join(ROOT, "readme.md");
const BEGIN = "<!-- capabilities: written by scripts/capability-table.js, npm run docs -->";
const END = "<!-- /capabilities -->";
// What was driven from a real Alexa account through the public Alex2MQTT service, and when. The runs used
// alex2node 1.5.2; a row is added here when a run is recorded, not when a descriptor is written.
// What was driven from a real Alexa account through the public Alex2MQTT service, and when: 33 test devices on
// the 2.0 API, 60 cases. A row is added here when a run is recorded, not when a descriptor is written.
const DRIVEN_ON = "2026-09-28";
const DRIVEN_WITH = "alex2node 1.5.2";
const DRIVEN_WITH = "alex2node 2.0.0";
const DRIVEN = {
"Alexa": "ReportState",
"Alexa.BrightnessController": "directives",
"Alexa.BrightnessController": "directives, change report",
"Alexa.ColorController": "directives",
"Alexa.ColorTemperatureController": "directives",
"Alexa.ContactSensor": "change report",
"Alexa.LockController": "directives, change report",
"Alexa.DoorbellEventSource": "event",
"Alexa.InventoryLevelSensor": "change report",
"Alexa.LockController": "directives with a deferred answer, change report",
"Alexa.ModeController": "directives",
"Alexa.MotionSensor": "change report",
"Alexa.PercentageController": "directives",
"Alexa.PowerController": "directives, change report",
"Alexa.PowerLevelController": "directives",
"Alexa.RangeController": "directives",
"Alexa.RangeController": "directives, change report",
"Alexa.SceneController": "directives",
"Alexa.TemperatureSensor": "change report",
"Alexa.SecurityPanelController": "directives, change report",
"Alexa.SimpleEventSource": "event",
"Alexa.TemperatureSensor": "state report",
"Alexa.ThermostatController": "directives",
"Alexa.ThermostatController.Schedule": "state report",
"Alexa.ToggleController": "directives",
};

View file

@ -32,12 +32,14 @@ test("the table has a row for every described interface, and a line for the othe
});
test("an interface is said to be driven through Alexa only when a run is recorded for it", () => {
// The runs of 2026-09-28, with alex2node 1.5.2. A name is added here with the record of its run.
// The run of 2026-09-28 with 33 test devices on the 2.0 API. A name is added here with the record of its run.
assert.deepEqual(Object.keys(DRIVEN).sort(), [
"Alexa", "Alexa.BrightnessController", "Alexa.ColorController", "Alexa.ColorTemperatureController",
"Alexa.ContactSensor", "Alexa.LockController", "Alexa.ModeController", "Alexa.MotionSensor",
"Alexa.PercentageController", "Alexa.PowerController", "Alexa.PowerLevelController", "Alexa.RangeController",
"Alexa.SceneController", "Alexa.TemperatureSensor", "Alexa.ThermostatController", "Alexa.ToggleController",
"Alexa.ContactSensor", "Alexa.DoorbellEventSource", "Alexa.InventoryLevelSensor", "Alexa.LockController",
"Alexa.ModeController", "Alexa.MotionSensor", "Alexa.PercentageController", "Alexa.PowerController",
"Alexa.PowerLevelController", "Alexa.RangeController", "Alexa.SceneController",
"Alexa.SecurityPanelController", "Alexa.SimpleEventSource", "Alexa.TemperatureSensor",
"Alexa.ThermostatController", "Alexa.ThermostatController.Schedule", "Alexa.ToggleController",
]);
const rows = table().split("\n").filter((line) => line.startsWith("| [Alexa"));
for (const line of rows) {