Six descriptors written from their pages replace the last stubs of tiers 1 and 2. A scene answers Activate and
Deactivate through ctx.respond() with ActivationStarted and DeactivationStarted, the time and the cause filled
in. device.raise(descriptor, name, payload) publishes DoorbellPress and the Event of a button on <root>/event
with the endpoint and a new messageId; it throws a MessageError for an interface or instance the device did
not declare, an event that answers a directive, a payload that does not fit and a message over 16000 bytes.
TurnOn of a device with WakeOnLANController is deferred without the warning.
On the wire: a doorbell has no properties object and proactivelyReported on the capability; SimpleEventSource
is version 1.0, InventoryLevelSensor and WakeOnLANController version 3 (1.5.2: 1). A scene declared without
options is announced as before. Alex2MQTT has no topic yet for the WakeUp event.
24 examples of the six pages are saved as fixtures. 267 tests pass, 241 before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Seven descriptors written from their pages replace seven stubs. All are version 3; 1.5.2 announced Speaker,
StepSpeaker, EqualizerController and PlaybackStateReporter as version 1, and all but ChannelController without
a property. StepSpeaker has no properties object in discovery and PlaybackController one without fields, next
to supportedOperations; a directive for an operation the device did not list is answered with
INVALID_DIRECTIVE. The equalizer declares bands, modes or both under "configurations", a TV its inputs from
the 61 names of the page.
Not described: the lineup of ChannelController, whose discovery example the page folds away. The discovery
examples of InputController spell one key "friendNames"; the fixtures have friendlyNames, as its table does.
62 examples of the six pages are saved as fixtures. 241 tests pass, 215 before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Alexa.SecurityPanelController is described from its page: version 3 (the stub of 1.5.2 said 1), armState and the
four alarms, Arm and Disarm. ctx.respond() answers Arm with Arm.Response under the namespace of the interface,
its payload checked (exitDelayInSeconds 0 to 255, bypassedEndpoints), and Disarm with a Response. A Disarm
carries the PIN the user said when the panel is declared with supportedAuthorizationTypes; the handler checks
it and answers UNAUTHORIZED, which goes under Alexa.SecurityPanelController like the five other error types.
A panel declared without options lists armState, where 1.5.2 listed no property; alarms are declared.
17 examples of the two pages are saved as fixtures. 215 tests pass, 207 before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The thermostat is described from its page: one, two or three setpoints with holdUntil, AdjustTargetTemperature,
SetThermostatMode with the six modes (EM_HEAT is new), ResumeSchedule, adaptiveRecoveryStatus, and its seven error
types under Alexa.ThermostatController. A declaration gives supportedModes, supportsScheduling and the properties
the device has; without them discovery is what 1.5.2 sent: HEAT, COOL, AUTO, OFF and the three setpoints with
thermostatMode. supportedModes without HEAT or COOL is refused where it is declared.
ThermostatController.Schedule replaces its stub. Wire change for a 1.x declaration of it: version 3.2 (was 1) and
scheduleEnabled listed (was nothing). SetAdaptiveRecovery is taken only with supportsAdaptiveRecovery.
24 examples of three pages are saved as fixtures; the page folds the SetWeeklySchedule example away, its payload
is tested from the tables. 207 tests pass, 191 before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Eight descriptors written from their pages, each with its version, the value of its property and the payload of
every directive. Seven replace a stub; Alexa.HumiditySensor is new, as AlexaInterfaceType.HUMIDITY_SENSOR.
LockController is deferrable: defer() on a lock logs no warning.
A 1.x declaration of LockController, MotionSensor, PercentageController or PowerLevelController now lists its
property in discovery, where 1.5.2 listed none: the zoo's lock, motion sensor and heater come out as the JSON
Alexa accepted. A payload that does not fit (hue 361, 900 K) is answered with INVALID_VALUE.
48 examples of the eight pages are saved as fixtures. 191 tests pass, 168 before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/dispatcher.ts routes a directive to capability.on(name | "*"), device.onDirective() or
device.onReportState(), with the payload checked by the descriptor of the interface. The
DirectiveContext answers with respond/report/defer/error; respond() and report() start from
device.state(). A handler or 1.x listener that throws or rejects is answered with INTERNAL_ERROR
(an AlexaError with itself) and reported through "error", never as an unhandled rejection.
Undeclared interface, unknown directive, AdjustMode on an unordered mode and a missing handler
get INVALID_DIRECTIVE, a bad payload INVALID_VALUE; a device with an "Event" or "ReportState"
listener keeps the answer to itself. Every answer passes the dispatcher: the first one per
correlationToken is published, a second is refused. No answer within answerWithinMs (6500,
0 = off, unref'd timer) sends INTERNAL_ERROR and emits "unanswered". A messageId that arrived
in the last 60 s is dropped. New: "unknownEndpoint", options answerUnknownEndpoints, publisher,
timers, and bridge.receive() to run a bridge without a broker. 168 tests pass (18 new).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/transport.ts has the Publisher interface, MqttPublisher (the client the bridge has at the time) and
MemoryPublisher (tests without a broker); src/topics.ts names the four topics the library publishes to.
Device, AlexaStatusMessage, AlexaErrorResponse and sendSceneResponse shared three copies of the
publish-and-report code: they now call one send() that resolves the topic or "" and never rejects.
registerDevice() and addDevice() work before connect(); a send() without a connection resolves "" and the
"error" event says to call connect(). unregisterDevice() and clearDevices() take the publisher from the device.
Device.setMqttClient() is gone, the first constructor argument of Device is ignored, and the message classes
take a Publisher where they took the client. Tests: 108 -> 113.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/messages builds Response, StateReport, DeferredResponse, ErrorResponse, ChangeReport and the scene, doorbell
and simple events from plain values; messageId and times are parameters, so a test compares whole objects.
StateBuilder collects properties checked by the descriptors, AlexaError and AlexaErrors carry the payload fields
of an error type. AlexaStatusMessage and AlexaErrorResponse move to src/compat and are written on the builders.
On the wire, against 1.5.2: a DeferredResponse has no context key, a ChangeReport has no correlationToken, the
namespace of an ErrorResponse follows its type, and a ChangeReport without a changed property is not published:
send() resolves "" and the bridge reports an error that names the endpoint.
143 tests pass (108 before).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The three generic controllers are described in full and leave the stub table (62 stubs remain). A declaration
takes an instance name, friendly names from text() and asset(), and the options of the interface: range, unit and
presets for a range; supportedModes and ordered for a mode; semantics() for all three, with actions mapped to
directives and states mapped to a value or a range of values.
Refused at device.add(), each with the endpoint, interface and instance in the message: no instance name or no
friendly name; an instance declared twice; a first friendly name or a phrase used by another capability of the
endpoint; a reserved word, an asset id that is not in the catalog; range.max not above range.min; a preset off the
precision grid or outside the range; fewer than two modes or a mode listed twice; a mapping to a directive the
interface does not have, to a payload that does not fit, to a mode that is not listed, to AdjustMode on modes
that are not ordered; SetEcoOn, SetEcoOff, EcoOn and EcoOff on anything but a toggle.
For 1.x callers: Alexa.RangeController lists rangeValue (1.5.2 sent supported: []); addSupportedModes() announces
a mode given as a string as { value } and notes it, and takes { ordered: false } as a second argument, the default
stays true as 1.5.2 sent it (device.add() defaults to false). Declared the 1.x way, the zoo fan and curtain give
the same JSON as before.
Tests: the zoo blind, whose range, unit and semantics were written by hand into the JSON of 1.5.2, is declared
with device.add() and equals that JSON plus the Alexa capability; five capability objects of Amazon's examples
are reproduced the same way. 27 examples of four pages added. npm test: 108 pass (was 85) in 10.5-11.3 s, also on
Node 18.20.8 and 20.20.2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A capability is a descriptor plus what the endpoint declares (device/Capability.ts), and its discovery object is
generated from the two. The new API is bridge.addDevice({ endpointId, name, categories, ... }) and
device.add(PowerController, options): both throw a DeclarationError that names the endpoint, the interface and
the instance (device/validate.ts), and leave the bridge and the device as they were. AlexaInterface is the same
Capability with the 1.x methods on it; it, ActionMapping and the enums moved to src/compat/, Device to src/device/.
What a 1.x caller can observe:
- every endpoint ends with { type: "AlexaInterface", interface: "Alexa", version: "3" } (alexa-interface.html);
new Alex2MQTT(..., { alexaInterface: false }) leaves it out
- the fields of a capability object come in the order of Amazon's examples; their content is unchanged
- addCapability() with a name that is not an interface throws (1.5.2 announced it with the version "UNKNOWN")
- ActionMapping takes the payload as an object; a JSON string is parsed (1.5.2 sent the string), any other throws
- what Alexa would reject in a 1.x declaration is not refused: device.check() lists it and the bridge logs each
line once, as "warning: ..." through the log hook, when it answers a discovery
- a device whose JSON cannot be built is left out of the answer and reported as an error event
- PowerController and EndpointHealth are the descriptors and keep ON/OFF and OK/UNREACHABLE; PowerState is new
Tests: six zoo devices declared the 1.x way give the JSON that Alexa accepted from 1.5.2 on 2026-09-28, plus the
Alexa capability. npm test: 85 pass (was 57) in 10-12 s, also on Node 18.20.8 and 20.20.2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/registry/ holds what the library knows about an interface: namespace, version, the page it was read from,
properties with their value schemas and directives with their payload schemas. Five interfaces are described
(Alexa, PowerController, BrightnessController, TemperatureSensor, EndpointHealth); the other 65 names of
AlexaInterfaceType are stubs with the version and property names of 1.5.2. schema.ts is the run-time check behind
it (241 lines, no new dependency), catalog.ts the vocabularies of the pages: 103 assets (23 units), 6 actions,
9 states, 56 display categories, 22 reserved words, 73 error types under 11 namespaces.
AlexaInterface.getVersion() and getProps() read the registry; the two switch statements are gone (-167 lines).
On the wire: Alexa.EndpointHealth is announced at 3.1 (was 3.3; the page is titled 3.1 and no page mentions 3.3),
and TimeHoldController and Camera.LiveViewController at 3 and 1.7 (1.5.2 sent the string "UNKNOWN").
DisplayCategory gains VACUUM. New exports: registry, DeclarationError, SchemaError, Assets, Units, Actions,
States, DisplayCategories and the descriptor types.
Tests: 20 JSON examples of the five pages under test/fixtures/alexa-docs; every directive payload and property
value in them parses with its descriptor. npm test: 57 pass (was 30) in 10.8 s, also on Node 18.20.8 and 20.20.2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An ES module TypeScript consumer was given the CommonJS declarations in dist/types. With those,
`import alex2node from "alex2node"` compiled (module Node16) and then failed when Node loaded it: "The requested
module 'alex2node' does not provide an export named 'default'". The ES module build has named exports only.
tsconfig.esm.json now emits declarations next to dist/esm/*.js, under that directory's {"type": "module"}, and
"exports" selects per condition: import -> dist/esm/index.d.ts, require -> dist/types/index.d.ts. The default
import is now refused with TS1192; named imports are unchanged. "main", "module" and "types" are as before.
test/fixtures/types.mts holds the default import under @ts-expect-error, and a new test requires "types" before
"default" and a declaration for every built file. dist/: 25 files, 132,291 B -> 33 files, 155,647 B; the npm
tarball 25,125 B -> 25,837 B. npm test: 25 pass in 5.6 s.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>