registry: describe an Alexa interface as data

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>
This commit is contained in:
David 2026-09-28 15:18:22 +00:00
parent 128ca35c2a
commit aa0ffd64ea
93 changed files with 4058 additions and 490 deletions

View file

@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AlexaInterface = exports.AlexaInterfaceType = void 0;
const index_js_1 = require("./registry/index.js");
var AlexaInterfaceType;
(function (AlexaInterfaceType) {
AlexaInterfaceType["APPLICATION_STATE_REPORTER"] = "Alexa.ApplicationStateReporter";
@ -103,170 +104,13 @@ class AlexaInterface {
getTypeString() {
return this.type;
}
/** The version of the interface, from its descriptor. "UNKNOWN" for a name the registry does not have. */
getVersion() {
switch (this.type) {
case AlexaInterfaceType.APPLICATION_STATE_REPORTER:
case AlexaInterfaceType.AUDIO_PLAY_QUEUE:
case AlexaInterfaceType.AUTHORIZATION_CONTROLLER:
case AlexaInterfaceType.AUTOMATION_MANAGEMENT:
case AlexaInterfaceType.AUTOMOTIVE_VEHICLE_DATA:
case AlexaInterfaceType.COMMISSIONABLE:
case AlexaInterfaceType.CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER:
case AlexaInterfaceType.COOKING:
case AlexaInterfaceType.COOKING_FOOD_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType.COOKING_FOOD_TEMPERATURE_SENSOR:
case AlexaInterfaceType.COOKING_PRESET_CONTROLLER:
case AlexaInterfaceType.COOKING_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType.COOKING_TEMPERATURE_SENSOR:
case AlexaInterfaceType.COOKING_TIME_CONTROLLER:
case AlexaInterfaceType.DATA_CONTROLLER:
case AlexaInterfaceType.DEVICE_USAGE_ESTIMATION:
case AlexaInterfaceType.DEVICE_USAGE_METER:
case AlexaInterfaceType.EQUALIZER_CONTROLLER:
case AlexaInterfaceType.INVENTORY_LEVEL_SENSOR:
case AlexaInterfaceType.INVENTORY_LEVEL_USAGE_SENSOR:
case AlexaInterfaceType.INVENTORY_USAGE_SENSOR:
case AlexaInterfaceType.KEYPAD_CONTROLLER:
case AlexaInterfaceType.MEDIA_PLAYBACK:
case AlexaInterfaceType.MEDIA_PLAY_QUEUE:
case AlexaInterfaceType.MEDIA_SEARCH:
case AlexaInterfaceType.PLAYBACK_STATE_REPORTER:
case AlexaInterfaceType.PROACTIVE_NOTIFICATION_SOURCE:
case AlexaInterfaceType.REMOTE_VIDEO_PLAYER:
case AlexaInterfaceType.RTC_SESSION_CONTROLLER:
case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER:
case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER_ALERT:
case AlexaInterfaceType.SIMPLE_EVENT_SOURCE:
case AlexaInterfaceType.SMART_VISION_OBJECT_DETECTION_SENSOR:
case AlexaInterfaceType.SMART_VISION_SNAPSHOT_PROVIDER:
case AlexaInterfaceType.SPEAKER:
case AlexaInterfaceType.STEP_SPEAKER:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_CONFIGURATION:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_HVAC_COMPONENTS:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_SCHEDULE:
case AlexaInterfaceType.UI_CONTROLLER:
case AlexaInterfaceType.USER_PREFERENCE:
case AlexaInterfaceType.VIDEO_RECORDER:
case AlexaInterfaceType.WAKE_ON_LAN_CONTROLLER:
return "1";
case AlexaInterfaceType.BRIGHTNESS_CONTROLLER:
case AlexaInterfaceType.CAMERA_STREAM_CONTROLLER:
case AlexaInterfaceType.CHANNEL_CONTROLLER:
case AlexaInterfaceType.COLOR_CONTROLLER:
case AlexaInterfaceType.COLOR_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType.CONTACT_SENSOR:
case AlexaInterfaceType.DOORBELL_EVENT_SOURCE:
case AlexaInterfaceType.INPUT_CONTROLLER:
case AlexaInterfaceType.LOCK_CONTROLLER:
case AlexaInterfaceType.MODE_CONTROLLER:
case AlexaInterfaceType.MOTION_SENSOR:
case AlexaInterfaceType.PERCENTAGE_CONTROLLER:
case AlexaInterfaceType.PLAYBACK_CONTROLLER:
case AlexaInterfaceType.POWER_CONTROLLER:
case AlexaInterfaceType.POWER_LEVEL_CONTROLLER:
case AlexaInterfaceType.RANGE_CONTROLLER:
case AlexaInterfaceType.RECORD_CONTROLLER:
case AlexaInterfaceType.SCENE_CONTROLLER:
case AlexaInterfaceType.SEEK_CONTROLLER:
case AlexaInterfaceType.TEMPERATURE_SENSOR:
case AlexaInterfaceType.TOGGLE_CONTROLLER:
return "3";
case AlexaInterfaceType.LAUNCHER:
return "1.1";
case AlexaInterfaceType.THERMOSTAT_CONTROLLER:
return "3.2";
case AlexaInterfaceType.ENDPOINT_HEALTH:
return "3.3";
default:
return "UNKNOWN";
}
return index_js_1.registry.has(this.type) ? index_js_1.registry.get(this.type).version : "UNKNOWN";
}
/** The names of the properties the interface reports, from its descriptor. */
getProps() {
switch (this.type) {
case AlexaInterfaceType.AUTOMATION_MANAGEMENT:
return ["automationStatuses"];
case AlexaInterfaceType.BRIGHTNESS_CONTROLLER:
return ["brightness"];
case AlexaInterfaceType.CHANNEL_CONTROLLER:
return ["channel"];
case AlexaInterfaceType.COLOR_CONTROLLER:
return ["color"];
case AlexaInterfaceType.COLOR_TEMPERATURE_CONTROLLER:
return ["colorTemperatureInKelvin"];
case AlexaInterfaceType.CONTACT_SENSOR:
return ["detectionState"];
case AlexaInterfaceType.POWER_CONTROLLER:
return ["powerState"];
case AlexaInterfaceType.ENDPOINT_HEALTH:
return ["connectivity"];
case AlexaInterfaceType.TOGGLE_CONTROLLER:
return ["toggleState"];
case AlexaInterfaceType.MODE_CONTROLLER:
return ["mode"];
case AlexaInterfaceType.TEMPERATURE_SENSOR:
return ["temperature"];
case AlexaInterfaceType.THERMOSTAT_CONTROLLER:
return [
"targetSetpoint",
"lowerSetpoint",
"upperSetpoint",
"thermostatMode",
];
case AlexaInterfaceType.APPLICATION_STATE_REPORTER:
case AlexaInterfaceType.AUDIO_PLAY_QUEUE:
case AlexaInterfaceType.AUTHORIZATION_CONTROLLER:
case AlexaInterfaceType.AUTOMOTIVE_VEHICLE_DATA:
case AlexaInterfaceType.CAMERA_LIVE_VIEW_CONTROLLER:
case AlexaInterfaceType.CAMERA_STREAM_CONTROLLER:
case AlexaInterfaceType.COMMISSIONABLE:
case AlexaInterfaceType.CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER:
case AlexaInterfaceType.COOKING:
case AlexaInterfaceType.DATA_CONTROLLER:
case AlexaInterfaceType.DEVICE_USAGE_ESTIMATION:
case AlexaInterfaceType.DEVICE_USAGE_METER:
case AlexaInterfaceType.DOORBELL_EVENT_SOURCE:
case AlexaInterfaceType.EQUALIZER_CONTROLLER:
case AlexaInterfaceType.INPUT_CONTROLLER:
case AlexaInterfaceType.INVENTORY_LEVEL_SENSOR:
case AlexaInterfaceType.INVENTORY_LEVEL_USAGE_SENSOR:
case AlexaInterfaceType.INVENTORY_USAGE_SENSOR:
case AlexaInterfaceType.KEYPAD_CONTROLLER:
case AlexaInterfaceType.LAUNCHER:
case AlexaInterfaceType.LOCK_CONTROLLER:
case AlexaInterfaceType.MEDIA_PLAYBACK:
case AlexaInterfaceType.MEDIA_SEARCH:
case AlexaInterfaceType.MOTION_SENSOR:
case AlexaInterfaceType.PERCENTAGE_CONTROLLER:
case AlexaInterfaceType.PLAYBACK_CONTROLLER:
case AlexaInterfaceType.PLAYBACK_STATE_REPORTER:
case AlexaInterfaceType.PROACTIVE_NOTIFICATION_SOURCE:
case AlexaInterfaceType.RANGE_CONTROLLER:
case AlexaInterfaceType.RECORD_CONTROLLER:
case AlexaInterfaceType.REMOTE_VIDEO_PLAYER:
case AlexaInterfaceType.RTC_SESSION_CONTROLLER:
case AlexaInterfaceType.SCENE_CONTROLLER:
return []; // scenes have no reportable properties: Activate/Deactivate answer with ActivationStarted (Device.sendSceneResponse)
case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER:
case AlexaInterfaceType.SEEK_CONTROLLER:
case AlexaInterfaceType.SIMPLE_EVENT_SOURCE:
case AlexaInterfaceType.SMART_VISION_OBJECT_DETECTION_SENSOR:
case AlexaInterfaceType.SMART_VISION_SNAPSHOT_PROVIDER:
case AlexaInterfaceType.SPEAKER:
case AlexaInterfaceType.STEP_SPEAKER:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_CONFIGURATION:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_HVAC_COMPONENTS:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_SCHEDULE:
case AlexaInterfaceType.TIME_HOLD_CONTROLLER:
case AlexaInterfaceType.UI_CONTROLLER:
case AlexaInterfaceType.USER_PREFERENCE:
case AlexaInterfaceType.VIDEO_RECORDER:
case AlexaInterfaceType.WAKE_ON_LAN_CONTROLLER:
// no property list known for these: discovery lists the capability with an empty "supported" (the bridge notes
// it through its log hook; 1.5.1 wrote "UNSUPORTED INTERFACE TYPE" to stderr on every discovery)
return [];
default:
return [];
}
return index_js_1.registry.has(this.type) ? Object.keys(index_js_1.registry.get(this.type).properties) : [];
}
getJSON() {
const doc = {

View file

@ -55,6 +55,8 @@ var DisplayCategory;
DisplayCategory["THERMOSTAT"] = "THERMOSTAT";
DisplayCategory["TV"] = "TV";
DisplayCategory["VACUUM_CLEANER"] = "VACUUM_CLEANER";
DisplayCategory["VACUUM"] = "VACUUM";
/** Not on the list of display categories any more; kept for 1.x callers. */
DisplayCategory["VEHICLE"] = "VEHICLE";
DisplayCategory["WASHER"] = "WASHER";
DisplayCategory["WATER_HEATER"] = "WATER_HEATER";

13
dist/cjs/index.js vendored
View file

@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AlexaErrorResponse = exports.AlexaErrorType = exports.ThermostatMode = exports.TemperatureSensorScale = exports.EndpointHealth = exports.PowerController = exports.AlexaStatusMessage = exports.DisplayCategory = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterfaceType = exports.AlexaInterface = exports.Device = exports.DEFAULT_HOST = exports.Alex2MQTT = void 0;
exports.DisplayCategories = exports.States = exports.Actions = exports.Units = exports.Assets = exports.SchemaError = exports.DeclarationError = exports.registry = exports.AlexaErrorResponse = exports.AlexaErrorType = exports.ThermostatMode = exports.TemperatureSensorScale = exports.EndpointHealth = exports.PowerController = exports.AlexaStatusMessage = exports.DisplayCategory = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterfaceType = exports.AlexaInterface = exports.Device = exports.DEFAULT_HOST = exports.Alex2MQTT = void 0;
// Relative specifiers carry ".js": Node's ES module loader resolves no extension, and TypeScript maps it back to the .ts.
var Alex2Node_js_1 = require("./Alex2Node.js");
Object.defineProperty(exports, "Alex2MQTT", { enumerable: true, get: function () { return __importDefault(Alex2Node_js_1).default; } });
@ -27,3 +27,14 @@ Object.defineProperty(exports, "ThermostatMode", { enumerable: true, get: functi
var AlexaErrorResponse_js_1 = require("./AlexaErrorResponse.js");
Object.defineProperty(exports, "AlexaErrorType", { enumerable: true, get: function () { return AlexaErrorResponse_js_1.AlexaErrorType; } });
Object.defineProperty(exports, "AlexaErrorResponse", { enumerable: true, get: function () { return AlexaErrorResponse_js_1.AlexaErrorResponse; } });
// The interface registry and the vocabularies of the Smart Home API
var index_js_1 = require("./registry/index.js");
Object.defineProperty(exports, "registry", { enumerable: true, get: function () { return index_js_1.registry; } });
Object.defineProperty(exports, "DeclarationError", { enumerable: true, get: function () { return index_js_1.DeclarationError; } });
Object.defineProperty(exports, "SchemaError", { enumerable: true, get: function () { return index_js_1.SchemaError; } });
var index_js_2 = require("./registry/index.js");
Object.defineProperty(exports, "Assets", { enumerable: true, get: function () { return index_js_2.ASSETS; } });
Object.defineProperty(exports, "Units", { enumerable: true, get: function () { return index_js_2.UNITS_OF_MEASURE; } });
Object.defineProperty(exports, "Actions", { enumerable: true, get: function () { return index_js_2.ACTIONS; } });
Object.defineProperty(exports, "States", { enumerable: true, get: function () { return index_js_2.STATES; } });
Object.defineProperty(exports, "DisplayCategories", { enumerable: true, get: function () { return index_js_2.DISPLAY_CATEGORIES; } });

132
dist/cjs/registry/catalog.js vendored Normal file
View file

@ -0,0 +1,132 @@
"use strict";
// The fixed vocabularies of the Smart Home API, copied from Amazon's pages as read on 2026-09-28. The pages are under
// https://developer.amazon.com/docs/alexaplus/device-apis/. A value missing here is one Amazon added since.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ERROR_TYPES = exports.LIMITS = exports.RESERVED_WORDS = exports.DISPLAY_CATEGORIES = exports.STATES = exports.ACTIONS = exports.UNITS_OF_MEASURE = exports.ASSETS = void 0;
/**
* The asset ids a friendly name can refer to: 103, the units of measure among them
* (resources-and-assets.html, "Global Alexa catalog").
*/
exports.ASSETS = [
"Alexa.Actions.Charge", "Alexa.Actions.Clean", "Alexa.Actions.Dispense", "Alexa.Actions.Dock",
"Alexa.Actions.Empty", "Alexa.Actions.Mop", "Alexa.Actions.Skip", "Alexa.Actions.Sweep", "Alexa.Actions.Vacuum",
"Alexa.Button.OffButton", "Alexa.Button.OnButton", "Alexa.Button.BrightenButton", "Alexa.Button.DimButton",
"Alexa.Button.MainButton", "Alexa.Button.TopButton", "Alexa.Button.BottomButton", "Alexa.Button.CenterButton",
"Alexa.Button.MiddleButton", "Alexa.Button.UpButton", "Alexa.Button.DownButton", "Alexa.Button.LeftButton",
"Alexa.Button.RightButton", "Alexa.Button.FirstButton", "Alexa.Button.SecondButton", "Alexa.Button.ThirdButton",
"Alexa.Button.FourthButton", "Alexa.Button.FifthButton", "Alexa.Button.SixthButton", "Alexa.Button.SeventhButton",
"Alexa.Button.EighthButton", "Alexa.Button.DoublePress", "Alexa.Button.DoublePush", "Alexa.Button.LongPress",
"Alexa.Button.LongPush", "Alexa.Button.SinglePress", "Alexa.Button.SinglePush",
"Alexa.DeviceName.AirPurifier", "Alexa.DeviceName.Camera", "Alexa.DeviceName.Fan", "Alexa.DeviceName.Router",
"Alexa.DeviceName.Shade", "Alexa.DeviceName.Shower", "Alexa.DeviceName.SpaceHeater", "Alexa.DeviceName.Washer",
"Alexa.Gesture.DoubleClick", "Alexa.Gesture.SingleClick", "Alexa.Gesture.SwipeDown", "Alexa.Gesture.SwipeLeft",
"Alexa.Gesture.SwipeRight", "Alexa.Gesture.SwipeUp", "Alexa.Gesture.Tap",
// "Gestures", plural: the page spells this one id differently from the seven above
"Alexa.Gestures.DoubleTap",
"Alexa.Setting.2GGuestWiFi", "Alexa.Setting.5GGuestWiFi", "Alexa.Setting.Auto", "Alexa.Setting.Direction",
"Alexa.Setting.DryCycle", "Alexa.Setting.FanSpeed", "Alexa.Setting.GuestWiFi", "Alexa.Setting.Heat",
"Alexa.Setting.Mode", "Alexa.Setting.Night", "Alexa.Setting.Opening", "Alexa.Setting.Oscillate",
"Alexa.Setting.Preset", "Alexa.Setting.Quiet", "Alexa.Setting.Temperature", "Alexa.Setting.WashCycle",
"Alexa.Setting.WaterTemperature",
"Alexa.Shower.HandHeld", "Alexa.Shower.RainHead",
"Alexa.Unit.Angle.Degrees", "Alexa.Unit.Angle.Radians", "Alexa.Unit.Distance.Feet", "Alexa.Unit.Distance.Inches",
"Alexa.Unit.Distance.Kilometers", "Alexa.Unit.Distance.Meters", "Alexa.Unit.Distance.Miles",
"Alexa.Unit.Distance.Yards", "Alexa.Unit.Mass.Grams", "Alexa.Unit.Mass.Kilograms", "Alexa.Unit.Percent",
"Alexa.Unit.Temperature.Celsius", "Alexa.Unit.Temperature.Degrees", "Alexa.Unit.Temperature.Fahrenheit",
"Alexa.Unit.Temperature.Kelvin", "Alexa.Unit.Volume.CubicFeet", "Alexa.Unit.Volume.CubicMeters",
"Alexa.Unit.Volume.Gallons", "Alexa.Unit.Volume.Liters", "Alexa.Unit.Volume.Pints", "Alexa.Unit.Volume.Quarts",
"Alexa.Unit.Weight.Ounces", "Alexa.Unit.Weight.Pounds",
"Alexa.Value.Close", "Alexa.Value.Delicate", "Alexa.Value.High", "Alexa.Value.Low", "Alexa.Value.Maximum",
"Alexa.Value.Medium", "Alexa.Value.Minimum", "Alexa.Value.Open", "Alexa.Value.QuickWash",
];
exports.UNITS_OF_MEASURE = exports.ASSETS.filter((id) => id.startsWith("Alexa.Unit."));
/** The phrases an action mapping gives to a directive (alexa-discovery-objects.html, "ActionMappings object"). */
exports.ACTIONS = [
"Alexa.Actions.Open", "Alexa.Actions.Close", "Alexa.Actions.Raise", "Alexa.Actions.Lower",
"Alexa.Actions.SetEcoOn", "Alexa.Actions.SetEcoOff",
];
/** The states a state mapping gives to a property value (alexa-discovery-objects.html, "StateMappings object"). */
exports.STATES = [
"Alexa.States.Open", "Alexa.States.Closed", "Alexa.States.EcoOn", "Alexa.States.EcoOff", "Alexa.States.Low",
"Alexa.States.Empty", "Alexa.States.Full", "Alexa.States.Done", "Alexa.States.Stuck",
];
/**
* The 56 display categories (alexa-discovery.html, "Display categories"). The DisplayCategory enum also keeps
* VEHICLE from 1.x, which the page no longer lists.
*/
exports.DISPLAY_CATEGORIES = [
"ACTIVITY_TRIGGER", "AIR_CONDITIONER", "AIR_FRESHENER", "AIR_PURIFIER", "AIR_QUALITY_MONITOR",
"ALEXA_VOICE_ENABLED", "AUTO_ACCESSORY", "BLUETOOTH_SPEAKER", "CAMERA", "CHRISTMAS_TREE", "COFFEE_MAKER",
"COMPUTER", "CONTACT_SENSOR", "DISHWASHER", "DOOR", "DOORBELL", "DRYER", "EXTERIOR_BLIND", "FAN", "GAME_CONSOLE",
"GARAGE_DOOR", "HEADPHONES", "HUB", "INTERIOR_BLIND", "LAPTOP", "LIGHT", "MICROWAVE", "MOBILE_PHONE",
"MOTION_SENSOR", "MUSIC_SYSTEM", "NETWORK_HARDWARE", "OTHER", "OVEN", "PHONE", "PRINTER", "REMOTE", "ROUTER",
"SCENE_TRIGGER", "SCREEN", "SECURITY_PANEL", "SECURITY_SYSTEM", "SLOW_COOKER", "SMARTLOCK", "SMARTPLUG", "SPEAKER",
"STREAMING_DEVICE", "SWITCH", "TABLET", "TEMPERATURE_SENSOR", "THERMOSTAT", "TV", "VACUUM_CLEANER", "VACUUM",
"WASHER", "WATER_HEATER", "WEARABLE",
];
/** Not to be used as a friendly name (resources-and-assets.html, "Reserved words"). */
exports.RESERVED_WORDS = [
"alarm", "alarms", "all alarms", "away mode", "bass", "camera", "date", "date today", "day", "do not disturb",
"drop in", "music", "night light", "notification", "playing", "sleep sounds", "time", "timer", "today in music",
"treble", "volume", "way f. m.",
];
/**
* What a discovery answer may hold (alexa-discovery.html, "Interface limits"; alexa-discovery-objects.html,
* "Endpoint object details" and "AdditionalAttributes object details"; alexa-scenecontroller.html, "Discovery").
*/
exports.LIMITS = {
endpointsPerCustomer: 300,
capabilitiesPerEndpoint: 100,
endpointIdLength: 256,
friendlyNameLength: 256,
sceneFriendlyNameLength: 128,
manufacturerNameLength: 128,
descriptionLength: 128,
additionalAttributeLength: 256,
cookieBytes: 5000,
};
// alexa-errorresponse.html, "Error type values". The Interface column names <namespace>.ErrorResponse; the event
// header carries the namespace and the name ErrorResponse, as the examples on the ThermostatController,
// SecurityPanelController and Safety error pages show. The other namespaces are as the table spells them.
// INVALID_VALUE is listed under Alexa and under SmartVision.ObjectDetectionSensor, SUBSCRIPTION_REQUIRED under both
// SmartVision interfaces: the first one listed is kept.
const ERROR_TYPES_BY_NAMESPACE = {
"Alexa": [
"ALREADY_IN_OPERATION", "BRIDGE_UNREACHABLE", "CLOUD_CONTROL_DISABLED", "DEVICE_STUCK", "DO_NOT_DISTURB_MODE",
"ENDPOINT_BUSY", "ENDPOINT_CONTROL_UNAVAILABLE", "ENDPOINT_LOW_POWER", "ENDPOINT_UNREACHABLE",
"EXPIRED_AUTHORIZATION_CREDENTIAL", "FIRMWARE_OUT_OF_DATE", "HARDWARE_MALFUNCTION", "INSUFFICIENT_PERMISSIONS",
"INSUFFICIENT_RESOURCE", "INTERNAL_ERROR", "INVALID_AUTHORIZATION_CREDENTIAL", "INVALID_DIRECTIVE",
"INVALID_VALUE", "MAINTENANCE_REQUIRED", "NO_SUCH_ENDPOINT", "NOT_CALIBRATED", "NOT_IN_OPERATION",
"NOT_SUPPORTED_IN_CURRENT_MODE", "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE",
"PARTNER_APPLICATION_REDIRECTION", "POWER_LEVEL_NOT_SUPPORTED", "RATE_LIMIT_EXCEEDED",
"TEMPERATURE_VALUE_OUT_OF_RANGE", "TOO_MANY_FAILED_ATTEMPTS", "UNABLE_TO_CHARGE", "VALUE_OUT_OF_RANGE",
],
"Alexa.Commissionable.ReportCommissioningInformation": [
"FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS", "MAX_COMMISSIONING_LIMIT_REACHED",
],
"Alexa.Cooking": [
"CHILD_LOCK", "COOK_DURATION_TOO_LONG", "DOOR_CLOSED_TOO_LONG", "DOOR_OPEN", "PREHEAT_REQUIRED",
"PROBE_REQUIRED", "REMOTE_START_NOT_SUPPORTED", "REMOVE_PROBE", "REMOTE_START_DISABLED",
],
"Alexa.DataController": ["DATA_DELETION_NOT_SUPPORTED", "DATA_RETRIEVAL_NOT_SUPPORTED"],
"Alexa.Safety": ["OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED"],
"Alexa.SecurityPanelController": [
"AUTHORIZATION_REQUIRED", "BYPASS_NEEDED", "NOT_READY", "UNAUTHORIZED", "UNCLEARED_ALARM", "UNCLEARED_TROUBLE",
],
"Alexa.SmartVision.ObjectDetectionSensor": ["SUBSCRIPTION_REQUIRED"],
"Alexa.SmartVision.SnapshotProvider": ["DISABLED_BY_USER"],
"Alexa.ThermostatController": [
"DUAL_SETPOINTS_UNSUPPORTED", "REQUESTED_SETPOINTS_TOO_CLOSE", "THERMOSTAT_IS_OFF",
"TRIPLE_SETPOINTS_UNSUPPORTED", "UNSUPPORTED_THERMOSTAT_MODE", "UNWILLING_TO_SET_SCHEDULE",
"UNWILLING_TO_SET_VALUE",
],
"Alexa.ThermostatController.Configuration": [
"CONFIGURATION_UPDATE_NOT_ALLOWED", "COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE",
"COOLING_STAGES_EXCEEDS_LIMIT", "HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", "HEATING_STAGES_EXCEEDS_LIMIT",
"INVALID_AUXILIARY_HEATING_SYSTEM_TYPE", "INVALID_SYSTEM_TYPE", "INVALID_TARGET_STATE",
"INVALID_TEMPERATURE_SCALE", "INVALID_TERMINAL_CONNECTION", "MISSING_SETUP_INFORMATION",
],
"Alexa.ThermostatController.Schedule": ["INSUFFICIENT_SPACE"],
};
/** The 73 error types of the table, each with the namespace its ErrorResponse goes under. */
exports.ERROR_TYPES = Object.fromEntries(Object.entries(ERROR_TYPES_BY_NAMESPACE).flatMap(([namespace, types]) => types.map((type) => [type, namespace])));

68
dist/cjs/registry/index.js vendored Normal file
View file

@ -0,0 +1,68 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaError = exports.s = exports.defineInterface = exports.DeclarationError = exports.TemperatureSensor = exports.PowerController = exports.EndpointHealth = exports.BrightnessController = exports.Alexa = exports.registry = void 0;
// The interfaces the library knows, by namespace.
const Alexa_js_1 = require("./interfaces/Alexa.js");
Object.defineProperty(exports, "Alexa", { enumerable: true, get: function () { return Alexa_js_1.Alexa; } });
const BrightnessController_js_1 = require("./interfaces/BrightnessController.js");
Object.defineProperty(exports, "BrightnessController", { enumerable: true, get: function () { return BrightnessController_js_1.BrightnessController; } });
const EndpointHealth_js_1 = require("./interfaces/EndpointHealth.js");
Object.defineProperty(exports, "EndpointHealth", { enumerable: true, get: function () { return EndpointHealth_js_1.EndpointHealth; } });
const PowerController_js_1 = require("./interfaces/PowerController.js");
Object.defineProperty(exports, "PowerController", { enumerable: true, get: function () { return PowerController_js_1.PowerController; } });
const TemperatureSensor_js_1 = require("./interfaces/TemperatureSensor.js");
Object.defineProperty(exports, "TemperatureSensor", { enumerable: true, get: function () { return TemperatureSensor_js_1.TemperatureSensor; } });
const stubs_js_1 = require("./interfaces/stubs.js");
const types_js_1 = require("./types.js");
const described = [
Alexa_js_1.Alexa,
BrightnessController_js_1.BrightnessController,
EndpointHealth_js_1.EndpointHealth,
PowerController_js_1.PowerController,
TemperatureSensor_js_1.TemperatureSensor,
];
const descriptors = new Map();
for (const descriptor of [...described, ...stubs_js_1.STUBS]) {
// A stub left in the table next to the descriptor that replaces it would win or lose by the order of this list
if (descriptors.has(descriptor.namespace))
throw new Error(`${descriptor.namespace} is described twice`);
descriptors.set(descriptor.namespace, descriptor);
}
exports.registry = {
/** Whether an interface of this name is known. */
has(namespace) {
return descriptors.has(namespace);
},
/** The descriptor of "Alexa.RangeController" or of AlexaInterfaceType.RANGE_CONTROLLER, which is that string. */
get(namespace) {
const descriptor = descriptors.get(namespace);
if (!descriptor)
throw new types_js_1.DeclarationError({}, `${JSON.stringify(namespace)} is not an interface alex2node knows`);
return descriptor;
},
/** Every descriptor, ordered by namespace. */
list() {
return [...descriptors.values()].sort((a, b) => (a.namespace < b.namespace ? -1 : 1));
},
};
var types_js_2 = require("./types.js");
Object.defineProperty(exports, "DeclarationError", { enumerable: true, get: function () { return types_js_2.DeclarationError; } });
Object.defineProperty(exports, "defineInterface", { enumerable: true, get: function () { return types_js_2.defineInterface; } });
var schema_js_1 = require("./schema.js");
Object.defineProperty(exports, "s", { enumerable: true, get: function () { return schema_js_1.s; } });
Object.defineProperty(exports, "SchemaError", { enumerable: true, get: function () { return schema_js_1.SchemaError; } });
__exportStar(require("./catalog.js"), exports);

23
dist/cjs/registry/interfaces/Alexa.js vendored Normal file
View file

@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Alexa = void 0;
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
/**
* The base interface: every endpoint lists it (alexa-interface.html, "Support the Alexa interface in all add-ons").
* Its one directive to a device is ReportState, answered with a StateReport.
*/
exports.Alexa = (0, types_js_1.defineInterface)({
namespace: "Alexa",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-interface.html",
kind: "base",
tier: 1,
instanced: false,
properties: {},
directives: {
ReportState: { name: "ReportState", payload: schema_js_1.s.object({}) },
},
// { type, interface, version } and nothing else, as in the discovery example of the page
discovery: () => ({ properties: false }),
});

View file

@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrightnessController = void 0;
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
/** Both directives turn a light that is off on, at the brightness asked for (alexa-brightnesscontroller.html). */
exports.BrightnessController = (0, types_js_1.defineInterface)({
namespace: "Alexa.BrightnessController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-brightnesscontroller.html",
kind: "controller",
tier: 1,
instanced: false,
properties: {
brightness: { name: "brightness", value: schema_js_1.s.number({ min: 0, max: 100, integer: true }) },
},
directives: {
SetBrightness: {
name: "SetBrightness",
payload: schema_js_1.s.object({ brightness: schema_js_1.s.number({ min: 0, max: 100, integer: true }) }),
},
AdjustBrightness: {
name: "AdjustBrightness",
payload: schema_js_1.s.object({ brightnessDelta: schema_js_1.s.number({ min: -100, max: 100, integer: true }) }),
},
},
});

View file

@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EndpointHealth = exports.CONNECTIVITY_REASONS = void 0;
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
exports.CONNECTIVITY_REASONS = [
"WIFI_BAD_PASSWORD", "WIFI_AP_NOT_FOUND", "WIFI_ROUTER_UNREACHABLE", "WIFI_AP_CHANNEL_QUALITY_LOW",
"INTERNET_UNREACHABLE", "CAPTIVE_PORTAL_CHECK_FAILED", "UNKNOWN",
];
/**
* Version 3.1, as the page is titled. It defines connectivity only. Examples on other pages declare 3 and 3.2;
* alex2node 1.x declared 3.3, which no page mentions.
*/
exports.EndpointHealth = (0, types_js_1.defineInterface)({
namespace: "Alexa.EndpointHealth",
version: "3.1",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-endpointhealth.html",
kind: "sensor",
tier: 1,
instanced: false,
properties: {
connectivity: {
name: "connectivity",
value: schema_js_1.s.object({
value: schema_js_1.s.enum("OK", "UNREACHABLE"),
reason: schema_js_1.s.optional(schema_js_1.s.enum(...exports.CONNECTIVITY_REASONS)),
}),
note: "in every Response, StateReport and ChangeReport; a change of it reported within three seconds",
},
},
directives: {},
});

View file

@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PowerController = void 0;
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
exports.PowerController = (0, types_js_1.defineInterface)({
namespace: "Alexa.PowerController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-powercontroller.html",
kind: "controller",
tier: 1,
instanced: false,
properties: {
powerState: { name: "powerState", value: schema_js_1.s.enum("ON", "OFF") },
},
directives: {
TurnOn: { name: "TurnOn", payload: schema_js_1.s.object({}) },
TurnOff: { name: "TurnOff", payload: schema_js_1.s.object({}) },
},
options: schema_js_1.s.object({
// The directives Alexa asks the user to confirm before it sends them. Amazon supports this for devices in Japan
// only (alexa-discovery-objects.html, "VerificationsRequired object").
verificationsRequired: schema_js_1.s.optional(schema_js_1.s.array(schema_js_1.s.enum("TurnOn", "TurnOff"), { min: 1 })),
}, { unknownKeys: "reject" }),
discovery({ options }) {
if (!options.verificationsRequired)
return {};
const confirmed = options.verificationsRequired.map((directive) => ({ directive, methods: [{ "@type": "Confirmation" }] }));
return { topLevel: { verificationsRequired: confirmed } };
},
});

View file

@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TemperatureSensor = void 0;
const schema_js_1 = require("../schema.js");
const types_js_1 = require("../types.js");
/** A sensor: an endpoint that has it declares Alexa.EndpointHealth as well (alexa-temperaturesensor.html). */
exports.TemperatureSensor = (0, types_js_1.defineInterface)({
namespace: "Alexa.TemperatureSensor",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-temperaturesensor.html",
kind: "sensor",
tier: 1,
instanced: false,
properties: {
temperature: { name: "temperature", value: schema_js_1.s.temperature() },
},
directives: {},
});

102
dist/cjs/registry/interfaces/stubs.js vendored Normal file
View file

@ -0,0 +1,102 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.STUBS = void 0;
// Interfaces without a descriptor of their own. A stub names the interface with the version and the properties
// alex2node 1.5.2 put in discovery, so an endpoint that declares one is announced as before. Nothing about it is
// checked, and the version is the one 1.5.2 sent, which is not always the one on the page of the interface
// (Alexa.Speaker is "3" there). A row leaves this table when a descriptor is written from that page.
const schema_js_1 = require("../schema.js");
const DEVICE_APIS = "https://developer.amazon.com/docs/alexaplus/device-apis";
const LIST_OF_INTERFACES = `${DEVICE_APIS}/list-of-interfaces.html`;
// "As of August 7, 2026, Automotive skills and the associated Alexa.AuthorizationController and
// Alexa.Automotive.VehicleData APIs are no longer available."
const DEPRECATED_FEATURES = "https://developer.amazon.com/en-US/docs/alexa/ask-overviews/deprecated-features.html";
const TABLE = [
["Alexa.ApplicationStateReporter", "1", [], "https://developer.amazon.com/docs/alexaplus/alexa-voice-service/alexa-applicationstatereporter.html"],
["Alexa.Audio.PlayQueue", "1", [], "alexa-audio-playqueue.html"],
["Alexa.AuthorizationController", "1", [], DEPRECATED_FEATURES],
["Alexa.AutomationManagement", "1", ["automationStatuses"], "alexa-automationmanagement.html"],
["Alexa.Automotive.VehicleData", "1", [], DEPRECATED_FEATURES],
// 1.5.2 announced the version "UNKNOWN"; 1.7 is what the list of interfaces gives
["Alexa.Camera.LiveViewController", "1.7", [], LIST_OF_INTERFACES],
["Alexa.CameraStreamController", "3", [], "alexa-camerastreamcontroller.html"],
["Alexa.ChannelController", "3", ["channel"], "alexa-channelcontroller.html"],
["Alexa.ColorController", "3", ["color"], "alexa-colorcontroller.html"],
["Alexa.ColorTemperatureController", "3", ["colorTemperatureInKelvin"], "alexa-colortemperaturecontroller.html"],
["Alexa.Commissionable", "1", [], "alexa-commissionable.html"],
["Alexa.ConsentManagement.ConsentRequiredReporter", "1", [], "alexa-consentrequiredreporter.html"],
["Alexa.ContactSensor", "3", ["detectionState"], "alexa-contactsensor.html"],
["Alexa.Cooking", "1", [], "alexa-cooking.html"],
["Alexa.Cooking.FoodTemperatureController", "1", [], "alexa-cooking-foodtemperaturecontroller.html"],
["Alexa.Cooking.FoodTemperatureSensor", "1", [], "alexa-cooking-foodtemperaturesensor.html"],
["Alexa.Cooking.PresetController", "1", [], "alexa-cooking-presetcontroller.html"],
["Alexa.Cooking.TemperatureController", "1", [], "alexa-cooking-temperaturecontroller.html"],
["Alexa.Cooking.TemperatureSensor", "1", [], "alexa-cooking-temperaturesensor.html"],
["Alexa.Cooking.TimeController", "1", [], "alexa-cooking-timecontroller.html"],
["Alexa.DataController", "1", [], "alexa-datacontroller.html"],
["Alexa.DeviceUsage.Estimation", "1", [], "alexa-deviceusage-estimation.html"],
["Alexa.DeviceUsage.Meter", "1", [], "alexa-deviceusage-meter.html"],
["Alexa.DoorbellEventSource", "3", [], "alexa-doorbelleventsource.html"],
["Alexa.EqualizerController", "1", [], "alexa-equalizercontroller.html"],
["Alexa.InputController", "3", [], "alexa-inputcontroller.html"],
["Alexa.InventoryLevelSensor", "1", [], "alexa-inventorylevelsensor.html"],
["Alexa.InventoryLevelUsageSensor", "1", [], "alexa-inventorylevelusagesensor.html"],
["Alexa.InventoryUsageSensor", "1", [], "alexa-inventoryusagesensor.html"],
["Alexa.KeypadController", "1", [], "alexa-keypadcontroller.html"],
["Alexa.Launcher", "1.1", [], "alexa-launcher.html"],
["Alexa.LockController", "3", [], "alexa-lockcontroller.html"],
["Alexa.Media.PlayQueue", "1", [], "alexa-media-playqueue.html"],
["Alexa.Media.Playback", "1", [], "alexa-media-playback.html"],
["Alexa.Media.Search", "1", [], "alexa-media-search.html"],
["Alexa.ModeController", "3", ["mode"], "alexa-modecontroller.html"],
["Alexa.MotionSensor", "3", [], "alexa-motionsensor.html"],
["Alexa.PercentageController", "3", [], "alexa-percentagecontroller.html"],
["Alexa.PlaybackController", "3", [], "alexa-playbackcontroller.html"],
["Alexa.PlaybackStateReporter", "1", [], "alexa-playbackcontroller.html"],
["Alexa.PowerLevelController", "3", [], "alexa-powerlevelcontroller.html"],
["Alexa.ProactiveNotificationSource", "1", [], "alexa-proactivenotificationsource.html"],
["Alexa.RTCSessionController", "1", [], "alexa-rtcsessioncontroller.html"],
["Alexa.RangeController", "3", [], "alexa-rangecontroller.html"],
["Alexa.RecordController", "3", [], "alexa-recordcontroller.html"],
["Alexa.RemoteVideoPlayer", "1", [], "alexa-remotevideoplayer.html"],
["Alexa.SceneController", "3", [], "alexa-scenecontroller.html"],
["Alexa.SecurityPanelController", "1", [], "alexa-securitypanelcontroller.html"],
["Alexa.SecurityPanelController.Alert", "1", [], "alexa-securitypanelcontroller-alert.html"],
["Alexa.SeekController", "3", [], "alexa-seekcontroller.html"],
["Alexa.SimpleEventSource", "1", [], "alexa-simpleeventsource.html"],
["Alexa.SmartVision.ObjectDetectionSensor", "1", [], "alexa-smartvision-objectdetectionsensor.html"],
["Alexa.SmartVision.SnapshotProvider", "1", [], "alexa-smartvision-snapshotprovider.html"],
["Alexa.Speaker", "1", [], "alexa-speaker.html"],
["Alexa.StepSpeaker", "1", [], "alexa-stepspeaker.html"],
["Alexa.ThermostatController", "3.2", ["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"], "alexa-thermostatcontroller.html"],
["Alexa.ThermostatController.Configuration", "1", [], "alexa-thermostatcontroller-configuration.html"],
["Alexa.ThermostatController.HVAC.Components", "1", [], "alexa-thermostatcontroller-hvac-components.html"],
["Alexa.ThermostatController.Schedule", "1", [], "alexa-thermostatcontroller-schedule.html"],
// "UNKNOWN" in 1.5.2 as well; the page is titled "Interface 3"
["Alexa.TimeHoldController", "3", [], "alexa-timeholdcontroller.html"],
["Alexa.ToggleController", "3", ["toggleState"], "alexa-togglecontroller.html"],
["Alexa.UIController", "1", [], "alexa-uicontroller.html"],
["Alexa.UserPreference", "1", [], "alexa-userpreference.html"],
["Alexa.VideoRecorder", "1", [], "alexa-videorecorder.html"],
["Alexa.WakeOnLANController", "1", [], "alexa-wakeonlancontroller.html"],
];
function kindOf(namespace) {
if (namespace.endsWith("Sensor"))
return "sensor";
if (namespace.endsWith("EventSource"))
return "eventSource";
return "controller";
}
function stub([namespace, version, properties, page]) {
return {
namespace,
version,
doc: page.startsWith("https://") ? page : `${DEVICE_APIS}/${page}`,
kind: kindOf(namespace),
tier: 3,
instanced: false,
properties: Object.fromEntries(properties.map((name) => [name, { name, value: schema_js_1.s.unknown() }])),
directives: {},
};
}
exports.STUBS = TABLE.map(stub);

190
dist/cjs/registry/schema.js vendored Normal file
View file

@ -0,0 +1,190 @@
"use strict";
// Values checked at run time and typed at compile time. A descriptor states its property values, directive payloads
// and declaration options with these; parse() returns the value or throws a SchemaError that names where it went
// wrong. Written here rather than taken from a package: mqtt stays the only runtime dependency.
Object.defineProperty(exports, "__esModule", { value: true });
exports.s = exports.SchemaError = void 0;
/** A value that does not fit its schema: path is where ("payload.targetSetpoint.scale"), problem is what. */
class SchemaError extends Error {
constructor(path, problem) {
super(path ? `${path}: ${problem}` : problem);
this.path = path;
this.problem = problem;
this.name = "SchemaError";
}
}
exports.SchemaError = SchemaError;
// The input as an error message shows it: short, and quoted when it is text.
function shown(input) {
if (input === undefined)
return "nothing";
if (typeof input === "function")
return "a function";
const text = JSON.stringify(input) ?? String(input);
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
}
function mismatch(path, expects, input) {
return new SchemaError(path, `expected ${expects}, got ${shown(input)}`);
}
const at = (path, key) => (path ? `${path}.${key}` : key);
function isRecord(input) {
return typeof input === "object" && input !== null && !Array.isArray(input);
}
function schema(expects, accepts) {
return {
expects,
parse(input, path = "") {
if (!accepts(input))
throw mismatch(path, expects, input);
return input;
},
};
}
function numberText({ min, max, gt, integer }) {
const kind = integer ? "an integer" : "a number";
if (min !== undefined && max !== undefined)
return `${kind} from ${min} to ${max}`;
if (min !== undefined)
return `${kind} of ${min} or more`;
if (max !== undefined)
return `${kind} of ${max} or less`;
if (gt !== undefined)
return `${kind} greater than ${gt}`;
return kind;
}
function number(rules = {}) {
const { min = -Infinity, max = Infinity, gt = -Infinity, integer = false } = rules;
return schema(numberText(rules), (input) => typeof input === "number" && Number.isFinite(input) && input >= min && input <= max && input > gt
&& (!integer || Number.isInteger(input)));
}
function string(rules = {}) {
const { min = 0, max = Infinity, pattern } = rules;
const length = max === Infinity ? (min > 0 ? ` of ${min} or more characters` : "") : ` of ${min} to ${max} characters`;
return schema(rules.expects ?? `a string${length}`, (input) => typeof input === "string" && input.length >= min && input.length <= max && (!pattern || pattern.test(input)));
}
function boolean() {
return schema("true or false", (input) => typeof input === "boolean");
}
function literal(value) {
return schema(JSON.stringify(value), (input) => input === value);
}
function enumeration(...values) {
return { ...schema(values.join(" | "), (input) => values.includes(input)), values };
}
function unknown() {
return schema("any value", () => true);
}
function optional(inner) {
return {
expects: inner.expects,
optional: true,
parse: (input, path = "") => (input === undefined ? undefined : inner.parse(input, path)),
};
}
function nullable(inner) {
const expects = `${inner.expects} or null`;
return {
expects,
parse(input, path = "") {
if (input === null)
return null;
try {
return inner.parse(input, path);
}
catch (err) {
// The value itself is of the wrong kind: null was a choice too. An error further in keeps its own path.
if (err instanceof SchemaError && err.path === path)
throw mismatch(path, expects, input);
throw err;
}
},
};
}
function array(item, rules = {}) {
const { min = 0, max = Infinity } = rules;
const count = max === Infinity ? (min > 0 ? ` with ${min} or more entries` : "") : ` with ${min} to ${max} entries`;
const expects = `a list${count}`;
return {
expects,
parse(input, path = "") {
if (!Array.isArray(input) || input.length < min || input.length > max)
throw mismatch(path, expects, input);
return input.map((entry, i) => item.parse(entry, `${path}[${i}]`));
},
};
}
/**
* An object with the keys of shape. Keys the shape does not name are kept as they are: a field Alexa adds to a
* directive reaches the handler. unknownKeys "reject" is for what a developer writes, where such a key is a typo.
*/
function object(shape, rules = {}) {
const known = Object.keys(shape);
const expects = known.length > 0 ? `an object with ${known.join(", ")}` : "an object";
return {
expects,
parse(input, path = "") {
if (!isRecord(input))
throw mismatch(path, expects, input);
const others = Object.keys(input).filter((key) => !known.includes(key));
if (others.length > 0 && rules.unknownKeys === "reject") {
throw new SchemaError(at(path, others[0]), `unknown key, the known ones are ${known.join(", ") || "none"}`);
}
const parsed = {};
for (const key of known) {
const value = shape[key].parse(input[key], at(path, key));
if (value !== undefined)
parsed[key] = value;
}
for (const key of others)
parsed[key] = input[key];
return parsed;
},
};
}
// alexa-property-schemas.html "Temperature", "Temperature scales"
function temperature() {
return object({ value: number(), scale: enumeration("CELSIUS", "FAHRENHEIT", "KELVIN") });
}
// alexa-property-schemas.html "DateTime": UTC, no offsets. The seconds are optional here because the TimeInterval
// examples on the same page leave them out ("2017-10-04T14:00Z").
const DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?Z$/;
function dateTime() {
return schema("a UTC time like 2017-08-30T01:18:21Z", (input) => typeof input === "string" && DATE_TIME.test(input) && !Number.isNaN(Date.parse(input)));
}
// alexa-property-schemas.html "Duration": the time portion of ISO 8601, negative for a delta ("PT-30S")
const DURATION = /^PT(?=.)(-?\d+H)?(-?\d+M)?(-?\d+S)?$/;
function duration() {
return string({ pattern: DURATION, expects: "a duration like PT3M15S" });
}
// alexa-property-schemas.html "TimeInterval": "Specify one or two of the time interval fields. If you specify all
// three fields, an error occurs."
function timeInterval() {
const fields = object({ start: optional(dateTime()), end: optional(dateTime()), duration: optional(duration()) });
const expects = "a time interval with one or two of start, end, duration";
return {
expects,
parse(input, path = "") {
const interval = fields.parse(input, path);
const given = [interval.start, interval.end, interval.duration].filter((field) => field !== undefined).length;
if (given < 1 || given > 2)
throw mismatch(path, expects, input);
return interval;
},
};
}
exports.s = {
string,
number,
boolean,
literal,
enum: enumeration,
unknown,
optional,
nullable,
array,
object,
temperature,
dateTime,
duration,
timeInterval,
};

25
dist/cjs/registry/types.js vendored Normal file
View file

@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeclarationError = void 0;
exports.defineInterface = defineInterface;
/** Types a descriptor from what it is given: the property names, the payload of each directive, the options. */
function defineInterface(descriptor) {
return descriptor;
}
/** A device, a capability or an interface name declared in a way Alexa would reject. Thrown where it is declared. */
class DeclarationError extends Error {
constructor(where, problem) {
const capability = where.namespace && where.instance ? `${where.namespace} "${where.instance}"` : where.namespace;
super([where.endpointId && shortened(where.endpointId), capability, problem].filter(Boolean).join(": "));
this.name = "DeclarationError";
this.endpointId = where.endpointId;
this.namespace = where.namespace;
this.instance = where.instance || undefined;
this.problem = problem;
}
}
exports.DeclarationError = DeclarationError;
// An endpointId may be the very thing that is wrong, 300 characters of it.
function shortened(text) {
return text.length > 64 ? `${text.slice(0, 61)}...` : text;
}

View file

@ -101,7 +101,9 @@ export declare class AlexaInterface {
setInstance(name: string): void;
getType(): AlexaInterfaceType;
getTypeString(): string;
/** The version of the interface, from its descriptor. "UNKNOWN" for a name the registry does not have. */
getVersion(): string;
/** The names of the properties the interface reports, from its descriptor. */
getProps(): string[];
getJSON(): object;
}

View file

@ -1,3 +1,4 @@
import { registry } from "./registry/index.js";
export var AlexaInterfaceType;
(function (AlexaInterfaceType) {
AlexaInterfaceType["APPLICATION_STATE_REPORTER"] = "Alexa.ApplicationStateReporter";
@ -100,170 +101,13 @@ export class AlexaInterface {
getTypeString() {
return this.type;
}
/** The version of the interface, from its descriptor. "UNKNOWN" for a name the registry does not have. */
getVersion() {
switch (this.type) {
case AlexaInterfaceType.APPLICATION_STATE_REPORTER:
case AlexaInterfaceType.AUDIO_PLAY_QUEUE:
case AlexaInterfaceType.AUTHORIZATION_CONTROLLER:
case AlexaInterfaceType.AUTOMATION_MANAGEMENT:
case AlexaInterfaceType.AUTOMOTIVE_VEHICLE_DATA:
case AlexaInterfaceType.COMMISSIONABLE:
case AlexaInterfaceType.CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER:
case AlexaInterfaceType.COOKING:
case AlexaInterfaceType.COOKING_FOOD_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType.COOKING_FOOD_TEMPERATURE_SENSOR:
case AlexaInterfaceType.COOKING_PRESET_CONTROLLER:
case AlexaInterfaceType.COOKING_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType.COOKING_TEMPERATURE_SENSOR:
case AlexaInterfaceType.COOKING_TIME_CONTROLLER:
case AlexaInterfaceType.DATA_CONTROLLER:
case AlexaInterfaceType.DEVICE_USAGE_ESTIMATION:
case AlexaInterfaceType.DEVICE_USAGE_METER:
case AlexaInterfaceType.EQUALIZER_CONTROLLER:
case AlexaInterfaceType.INVENTORY_LEVEL_SENSOR:
case AlexaInterfaceType.INVENTORY_LEVEL_USAGE_SENSOR:
case AlexaInterfaceType.INVENTORY_USAGE_SENSOR:
case AlexaInterfaceType.KEYPAD_CONTROLLER:
case AlexaInterfaceType.MEDIA_PLAYBACK:
case AlexaInterfaceType.MEDIA_PLAY_QUEUE:
case AlexaInterfaceType.MEDIA_SEARCH:
case AlexaInterfaceType.PLAYBACK_STATE_REPORTER:
case AlexaInterfaceType.PROACTIVE_NOTIFICATION_SOURCE:
case AlexaInterfaceType.REMOTE_VIDEO_PLAYER:
case AlexaInterfaceType.RTC_SESSION_CONTROLLER:
case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER:
case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER_ALERT:
case AlexaInterfaceType.SIMPLE_EVENT_SOURCE:
case AlexaInterfaceType.SMART_VISION_OBJECT_DETECTION_SENSOR:
case AlexaInterfaceType.SMART_VISION_SNAPSHOT_PROVIDER:
case AlexaInterfaceType.SPEAKER:
case AlexaInterfaceType.STEP_SPEAKER:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_CONFIGURATION:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_HVAC_COMPONENTS:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_SCHEDULE:
case AlexaInterfaceType.UI_CONTROLLER:
case AlexaInterfaceType.USER_PREFERENCE:
case AlexaInterfaceType.VIDEO_RECORDER:
case AlexaInterfaceType.WAKE_ON_LAN_CONTROLLER:
return "1";
case AlexaInterfaceType.BRIGHTNESS_CONTROLLER:
case AlexaInterfaceType.CAMERA_STREAM_CONTROLLER:
case AlexaInterfaceType.CHANNEL_CONTROLLER:
case AlexaInterfaceType.COLOR_CONTROLLER:
case AlexaInterfaceType.COLOR_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType.CONTACT_SENSOR:
case AlexaInterfaceType.DOORBELL_EVENT_SOURCE:
case AlexaInterfaceType.INPUT_CONTROLLER:
case AlexaInterfaceType.LOCK_CONTROLLER:
case AlexaInterfaceType.MODE_CONTROLLER:
case AlexaInterfaceType.MOTION_SENSOR:
case AlexaInterfaceType.PERCENTAGE_CONTROLLER:
case AlexaInterfaceType.PLAYBACK_CONTROLLER:
case AlexaInterfaceType.POWER_CONTROLLER:
case AlexaInterfaceType.POWER_LEVEL_CONTROLLER:
case AlexaInterfaceType.RANGE_CONTROLLER:
case AlexaInterfaceType.RECORD_CONTROLLER:
case AlexaInterfaceType.SCENE_CONTROLLER:
case AlexaInterfaceType.SEEK_CONTROLLER:
case AlexaInterfaceType.TEMPERATURE_SENSOR:
case AlexaInterfaceType.TOGGLE_CONTROLLER:
return "3";
case AlexaInterfaceType.LAUNCHER:
return "1.1";
case AlexaInterfaceType.THERMOSTAT_CONTROLLER:
return "3.2";
case AlexaInterfaceType.ENDPOINT_HEALTH:
return "3.3";
default:
return "UNKNOWN";
}
return registry.has(this.type) ? registry.get(this.type).version : "UNKNOWN";
}
/** The names of the properties the interface reports, from its descriptor. */
getProps() {
switch (this.type) {
case AlexaInterfaceType.AUTOMATION_MANAGEMENT:
return ["automationStatuses"];
case AlexaInterfaceType.BRIGHTNESS_CONTROLLER:
return ["brightness"];
case AlexaInterfaceType.CHANNEL_CONTROLLER:
return ["channel"];
case AlexaInterfaceType.COLOR_CONTROLLER:
return ["color"];
case AlexaInterfaceType.COLOR_TEMPERATURE_CONTROLLER:
return ["colorTemperatureInKelvin"];
case AlexaInterfaceType.CONTACT_SENSOR:
return ["detectionState"];
case AlexaInterfaceType.POWER_CONTROLLER:
return ["powerState"];
case AlexaInterfaceType.ENDPOINT_HEALTH:
return ["connectivity"];
case AlexaInterfaceType.TOGGLE_CONTROLLER:
return ["toggleState"];
case AlexaInterfaceType.MODE_CONTROLLER:
return ["mode"];
case AlexaInterfaceType.TEMPERATURE_SENSOR:
return ["temperature"];
case AlexaInterfaceType.THERMOSTAT_CONTROLLER:
return [
"targetSetpoint",
"lowerSetpoint",
"upperSetpoint",
"thermostatMode",
];
case AlexaInterfaceType.APPLICATION_STATE_REPORTER:
case AlexaInterfaceType.AUDIO_PLAY_QUEUE:
case AlexaInterfaceType.AUTHORIZATION_CONTROLLER:
case AlexaInterfaceType.AUTOMOTIVE_VEHICLE_DATA:
case AlexaInterfaceType.CAMERA_LIVE_VIEW_CONTROLLER:
case AlexaInterfaceType.CAMERA_STREAM_CONTROLLER:
case AlexaInterfaceType.COMMISSIONABLE:
case AlexaInterfaceType.CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER:
case AlexaInterfaceType.COOKING:
case AlexaInterfaceType.DATA_CONTROLLER:
case AlexaInterfaceType.DEVICE_USAGE_ESTIMATION:
case AlexaInterfaceType.DEVICE_USAGE_METER:
case AlexaInterfaceType.DOORBELL_EVENT_SOURCE:
case AlexaInterfaceType.EQUALIZER_CONTROLLER:
case AlexaInterfaceType.INPUT_CONTROLLER:
case AlexaInterfaceType.INVENTORY_LEVEL_SENSOR:
case AlexaInterfaceType.INVENTORY_LEVEL_USAGE_SENSOR:
case AlexaInterfaceType.INVENTORY_USAGE_SENSOR:
case AlexaInterfaceType.KEYPAD_CONTROLLER:
case AlexaInterfaceType.LAUNCHER:
case AlexaInterfaceType.LOCK_CONTROLLER:
case AlexaInterfaceType.MEDIA_PLAYBACK:
case AlexaInterfaceType.MEDIA_SEARCH:
case AlexaInterfaceType.MOTION_SENSOR:
case AlexaInterfaceType.PERCENTAGE_CONTROLLER:
case AlexaInterfaceType.PLAYBACK_CONTROLLER:
case AlexaInterfaceType.PLAYBACK_STATE_REPORTER:
case AlexaInterfaceType.PROACTIVE_NOTIFICATION_SOURCE:
case AlexaInterfaceType.RANGE_CONTROLLER:
case AlexaInterfaceType.RECORD_CONTROLLER:
case AlexaInterfaceType.REMOTE_VIDEO_PLAYER:
case AlexaInterfaceType.RTC_SESSION_CONTROLLER:
case AlexaInterfaceType.SCENE_CONTROLLER:
return []; // scenes have no reportable properties: Activate/Deactivate answer with ActivationStarted (Device.sendSceneResponse)
case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER:
case AlexaInterfaceType.SEEK_CONTROLLER:
case AlexaInterfaceType.SIMPLE_EVENT_SOURCE:
case AlexaInterfaceType.SMART_VISION_OBJECT_DETECTION_SENSOR:
case AlexaInterfaceType.SMART_VISION_SNAPSHOT_PROVIDER:
case AlexaInterfaceType.SPEAKER:
case AlexaInterfaceType.STEP_SPEAKER:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_CONFIGURATION:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_HVAC_COMPONENTS:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_SCHEDULE:
case AlexaInterfaceType.TIME_HOLD_CONTROLLER:
case AlexaInterfaceType.UI_CONTROLLER:
case AlexaInterfaceType.USER_PREFERENCE:
case AlexaInterfaceType.VIDEO_RECORDER:
case AlexaInterfaceType.WAKE_ON_LAN_CONTROLLER:
// no property list known for these: discovery lists the capability with an empty "supported" (the bridge notes
// it through its log hook; 1.5.1 wrote "UNSUPORTED INTERFACE TYPE" to stderr on every discovery)
return [];
default:
return [];
}
return registry.has(this.type) ? Object.keys(registry.get(this.type).properties) : [];
}
getJSON() {
const doc = {

View file

@ -51,6 +51,8 @@ export declare enum DisplayCategory {
THERMOSTAT = "THERMOSTAT",
TV = "TV",
VACUUM_CLEANER = "VACUUM_CLEANER",
VACUUM = "VACUUM",
/** Not on the list of display categories any more; kept for 1.x callers. */
VEHICLE = "VEHICLE",
WASHER = "WASHER",
WATER_HEATER = "WATER_HEATER",

View file

@ -52,6 +52,8 @@ export var DisplayCategory;
DisplayCategory["THERMOSTAT"] = "THERMOSTAT";
DisplayCategory["TV"] = "TV";
DisplayCategory["VACUUM_CLEANER"] = "VACUUM_CLEANER";
DisplayCategory["VACUUM"] = "VACUUM";
/** Not on the list of display categories any more; kept for 1.x callers. */
DisplayCategory["VEHICLE"] = "VEHICLE";
DisplayCategory["WASHER"] = "WASHER";
DisplayCategory["WATER_HEATER"] = "WATER_HEATER";

3
dist/esm/index.d.ts vendored
View file

@ -8,3 +8,6 @@ export { DisplayCategory } from "./DisplayCategory.js";
export { AlexaStatusMessage, PowerController, EndpointHealth, TemperatureSensorScale, ThermostatMode } from "./AlexaStatusMessage.js";
export type { ChangeCause } from "./AlexaStatusMessage.js";
export { AlexaErrorType, AlexaErrorResponse } from "./AlexaErrorResponse.js";
export { registry, DeclarationError, SchemaError } from "./registry/index.js";
export { ASSETS as Assets, UNITS_OF_MEASURE as Units, ACTIONS as Actions, STATES as States, DISPLAY_CATEGORIES as DisplayCategories, } from "./registry/index.js";
export type { ActionId, AssetId, DisplayCategoryName, StateId, UnitOfMeasure, AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor, Label, PropertyDescriptor, Infer, Schema, Temperature, TimeInterval, } from "./registry/index.js";

3
dist/esm/index.js vendored
View file

@ -6,3 +6,6 @@ export { ActionMapping, AlexaActions } from "./ActionMapping.js";
export { DisplayCategory } from "./DisplayCategory.js";
export { AlexaStatusMessage, PowerController, EndpointHealth, TemperatureSensorScale, ThermostatMode } from "./AlexaStatusMessage.js";
export { AlexaErrorType, AlexaErrorResponse } from "./AlexaErrorResponse.js";
// The interface registry and the vocabularies of the Smart Home API
export { registry, DeclarationError, SchemaError } from "./registry/index.js";
export { ASSETS as Assets, UNITS_OF_MEASURE as Units, ACTIONS as Actions, STATES as States, DISPLAY_CATEGORIES as DisplayCategories, } from "./registry/index.js";

40
dist/esm/registry/catalog.d.ts vendored Normal file
View file

@ -0,0 +1,40 @@
/**
* The asset ids a friendly name can refer to: 103, the units of measure among them
* (resources-and-assets.html, "Global Alexa catalog").
*/
export declare const ASSETS: readonly ["Alexa.Actions.Charge", "Alexa.Actions.Clean", "Alexa.Actions.Dispense", "Alexa.Actions.Dock", "Alexa.Actions.Empty", "Alexa.Actions.Mop", "Alexa.Actions.Skip", "Alexa.Actions.Sweep", "Alexa.Actions.Vacuum", "Alexa.Button.OffButton", "Alexa.Button.OnButton", "Alexa.Button.BrightenButton", "Alexa.Button.DimButton", "Alexa.Button.MainButton", "Alexa.Button.TopButton", "Alexa.Button.BottomButton", "Alexa.Button.CenterButton", "Alexa.Button.MiddleButton", "Alexa.Button.UpButton", "Alexa.Button.DownButton", "Alexa.Button.LeftButton", "Alexa.Button.RightButton", "Alexa.Button.FirstButton", "Alexa.Button.SecondButton", "Alexa.Button.ThirdButton", "Alexa.Button.FourthButton", "Alexa.Button.FifthButton", "Alexa.Button.SixthButton", "Alexa.Button.SeventhButton", "Alexa.Button.EighthButton", "Alexa.Button.DoublePress", "Alexa.Button.DoublePush", "Alexa.Button.LongPress", "Alexa.Button.LongPush", "Alexa.Button.SinglePress", "Alexa.Button.SinglePush", "Alexa.DeviceName.AirPurifier", "Alexa.DeviceName.Camera", "Alexa.DeviceName.Fan", "Alexa.DeviceName.Router", "Alexa.DeviceName.Shade", "Alexa.DeviceName.Shower", "Alexa.DeviceName.SpaceHeater", "Alexa.DeviceName.Washer", "Alexa.Gesture.DoubleClick", "Alexa.Gesture.SingleClick", "Alexa.Gesture.SwipeDown", "Alexa.Gesture.SwipeLeft", "Alexa.Gesture.SwipeRight", "Alexa.Gesture.SwipeUp", "Alexa.Gesture.Tap", "Alexa.Gestures.DoubleTap", "Alexa.Setting.2GGuestWiFi", "Alexa.Setting.5GGuestWiFi", "Alexa.Setting.Auto", "Alexa.Setting.Direction", "Alexa.Setting.DryCycle", "Alexa.Setting.FanSpeed", "Alexa.Setting.GuestWiFi", "Alexa.Setting.Heat", "Alexa.Setting.Mode", "Alexa.Setting.Night", "Alexa.Setting.Opening", "Alexa.Setting.Oscillate", "Alexa.Setting.Preset", "Alexa.Setting.Quiet", "Alexa.Setting.Temperature", "Alexa.Setting.WashCycle", "Alexa.Setting.WaterTemperature", "Alexa.Shower.HandHeld", "Alexa.Shower.RainHead", "Alexa.Unit.Angle.Degrees", "Alexa.Unit.Angle.Radians", "Alexa.Unit.Distance.Feet", "Alexa.Unit.Distance.Inches", "Alexa.Unit.Distance.Kilometers", "Alexa.Unit.Distance.Meters", "Alexa.Unit.Distance.Miles", "Alexa.Unit.Distance.Yards", "Alexa.Unit.Mass.Grams", "Alexa.Unit.Mass.Kilograms", "Alexa.Unit.Percent", "Alexa.Unit.Temperature.Celsius", "Alexa.Unit.Temperature.Degrees", "Alexa.Unit.Temperature.Fahrenheit", "Alexa.Unit.Temperature.Kelvin", "Alexa.Unit.Volume.CubicFeet", "Alexa.Unit.Volume.CubicMeters", "Alexa.Unit.Volume.Gallons", "Alexa.Unit.Volume.Liters", "Alexa.Unit.Volume.Pints", "Alexa.Unit.Volume.Quarts", "Alexa.Unit.Weight.Ounces", "Alexa.Unit.Weight.Pounds", "Alexa.Value.Close", "Alexa.Value.Delicate", "Alexa.Value.High", "Alexa.Value.Low", "Alexa.Value.Maximum", "Alexa.Value.Medium", "Alexa.Value.Minimum", "Alexa.Value.Open", "Alexa.Value.QuickWash"];
export type AssetId = (typeof ASSETS)[number];
/** A unit of measure: the 23 Alexa.Unit.* assets, what a RangeController's unitOfMeasure takes. */
export type UnitOfMeasure = Extract<AssetId, `Alexa.Unit.${string}`>;
export declare const UNITS_OF_MEASURE: UnitOfMeasure[];
/** The phrases an action mapping gives to a directive (alexa-discovery-objects.html, "ActionMappings object"). */
export declare const ACTIONS: readonly ["Alexa.Actions.Open", "Alexa.Actions.Close", "Alexa.Actions.Raise", "Alexa.Actions.Lower", "Alexa.Actions.SetEcoOn", "Alexa.Actions.SetEcoOff"];
export type ActionId = (typeof ACTIONS)[number];
/** The states a state mapping gives to a property value (alexa-discovery-objects.html, "StateMappings object"). */
export declare const STATES: readonly ["Alexa.States.Open", "Alexa.States.Closed", "Alexa.States.EcoOn", "Alexa.States.EcoOff", "Alexa.States.Low", "Alexa.States.Empty", "Alexa.States.Full", "Alexa.States.Done", "Alexa.States.Stuck"];
export type StateId = (typeof STATES)[number];
/**
* The 56 display categories (alexa-discovery.html, "Display categories"). The DisplayCategory enum also keeps
* VEHICLE from 1.x, which the page no longer lists.
*/
export declare const DISPLAY_CATEGORIES: readonly ["ACTIVITY_TRIGGER", "AIR_CONDITIONER", "AIR_FRESHENER", "AIR_PURIFIER", "AIR_QUALITY_MONITOR", "ALEXA_VOICE_ENABLED", "AUTO_ACCESSORY", "BLUETOOTH_SPEAKER", "CAMERA", "CHRISTMAS_TREE", "COFFEE_MAKER", "COMPUTER", "CONTACT_SENSOR", "DISHWASHER", "DOOR", "DOORBELL", "DRYER", "EXTERIOR_BLIND", "FAN", "GAME_CONSOLE", "GARAGE_DOOR", "HEADPHONES", "HUB", "INTERIOR_BLIND", "LAPTOP", "LIGHT", "MICROWAVE", "MOBILE_PHONE", "MOTION_SENSOR", "MUSIC_SYSTEM", "NETWORK_HARDWARE", "OTHER", "OVEN", "PHONE", "PRINTER", "REMOTE", "ROUTER", "SCENE_TRIGGER", "SCREEN", "SECURITY_PANEL", "SECURITY_SYSTEM", "SLOW_COOKER", "SMARTLOCK", "SMARTPLUG", "SPEAKER", "STREAMING_DEVICE", "SWITCH", "TABLET", "TEMPERATURE_SENSOR", "THERMOSTAT", "TV", "VACUUM_CLEANER", "VACUUM", "WASHER", "WATER_HEATER", "WEARABLE"];
export type DisplayCategoryName = (typeof DISPLAY_CATEGORIES)[number];
/** Not to be used as a friendly name (resources-and-assets.html, "Reserved words"). */
export declare const RESERVED_WORDS: readonly ["alarm", "alarms", "all alarms", "away mode", "bass", "camera", "date", "date today", "day", "do not disturb", "drop in", "music", "night light", "notification", "playing", "sleep sounds", "time", "timer", "today in music", "treble", "volume", "way f. m."];
/**
* What a discovery answer may hold (alexa-discovery.html, "Interface limits"; alexa-discovery-objects.html,
* "Endpoint object details" and "AdditionalAttributes object details"; alexa-scenecontroller.html, "Discovery").
*/
export declare const LIMITS: {
readonly endpointsPerCustomer: 300;
readonly capabilitiesPerEndpoint: 100;
readonly endpointIdLength: 256;
readonly friendlyNameLength: 256;
readonly sceneFriendlyNameLength: 128;
readonly manufacturerNameLength: 128;
readonly descriptionLength: 128;
readonly additionalAttributeLength: 256;
readonly cookieBytes: 5000;
};
/** The 73 error types of the table, each with the namespace its ErrorResponse goes under. */
export declare const ERROR_TYPES: Readonly<Record<string, string>>;

129
dist/esm/registry/catalog.js vendored Normal file
View file

@ -0,0 +1,129 @@
// The fixed vocabularies of the Smart Home API, copied from Amazon's pages as read on 2026-09-28. The pages are under
// https://developer.amazon.com/docs/alexaplus/device-apis/. A value missing here is one Amazon added since.
/**
* The asset ids a friendly name can refer to: 103, the units of measure among them
* (resources-and-assets.html, "Global Alexa catalog").
*/
export const ASSETS = [
"Alexa.Actions.Charge", "Alexa.Actions.Clean", "Alexa.Actions.Dispense", "Alexa.Actions.Dock",
"Alexa.Actions.Empty", "Alexa.Actions.Mop", "Alexa.Actions.Skip", "Alexa.Actions.Sweep", "Alexa.Actions.Vacuum",
"Alexa.Button.OffButton", "Alexa.Button.OnButton", "Alexa.Button.BrightenButton", "Alexa.Button.DimButton",
"Alexa.Button.MainButton", "Alexa.Button.TopButton", "Alexa.Button.BottomButton", "Alexa.Button.CenterButton",
"Alexa.Button.MiddleButton", "Alexa.Button.UpButton", "Alexa.Button.DownButton", "Alexa.Button.LeftButton",
"Alexa.Button.RightButton", "Alexa.Button.FirstButton", "Alexa.Button.SecondButton", "Alexa.Button.ThirdButton",
"Alexa.Button.FourthButton", "Alexa.Button.FifthButton", "Alexa.Button.SixthButton", "Alexa.Button.SeventhButton",
"Alexa.Button.EighthButton", "Alexa.Button.DoublePress", "Alexa.Button.DoublePush", "Alexa.Button.LongPress",
"Alexa.Button.LongPush", "Alexa.Button.SinglePress", "Alexa.Button.SinglePush",
"Alexa.DeviceName.AirPurifier", "Alexa.DeviceName.Camera", "Alexa.DeviceName.Fan", "Alexa.DeviceName.Router",
"Alexa.DeviceName.Shade", "Alexa.DeviceName.Shower", "Alexa.DeviceName.SpaceHeater", "Alexa.DeviceName.Washer",
"Alexa.Gesture.DoubleClick", "Alexa.Gesture.SingleClick", "Alexa.Gesture.SwipeDown", "Alexa.Gesture.SwipeLeft",
"Alexa.Gesture.SwipeRight", "Alexa.Gesture.SwipeUp", "Alexa.Gesture.Tap",
// "Gestures", plural: the page spells this one id differently from the seven above
"Alexa.Gestures.DoubleTap",
"Alexa.Setting.2GGuestWiFi", "Alexa.Setting.5GGuestWiFi", "Alexa.Setting.Auto", "Alexa.Setting.Direction",
"Alexa.Setting.DryCycle", "Alexa.Setting.FanSpeed", "Alexa.Setting.GuestWiFi", "Alexa.Setting.Heat",
"Alexa.Setting.Mode", "Alexa.Setting.Night", "Alexa.Setting.Opening", "Alexa.Setting.Oscillate",
"Alexa.Setting.Preset", "Alexa.Setting.Quiet", "Alexa.Setting.Temperature", "Alexa.Setting.WashCycle",
"Alexa.Setting.WaterTemperature",
"Alexa.Shower.HandHeld", "Alexa.Shower.RainHead",
"Alexa.Unit.Angle.Degrees", "Alexa.Unit.Angle.Radians", "Alexa.Unit.Distance.Feet", "Alexa.Unit.Distance.Inches",
"Alexa.Unit.Distance.Kilometers", "Alexa.Unit.Distance.Meters", "Alexa.Unit.Distance.Miles",
"Alexa.Unit.Distance.Yards", "Alexa.Unit.Mass.Grams", "Alexa.Unit.Mass.Kilograms", "Alexa.Unit.Percent",
"Alexa.Unit.Temperature.Celsius", "Alexa.Unit.Temperature.Degrees", "Alexa.Unit.Temperature.Fahrenheit",
"Alexa.Unit.Temperature.Kelvin", "Alexa.Unit.Volume.CubicFeet", "Alexa.Unit.Volume.CubicMeters",
"Alexa.Unit.Volume.Gallons", "Alexa.Unit.Volume.Liters", "Alexa.Unit.Volume.Pints", "Alexa.Unit.Volume.Quarts",
"Alexa.Unit.Weight.Ounces", "Alexa.Unit.Weight.Pounds",
"Alexa.Value.Close", "Alexa.Value.Delicate", "Alexa.Value.High", "Alexa.Value.Low", "Alexa.Value.Maximum",
"Alexa.Value.Medium", "Alexa.Value.Minimum", "Alexa.Value.Open", "Alexa.Value.QuickWash",
];
export const UNITS_OF_MEASURE = ASSETS.filter((id) => id.startsWith("Alexa.Unit."));
/** The phrases an action mapping gives to a directive (alexa-discovery-objects.html, "ActionMappings object"). */
export const ACTIONS = [
"Alexa.Actions.Open", "Alexa.Actions.Close", "Alexa.Actions.Raise", "Alexa.Actions.Lower",
"Alexa.Actions.SetEcoOn", "Alexa.Actions.SetEcoOff",
];
/** The states a state mapping gives to a property value (alexa-discovery-objects.html, "StateMappings object"). */
export const STATES = [
"Alexa.States.Open", "Alexa.States.Closed", "Alexa.States.EcoOn", "Alexa.States.EcoOff", "Alexa.States.Low",
"Alexa.States.Empty", "Alexa.States.Full", "Alexa.States.Done", "Alexa.States.Stuck",
];
/**
* The 56 display categories (alexa-discovery.html, "Display categories"). The DisplayCategory enum also keeps
* VEHICLE from 1.x, which the page no longer lists.
*/
export const DISPLAY_CATEGORIES = [
"ACTIVITY_TRIGGER", "AIR_CONDITIONER", "AIR_FRESHENER", "AIR_PURIFIER", "AIR_QUALITY_MONITOR",
"ALEXA_VOICE_ENABLED", "AUTO_ACCESSORY", "BLUETOOTH_SPEAKER", "CAMERA", "CHRISTMAS_TREE", "COFFEE_MAKER",
"COMPUTER", "CONTACT_SENSOR", "DISHWASHER", "DOOR", "DOORBELL", "DRYER", "EXTERIOR_BLIND", "FAN", "GAME_CONSOLE",
"GARAGE_DOOR", "HEADPHONES", "HUB", "INTERIOR_BLIND", "LAPTOP", "LIGHT", "MICROWAVE", "MOBILE_PHONE",
"MOTION_SENSOR", "MUSIC_SYSTEM", "NETWORK_HARDWARE", "OTHER", "OVEN", "PHONE", "PRINTER", "REMOTE", "ROUTER",
"SCENE_TRIGGER", "SCREEN", "SECURITY_PANEL", "SECURITY_SYSTEM", "SLOW_COOKER", "SMARTLOCK", "SMARTPLUG", "SPEAKER",
"STREAMING_DEVICE", "SWITCH", "TABLET", "TEMPERATURE_SENSOR", "THERMOSTAT", "TV", "VACUUM_CLEANER", "VACUUM",
"WASHER", "WATER_HEATER", "WEARABLE",
];
/** Not to be used as a friendly name (resources-and-assets.html, "Reserved words"). */
export const RESERVED_WORDS = [
"alarm", "alarms", "all alarms", "away mode", "bass", "camera", "date", "date today", "day", "do not disturb",
"drop in", "music", "night light", "notification", "playing", "sleep sounds", "time", "timer", "today in music",
"treble", "volume", "way f. m.",
];
/**
* What a discovery answer may hold (alexa-discovery.html, "Interface limits"; alexa-discovery-objects.html,
* "Endpoint object details" and "AdditionalAttributes object details"; alexa-scenecontroller.html, "Discovery").
*/
export const LIMITS = {
endpointsPerCustomer: 300,
capabilitiesPerEndpoint: 100,
endpointIdLength: 256,
friendlyNameLength: 256,
sceneFriendlyNameLength: 128,
manufacturerNameLength: 128,
descriptionLength: 128,
additionalAttributeLength: 256,
cookieBytes: 5000,
};
// alexa-errorresponse.html, "Error type values". The Interface column names <namespace>.ErrorResponse; the event
// header carries the namespace and the name ErrorResponse, as the examples on the ThermostatController,
// SecurityPanelController and Safety error pages show. The other namespaces are as the table spells them.
// INVALID_VALUE is listed under Alexa and under SmartVision.ObjectDetectionSensor, SUBSCRIPTION_REQUIRED under both
// SmartVision interfaces: the first one listed is kept.
const ERROR_TYPES_BY_NAMESPACE = {
"Alexa": [
"ALREADY_IN_OPERATION", "BRIDGE_UNREACHABLE", "CLOUD_CONTROL_DISABLED", "DEVICE_STUCK", "DO_NOT_DISTURB_MODE",
"ENDPOINT_BUSY", "ENDPOINT_CONTROL_UNAVAILABLE", "ENDPOINT_LOW_POWER", "ENDPOINT_UNREACHABLE",
"EXPIRED_AUTHORIZATION_CREDENTIAL", "FIRMWARE_OUT_OF_DATE", "HARDWARE_MALFUNCTION", "INSUFFICIENT_PERMISSIONS",
"INSUFFICIENT_RESOURCE", "INTERNAL_ERROR", "INVALID_AUTHORIZATION_CREDENTIAL", "INVALID_DIRECTIVE",
"INVALID_VALUE", "MAINTENANCE_REQUIRED", "NO_SUCH_ENDPOINT", "NOT_CALIBRATED", "NOT_IN_OPERATION",
"NOT_SUPPORTED_IN_CURRENT_MODE", "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE",
"PARTNER_APPLICATION_REDIRECTION", "POWER_LEVEL_NOT_SUPPORTED", "RATE_LIMIT_EXCEEDED",
"TEMPERATURE_VALUE_OUT_OF_RANGE", "TOO_MANY_FAILED_ATTEMPTS", "UNABLE_TO_CHARGE", "VALUE_OUT_OF_RANGE",
],
"Alexa.Commissionable.ReportCommissioningInformation": [
"FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS", "MAX_COMMISSIONING_LIMIT_REACHED",
],
"Alexa.Cooking": [
"CHILD_LOCK", "COOK_DURATION_TOO_LONG", "DOOR_CLOSED_TOO_LONG", "DOOR_OPEN", "PREHEAT_REQUIRED",
"PROBE_REQUIRED", "REMOTE_START_NOT_SUPPORTED", "REMOVE_PROBE", "REMOTE_START_DISABLED",
],
"Alexa.DataController": ["DATA_DELETION_NOT_SUPPORTED", "DATA_RETRIEVAL_NOT_SUPPORTED"],
"Alexa.Safety": ["OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED"],
"Alexa.SecurityPanelController": [
"AUTHORIZATION_REQUIRED", "BYPASS_NEEDED", "NOT_READY", "UNAUTHORIZED", "UNCLEARED_ALARM", "UNCLEARED_TROUBLE",
],
"Alexa.SmartVision.ObjectDetectionSensor": ["SUBSCRIPTION_REQUIRED"],
"Alexa.SmartVision.SnapshotProvider": ["DISABLED_BY_USER"],
"Alexa.ThermostatController": [
"DUAL_SETPOINTS_UNSUPPORTED", "REQUESTED_SETPOINTS_TOO_CLOSE", "THERMOSTAT_IS_OFF",
"TRIPLE_SETPOINTS_UNSUPPORTED", "UNSUPPORTED_THERMOSTAT_MODE", "UNWILLING_TO_SET_SCHEDULE",
"UNWILLING_TO_SET_VALUE",
],
"Alexa.ThermostatController.Configuration": [
"CONFIGURATION_UPDATE_NOT_ALLOWED", "COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE",
"COOLING_STAGES_EXCEEDS_LIMIT", "HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", "HEATING_STAGES_EXCEEDS_LIMIT",
"INVALID_AUXILIARY_HEATING_SYSTEM_TYPE", "INVALID_SYSTEM_TYPE", "INVALID_TARGET_STATE",
"INVALID_TEMPERATURE_SCALE", "INVALID_TERMINAL_CONNECTION", "MISSING_SETUP_INFORMATION",
],
"Alexa.ThermostatController.Schedule": ["INSUFFICIENT_SPACE"],
};
/** The 73 error types of the table, each with the namespace its ErrorResponse goes under. */
export const ERROR_TYPES = Object.fromEntries(Object.entries(ERROR_TYPES_BY_NAMESPACE).flatMap(([namespace, types]) => types.map((type) => [type, namespace])));

20
dist/esm/registry/index.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
import { Alexa } from "./interfaces/Alexa.js";
import { BrightnessController } from "./interfaces/BrightnessController.js";
import { EndpointHealth } from "./interfaces/EndpointHealth.js";
import { PowerController } from "./interfaces/PowerController.js";
import { TemperatureSensor } from "./interfaces/TemperatureSensor.js";
import type { AnyDescriptor } from "./types.js";
export declare const registry: {
/** Whether an interface of this name is known. */
has(namespace: string): boolean;
/** The descriptor of "Alexa.RangeController" or of AlexaInterfaceType.RANGE_CONTROLLER, which is that string. */
get(namespace: string): AnyDescriptor;
/** Every descriptor, ordered by namespace. */
list(): AnyDescriptor[];
};
export { Alexa, BrightnessController, EndpointHealth, PowerController, TemperatureSensor };
export { DeclarationError, defineInterface } from "./types.js";
export type { AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor, Label, PropertyDescriptor, } from "./types.js";
export { s, SchemaError } from "./schema.js";
export type { Infer, Schema, Temperature, TimeInterval } from "./schema.js";
export * from "./catalog.js";

43
dist/esm/registry/index.js vendored Normal file
View file

@ -0,0 +1,43 @@
// The interfaces the library knows, by namespace.
import { Alexa } from "./interfaces/Alexa.js";
import { BrightnessController } from "./interfaces/BrightnessController.js";
import { EndpointHealth } from "./interfaces/EndpointHealth.js";
import { PowerController } from "./interfaces/PowerController.js";
import { TemperatureSensor } from "./interfaces/TemperatureSensor.js";
import { STUBS } from "./interfaces/stubs.js";
import { DeclarationError } from "./types.js";
const described = [
Alexa,
BrightnessController,
EndpointHealth,
PowerController,
TemperatureSensor,
];
const descriptors = new Map();
for (const descriptor of [...described, ...STUBS]) {
// A stub left in the table next to the descriptor that replaces it would win or lose by the order of this list
if (descriptors.has(descriptor.namespace))
throw new Error(`${descriptor.namespace} is described twice`);
descriptors.set(descriptor.namespace, descriptor);
}
export const registry = {
/** Whether an interface of this name is known. */
has(namespace) {
return descriptors.has(namespace);
},
/** The descriptor of "Alexa.RangeController" or of AlexaInterfaceType.RANGE_CONTROLLER, which is that string. */
get(namespace) {
const descriptor = descriptors.get(namespace);
if (!descriptor)
throw new DeclarationError({}, `${JSON.stringify(namespace)} is not an interface alex2node knows`);
return descriptor;
},
/** Every descriptor, ordered by namespace. */
list() {
return [...descriptors.values()].sort((a, b) => (a.namespace < b.namespace ? -1 : 1));
},
};
export { Alexa, BrightnessController, EndpointHealth, PowerController, TemperatureSensor };
export { DeclarationError, defineInterface } from "./types.js";
export { s, SchemaError } from "./schema.js";
export * from "./catalog.js";

10
dist/esm/registry/interfaces/Alexa.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
/**
* The base interface: every endpoint lists it (alexa-interface.html, "Support the Alexa interface in all add-ons").
* Its one directive to a device is ReportState, answered with a StateReport.
*/
export declare const Alexa: import("../types.js").InterfaceDescriptor<{}, {
ReportState: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{}>>;
};
}, {}, false>;

20
dist/esm/registry/interfaces/Alexa.js vendored Normal file
View file

@ -0,0 +1,20 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
/**
* The base interface: every endpoint lists it (alexa-interface.html, "Support the Alexa interface in all add-ons").
* Its one directive to a device is ReportState, answered with a StateReport.
*/
export const Alexa = defineInterface({
namespace: "Alexa",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-interface.html",
kind: "base",
tier: 1,
instanced: false,
properties: {},
directives: {
ReportState: { name: "ReportState", payload: s.object({}) },
},
// { type, interface, version } and nothing else, as in the discovery example of the page
discovery: () => ({ properties: false }),
});

View file

@ -0,0 +1,20 @@
/** Both directives turn a light that is off on, at the brightness asked for (alexa-brightnesscontroller.html). */
export declare const BrightnessController: import("../types.js").InterfaceDescriptor<{
brightness: {
name: string;
value: import("../schema.js").Schema<number>;
};
}, {
SetBrightness: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
brightness: import("../schema.js").Schema<number>;
}>>;
};
AdjustBrightness: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
brightnessDelta: import("../schema.js").Schema<number>;
}>>;
};
}, {}, false>;

View file

@ -0,0 +1,24 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
/** Both directives turn a light that is off on, at the brightness asked for (alexa-brightnesscontroller.html). */
export const BrightnessController = defineInterface({
namespace: "Alexa.BrightnessController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-brightnesscontroller.html",
kind: "controller",
tier: 1,
instanced: false,
properties: {
brightness: { name: "brightness", value: s.number({ min: 0, max: 100, integer: true }) },
},
directives: {
SetBrightness: {
name: "SetBrightness",
payload: s.object({ brightness: s.number({ min: 0, max: 100, integer: true }) }),
},
AdjustBrightness: {
name: "AdjustBrightness",
payload: s.object({ brightnessDelta: s.number({ min: -100, max: 100, integer: true }) }),
},
},
});

View file

@ -0,0 +1,15 @@
export declare const CONNECTIVITY_REASONS: readonly ["WIFI_BAD_PASSWORD", "WIFI_AP_NOT_FOUND", "WIFI_ROUTER_UNREACHABLE", "WIFI_AP_CHANNEL_QUALITY_LOW", "INTERNET_UNREACHABLE", "CAPTIVE_PORTAL_CHECK_FAILED", "UNKNOWN"];
/**
* Version 3.1, as the page is titled. It defines connectivity only. Examples on other pages declare 3 and 3.2;
* alex2node 1.x declared 3.3, which no page mentions.
*/
export declare const EndpointHealth: import("../types.js").InterfaceDescriptor<{
connectivity: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").InferShape<{
value: import("../schema.js").EnumSchema<"OK" | "UNREACHABLE">;
reason: import("../schema.js").OptionalSchema<"WIFI_BAD_PASSWORD" | "WIFI_AP_NOT_FOUND" | "WIFI_ROUTER_UNREACHABLE" | "WIFI_AP_CHANNEL_QUALITY_LOW" | "INTERNET_UNREACHABLE" | "CAPTIVE_PORTAL_CHECK_FAILED" | "UNKNOWN">;
}>>;
note: string;
};
}, {}, {}, false>;

View file

@ -0,0 +1,29 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
export const CONNECTIVITY_REASONS = [
"WIFI_BAD_PASSWORD", "WIFI_AP_NOT_FOUND", "WIFI_ROUTER_UNREACHABLE", "WIFI_AP_CHANNEL_QUALITY_LOW",
"INTERNET_UNREACHABLE", "CAPTIVE_PORTAL_CHECK_FAILED", "UNKNOWN",
];
/**
* Version 3.1, as the page is titled. It defines connectivity only. Examples on other pages declare 3 and 3.2;
* alex2node 1.x declared 3.3, which no page mentions.
*/
export const EndpointHealth = defineInterface({
namespace: "Alexa.EndpointHealth",
version: "3.1",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-endpointhealth.html",
kind: "sensor",
tier: 1,
instanced: false,
properties: {
connectivity: {
name: "connectivity",
value: s.object({
value: s.enum("OK", "UNREACHABLE"),
reason: s.optional(s.enum(...CONNECTIVITY_REASONS)),
}),
note: "in every Response, StateReport and ChangeReport; a change of it reported within three seconds",
},
},
directives: {},
});

View file

@ -0,0 +1,17 @@
export declare const PowerController: import("../types.js").InterfaceDescriptor<{
powerState: {
name: string;
value: import("../schema.js").EnumSchema<"ON" | "OFF">;
};
}, {
TurnOn: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{}>>;
};
TurnOff: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{}>>;
};
}, import("../schema.js").InferShape<{
verificationsRequired: import("../schema.js").OptionalSchema<("TurnOn" | "TurnOff")[]>;
}>, false>;

View file

@ -0,0 +1,28 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
export const PowerController = defineInterface({
namespace: "Alexa.PowerController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-powercontroller.html",
kind: "controller",
tier: 1,
instanced: false,
properties: {
powerState: { name: "powerState", value: s.enum("ON", "OFF") },
},
directives: {
TurnOn: { name: "TurnOn", payload: s.object({}) },
TurnOff: { name: "TurnOff", payload: s.object({}) },
},
options: s.object({
// The directives Alexa asks the user to confirm before it sends them. Amazon supports this for devices in Japan
// only (alexa-discovery-objects.html, "VerificationsRequired object").
verificationsRequired: s.optional(s.array(s.enum("TurnOn", "TurnOff"), { min: 1 })),
}, { unknownKeys: "reject" }),
discovery({ options }) {
if (!options.verificationsRequired)
return {};
const confirmed = options.verificationsRequired.map((directive) => ({ directive, methods: [{ "@type": "Confirmation" }] }));
return { topLevel: { verificationsRequired: confirmed } };
},
});

View file

@ -0,0 +1,7 @@
/** A sensor: an endpoint that has it declares Alexa.EndpointHealth as well (alexa-temperaturesensor.html). */
export declare const TemperatureSensor: import("../types.js").InterfaceDescriptor<{
temperature: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").Temperature>;
};
}, {}, {}, false>;

View file

@ -0,0 +1,15 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
/** A sensor: an endpoint that has it declares Alexa.EndpointHealth as well (alexa-temperaturesensor.html). */
export const TemperatureSensor = defineInterface({
namespace: "Alexa.TemperatureSensor",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-temperaturesensor.html",
kind: "sensor",
tier: 1,
instanced: false,
properties: {
temperature: { name: "temperature", value: s.temperature() },
},
directives: {},
});

View file

@ -0,0 +1,2 @@
import type { AnyDescriptor } from "../types.js";
export declare const STUBS: readonly AnyDescriptor[];

99
dist/esm/registry/interfaces/stubs.js vendored Normal file
View file

@ -0,0 +1,99 @@
// Interfaces without a descriptor of their own. A stub names the interface with the version and the properties
// alex2node 1.5.2 put in discovery, so an endpoint that declares one is announced as before. Nothing about it is
// checked, and the version is the one 1.5.2 sent, which is not always the one on the page of the interface
// (Alexa.Speaker is "3" there). A row leaves this table when a descriptor is written from that page.
import { s } from "../schema.js";
const DEVICE_APIS = "https://developer.amazon.com/docs/alexaplus/device-apis";
const LIST_OF_INTERFACES = `${DEVICE_APIS}/list-of-interfaces.html`;
// "As of August 7, 2026, Automotive skills and the associated Alexa.AuthorizationController and
// Alexa.Automotive.VehicleData APIs are no longer available."
const DEPRECATED_FEATURES = "https://developer.amazon.com/en-US/docs/alexa/ask-overviews/deprecated-features.html";
const TABLE = [
["Alexa.ApplicationStateReporter", "1", [], "https://developer.amazon.com/docs/alexaplus/alexa-voice-service/alexa-applicationstatereporter.html"],
["Alexa.Audio.PlayQueue", "1", [], "alexa-audio-playqueue.html"],
["Alexa.AuthorizationController", "1", [], DEPRECATED_FEATURES],
["Alexa.AutomationManagement", "1", ["automationStatuses"], "alexa-automationmanagement.html"],
["Alexa.Automotive.VehicleData", "1", [], DEPRECATED_FEATURES],
// 1.5.2 announced the version "UNKNOWN"; 1.7 is what the list of interfaces gives
["Alexa.Camera.LiveViewController", "1.7", [], LIST_OF_INTERFACES],
["Alexa.CameraStreamController", "3", [], "alexa-camerastreamcontroller.html"],
["Alexa.ChannelController", "3", ["channel"], "alexa-channelcontroller.html"],
["Alexa.ColorController", "3", ["color"], "alexa-colorcontroller.html"],
["Alexa.ColorTemperatureController", "3", ["colorTemperatureInKelvin"], "alexa-colortemperaturecontroller.html"],
["Alexa.Commissionable", "1", [], "alexa-commissionable.html"],
["Alexa.ConsentManagement.ConsentRequiredReporter", "1", [], "alexa-consentrequiredreporter.html"],
["Alexa.ContactSensor", "3", ["detectionState"], "alexa-contactsensor.html"],
["Alexa.Cooking", "1", [], "alexa-cooking.html"],
["Alexa.Cooking.FoodTemperatureController", "1", [], "alexa-cooking-foodtemperaturecontroller.html"],
["Alexa.Cooking.FoodTemperatureSensor", "1", [], "alexa-cooking-foodtemperaturesensor.html"],
["Alexa.Cooking.PresetController", "1", [], "alexa-cooking-presetcontroller.html"],
["Alexa.Cooking.TemperatureController", "1", [], "alexa-cooking-temperaturecontroller.html"],
["Alexa.Cooking.TemperatureSensor", "1", [], "alexa-cooking-temperaturesensor.html"],
["Alexa.Cooking.TimeController", "1", [], "alexa-cooking-timecontroller.html"],
["Alexa.DataController", "1", [], "alexa-datacontroller.html"],
["Alexa.DeviceUsage.Estimation", "1", [], "alexa-deviceusage-estimation.html"],
["Alexa.DeviceUsage.Meter", "1", [], "alexa-deviceusage-meter.html"],
["Alexa.DoorbellEventSource", "3", [], "alexa-doorbelleventsource.html"],
["Alexa.EqualizerController", "1", [], "alexa-equalizercontroller.html"],
["Alexa.InputController", "3", [], "alexa-inputcontroller.html"],
["Alexa.InventoryLevelSensor", "1", [], "alexa-inventorylevelsensor.html"],
["Alexa.InventoryLevelUsageSensor", "1", [], "alexa-inventorylevelusagesensor.html"],
["Alexa.InventoryUsageSensor", "1", [], "alexa-inventoryusagesensor.html"],
["Alexa.KeypadController", "1", [], "alexa-keypadcontroller.html"],
["Alexa.Launcher", "1.1", [], "alexa-launcher.html"],
["Alexa.LockController", "3", [], "alexa-lockcontroller.html"],
["Alexa.Media.PlayQueue", "1", [], "alexa-media-playqueue.html"],
["Alexa.Media.Playback", "1", [], "alexa-media-playback.html"],
["Alexa.Media.Search", "1", [], "alexa-media-search.html"],
["Alexa.ModeController", "3", ["mode"], "alexa-modecontroller.html"],
["Alexa.MotionSensor", "3", [], "alexa-motionsensor.html"],
["Alexa.PercentageController", "3", [], "alexa-percentagecontroller.html"],
["Alexa.PlaybackController", "3", [], "alexa-playbackcontroller.html"],
["Alexa.PlaybackStateReporter", "1", [], "alexa-playbackcontroller.html"],
["Alexa.PowerLevelController", "3", [], "alexa-powerlevelcontroller.html"],
["Alexa.ProactiveNotificationSource", "1", [], "alexa-proactivenotificationsource.html"],
["Alexa.RTCSessionController", "1", [], "alexa-rtcsessioncontroller.html"],
["Alexa.RangeController", "3", [], "alexa-rangecontroller.html"],
["Alexa.RecordController", "3", [], "alexa-recordcontroller.html"],
["Alexa.RemoteVideoPlayer", "1", [], "alexa-remotevideoplayer.html"],
["Alexa.SceneController", "3", [], "alexa-scenecontroller.html"],
["Alexa.SecurityPanelController", "1", [], "alexa-securitypanelcontroller.html"],
["Alexa.SecurityPanelController.Alert", "1", [], "alexa-securitypanelcontroller-alert.html"],
["Alexa.SeekController", "3", [], "alexa-seekcontroller.html"],
["Alexa.SimpleEventSource", "1", [], "alexa-simpleeventsource.html"],
["Alexa.SmartVision.ObjectDetectionSensor", "1", [], "alexa-smartvision-objectdetectionsensor.html"],
["Alexa.SmartVision.SnapshotProvider", "1", [], "alexa-smartvision-snapshotprovider.html"],
["Alexa.Speaker", "1", [], "alexa-speaker.html"],
["Alexa.StepSpeaker", "1", [], "alexa-stepspeaker.html"],
["Alexa.ThermostatController", "3.2", ["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"], "alexa-thermostatcontroller.html"],
["Alexa.ThermostatController.Configuration", "1", [], "alexa-thermostatcontroller-configuration.html"],
["Alexa.ThermostatController.HVAC.Components", "1", [], "alexa-thermostatcontroller-hvac-components.html"],
["Alexa.ThermostatController.Schedule", "1", [], "alexa-thermostatcontroller-schedule.html"],
// "UNKNOWN" in 1.5.2 as well; the page is titled "Interface 3"
["Alexa.TimeHoldController", "3", [], "alexa-timeholdcontroller.html"],
["Alexa.ToggleController", "3", ["toggleState"], "alexa-togglecontroller.html"],
["Alexa.UIController", "1", [], "alexa-uicontroller.html"],
["Alexa.UserPreference", "1", [], "alexa-userpreference.html"],
["Alexa.VideoRecorder", "1", [], "alexa-videorecorder.html"],
["Alexa.WakeOnLANController", "1", [], "alexa-wakeonlancontroller.html"],
];
function kindOf(namespace) {
if (namespace.endsWith("Sensor"))
return "sensor";
if (namespace.endsWith("EventSource"))
return "eventSource";
return "controller";
}
function stub([namespace, version, properties, page]) {
return {
namespace,
version,
doc: page.startsWith("https://") ? page : `${DEVICE_APIS}/${page}`,
kind: kindOf(namespace),
tier: 3,
instanced: false,
properties: Object.fromEntries(properties.map((name) => [name, { name, value: s.unknown() }])),
directives: {},
};
}
export const STUBS = TABLE.map(stub);

91
dist/esm/registry/schema.d.ts vendored Normal file
View file

@ -0,0 +1,91 @@
/** A value that does not fit its schema: path is where ("payload.targetSetpoint.scale"), problem is what. */
export declare class SchemaError extends Error {
readonly path: string;
readonly problem: string;
constructor(path: string, problem: string);
}
export interface Schema<T> {
/** What the schema accepts, in the words of an error message: "an integer from 0 to 100", "ON | OFF". */
readonly expects: string;
/** The checked value. path names it in the error message. */
parse(input: unknown, path?: string): T;
}
/** A schema that accepts a missing value; s.object() makes its key optional in the inferred type. */
export interface OptionalSchema<T> extends Schema<T | undefined> {
readonly optional: true;
}
export interface EnumSchema<V> extends Schema<V> {
readonly values: readonly V[];
}
/** The type a schema produces. */
export type Infer<S> = S extends Schema<infer T> ? T : never;
export type Shape = Record<string, Schema<any>>;
type OptionalKeys<S extends Shape> = {
[K in keyof S]: S[K] extends OptionalSchema<any> ? K : never;
}[keyof S];
export type InferShape<S extends Shape> = {
[K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]>;
} & {
[K in OptionalKeys<S>]?: Infer<S[K]>;
};
export interface NumberRules {
min?: number;
max?: number;
/** Greater than, the bound itself excluded. */
gt?: number;
integer?: boolean;
}
export interface Temperature {
value: number;
scale: "CELSIUS" | "FAHRENHEIT" | "KELVIN";
}
export interface TimeInterval {
start?: string;
end?: string;
duration?: string;
}
declare function number(rules?: NumberRules): Schema<number>;
declare function string(rules?: {
min?: number;
max?: number;
pattern?: RegExp;
expects?: string;
}): Schema<string>;
declare function boolean(): Schema<boolean>;
declare function literal<V extends string | number | boolean | null>(value: V): Schema<V>;
declare function enumeration<V extends readonly string[]>(...values: V): EnumSchema<V[number]>;
declare function unknown(): Schema<unknown>;
declare function optional<T>(inner: Schema<T>): OptionalSchema<T>;
declare function nullable<T>(inner: Schema<T>): Schema<T | null>;
declare function array<T>(item: Schema<T>, rules?: {
min?: number;
max?: number;
}): Schema<T[]>;
/**
* An object with the keys of shape. Keys the shape does not name are kept as they are: a field Alexa adds to a
* directive reaches the handler. unknownKeys "reject" is for what a developer writes, where such a key is a typo.
*/
declare function object<S extends Shape>(shape: S, rules?: {
unknownKeys?: "keep" | "reject";
}): Schema<InferShape<S>>;
declare function temperature(): Schema<Temperature>;
declare function dateTime(): Schema<string>;
declare function duration(): Schema<string>;
declare function timeInterval(): Schema<TimeInterval>;
export declare const s: {
string: typeof string;
number: typeof number;
boolean: typeof boolean;
literal: typeof literal;
enum: typeof enumeration;
unknown: typeof unknown;
optional: typeof optional;
nullable: typeof nullable;
array: typeof array;
object: typeof object;
temperature: typeof temperature;
dateTime: typeof dateTime;
duration: typeof duration;
timeInterval: typeof timeInterval;
};
export {};

186
dist/esm/registry/schema.js vendored Normal file
View file

@ -0,0 +1,186 @@
// Values checked at run time and typed at compile time. A descriptor states its property values, directive payloads
// and declaration options with these; parse() returns the value or throws a SchemaError that names where it went
// wrong. Written here rather than taken from a package: mqtt stays the only runtime dependency.
/** A value that does not fit its schema: path is where ("payload.targetSetpoint.scale"), problem is what. */
export class SchemaError extends Error {
constructor(path, problem) {
super(path ? `${path}: ${problem}` : problem);
this.path = path;
this.problem = problem;
this.name = "SchemaError";
}
}
// The input as an error message shows it: short, and quoted when it is text.
function shown(input) {
if (input === undefined)
return "nothing";
if (typeof input === "function")
return "a function";
const text = JSON.stringify(input) ?? String(input);
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
}
function mismatch(path, expects, input) {
return new SchemaError(path, `expected ${expects}, got ${shown(input)}`);
}
const at = (path, key) => (path ? `${path}.${key}` : key);
function isRecord(input) {
return typeof input === "object" && input !== null && !Array.isArray(input);
}
function schema(expects, accepts) {
return {
expects,
parse(input, path = "") {
if (!accepts(input))
throw mismatch(path, expects, input);
return input;
},
};
}
function numberText({ min, max, gt, integer }) {
const kind = integer ? "an integer" : "a number";
if (min !== undefined && max !== undefined)
return `${kind} from ${min} to ${max}`;
if (min !== undefined)
return `${kind} of ${min} or more`;
if (max !== undefined)
return `${kind} of ${max} or less`;
if (gt !== undefined)
return `${kind} greater than ${gt}`;
return kind;
}
function number(rules = {}) {
const { min = -Infinity, max = Infinity, gt = -Infinity, integer = false } = rules;
return schema(numberText(rules), (input) => typeof input === "number" && Number.isFinite(input) && input >= min && input <= max && input > gt
&& (!integer || Number.isInteger(input)));
}
function string(rules = {}) {
const { min = 0, max = Infinity, pattern } = rules;
const length = max === Infinity ? (min > 0 ? ` of ${min} or more characters` : "") : ` of ${min} to ${max} characters`;
return schema(rules.expects ?? `a string${length}`, (input) => typeof input === "string" && input.length >= min && input.length <= max && (!pattern || pattern.test(input)));
}
function boolean() {
return schema("true or false", (input) => typeof input === "boolean");
}
function literal(value) {
return schema(JSON.stringify(value), (input) => input === value);
}
function enumeration(...values) {
return { ...schema(values.join(" | "), (input) => values.includes(input)), values };
}
function unknown() {
return schema("any value", () => true);
}
function optional(inner) {
return {
expects: inner.expects,
optional: true,
parse: (input, path = "") => (input === undefined ? undefined : inner.parse(input, path)),
};
}
function nullable(inner) {
const expects = `${inner.expects} or null`;
return {
expects,
parse(input, path = "") {
if (input === null)
return null;
try {
return inner.parse(input, path);
}
catch (err) {
// The value itself is of the wrong kind: null was a choice too. An error further in keeps its own path.
if (err instanceof SchemaError && err.path === path)
throw mismatch(path, expects, input);
throw err;
}
},
};
}
function array(item, rules = {}) {
const { min = 0, max = Infinity } = rules;
const count = max === Infinity ? (min > 0 ? ` with ${min} or more entries` : "") : ` with ${min} to ${max} entries`;
const expects = `a list${count}`;
return {
expects,
parse(input, path = "") {
if (!Array.isArray(input) || input.length < min || input.length > max)
throw mismatch(path, expects, input);
return input.map((entry, i) => item.parse(entry, `${path}[${i}]`));
},
};
}
/**
* An object with the keys of shape. Keys the shape does not name are kept as they are: a field Alexa adds to a
* directive reaches the handler. unknownKeys "reject" is for what a developer writes, where such a key is a typo.
*/
function object(shape, rules = {}) {
const known = Object.keys(shape);
const expects = known.length > 0 ? `an object with ${known.join(", ")}` : "an object";
return {
expects,
parse(input, path = "") {
if (!isRecord(input))
throw mismatch(path, expects, input);
const others = Object.keys(input).filter((key) => !known.includes(key));
if (others.length > 0 && rules.unknownKeys === "reject") {
throw new SchemaError(at(path, others[0]), `unknown key, the known ones are ${known.join(", ") || "none"}`);
}
const parsed = {};
for (const key of known) {
const value = shape[key].parse(input[key], at(path, key));
if (value !== undefined)
parsed[key] = value;
}
for (const key of others)
parsed[key] = input[key];
return parsed;
},
};
}
// alexa-property-schemas.html "Temperature", "Temperature scales"
function temperature() {
return object({ value: number(), scale: enumeration("CELSIUS", "FAHRENHEIT", "KELVIN") });
}
// alexa-property-schemas.html "DateTime": UTC, no offsets. The seconds are optional here because the TimeInterval
// examples on the same page leave them out ("2017-10-04T14:00Z").
const DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?Z$/;
function dateTime() {
return schema("a UTC time like 2017-08-30T01:18:21Z", (input) => typeof input === "string" && DATE_TIME.test(input) && !Number.isNaN(Date.parse(input)));
}
// alexa-property-schemas.html "Duration": the time portion of ISO 8601, negative for a delta ("PT-30S")
const DURATION = /^PT(?=.)(-?\d+H)?(-?\d+M)?(-?\d+S)?$/;
function duration() {
return string({ pattern: DURATION, expects: "a duration like PT3M15S" });
}
// alexa-property-schemas.html "TimeInterval": "Specify one or two of the time interval fields. If you specify all
// three fields, an error occurs."
function timeInterval() {
const fields = object({ start: optional(dateTime()), end: optional(dateTime()), duration: optional(duration()) });
const expects = "a time interval with one or two of start, end, duration";
return {
expects,
parse(input, path = "") {
const interval = fields.parse(input, path);
const given = [interval.start, interval.end, interval.duration].filter((field) => field !== undefined).length;
if (given < 1 || given > 2)
throw mismatch(path, expects, input);
return interval;
},
};
}
export const s = {
string,
number,
boolean,
literal,
enum: enumeration,
unknown,
optional,
nullable,
array,
object,
temperature,
dateTime,
duration,
timeInterval,
};

123
dist/esm/registry/types.d.ts vendored Normal file
View file

@ -0,0 +1,123 @@
import type { Schema } from "./schema.js";
/**
* A friendly name: text in one locale, or an asset of the global Alexa catalog, which stands for several names in
* every language Alexa speaks (resources-and-assets.html, "Label object").
*/
export type Label = {
"@type": "text";
value: {
text: string;
locale: string;
};
} | {
"@type": "asset";
value: {
assetId: string;
};
};
export interface PropertyDescriptor<V> {
name: string;
value: Schema<V>;
note?: string;
}
export interface DirectiveDescriptor<T> {
name: string;
payload: Schema<T>;
/** Alexa sends the directive only to a capability declared like this (AdjustMode: an ordered mode). */
when?: (capability: Declared<any>) => boolean;
note?: string;
}
/** An event a device raises that is not a Response: ActivationStarted, DoorbellPress. */
export interface EventDescriptor {
name: string;
/** Default: the namespace of the interface. */
namespace?: string;
/** Default: the version of the interface. */
payloadVersion?: string;
payload: Schema<any>;
/** response: the answer to a directive. proactive: sent without one. */
topic: "response" | "proactive";
}
/** What an interface adds to the capability object in discovery, next to the fields every capability has. */
export interface CapabilityExtras {
configuration?: Record<string, unknown>;
/** Alexa.EqualizerController spells it in the plural. */
configurations?: Record<string, unknown>;
/** Fields of the capability object itself: supportsDeactivation, supportedOperations, inputs. */
topLevel?: Record<string, unknown>;
/** false: the capability has no properties object (the Alexa interface, a scene). */
properties?: false;
}
/** A capability as declared on an endpoint: what discovery() and validate() of its descriptor are given. */
export interface Declared<O = Record<string, unknown>> {
readonly endpointId: string;
readonly namespace: string;
/** "" when the interface is declared without an instance. */
readonly instance: string;
readonly friendlyNames: readonly Label[];
readonly retrievable: boolean;
readonly proactivelyReported: boolean;
readonly nonControllable?: boolean;
/** The options of the interface, as its options schema returned them. */
readonly options: O;
}
/** The endpoint a capability is declared on, as validate() sees it. */
export interface EndpointView {
readonly endpointId: string;
readonly friendlyName: string;
readonly description: string;
readonly displayCategories: readonly string[];
/** The other capabilities of the endpoint. */
readonly capabilities: readonly Declared[];
}
export type Properties = Record<string, PropertyDescriptor<any>>;
export type Directives = Record<string, DirectiveDescriptor<any>>;
export interface InterfaceDescriptor<P extends Properties = Properties, D extends Directives = Directives, O = {}, I extends boolean = boolean> {
/** "Alexa.RangeController" */
namespace: string;
/** As the title of the interface's page gives it: "3", "3.1", "1.0". */
version: string;
/** The page the descriptor was written from. */
doc: string;
kind: "base" | "controller" | "sensor" | "eventSource";
/** 1 and 2: described in full. 3: a stub, the namespace with its version and property names and nothing checked. */
tier: 1 | 2 | 3;
/** A generic controller: each capability needs an instance name and friendly names. */
instanced: I;
/** The properties the interface reports. */
properties: P;
/** The directives Alexa sends. */
directives: D;
events?: Record<string, EventDescriptor>;
/** What a declaration can set beyond the fields every capability has: a range, the supported modes. */
options?: Schema<O>;
discovery?: (capability: Declared<O>) => CapabilityExtras;
/** The event that answers a directive when it is not Alexa / Response (Arm -> Arm.Response). */
responseFor?: (directive: string) => {
namespace: string;
name: string;
payload?: Schema<any>;
} | undefined;
/** The namespace an ErrorResponse goes under when its type is one of errorTypes. */
errorNamespace?: string;
errorTypes?: readonly string[];
/** Amazon documents a DeferredResponse for the interface. */
deferrable?: boolean;
/** Rules a schema cannot state. Throws DeclarationError. */
validate?: (capability: Declared<O>, endpoint: EndpointView) => void;
}
export type AnyDescriptor = InterfaceDescriptor<any, any, any, boolean>;
/** Types a descriptor from what it is given: the property names, the payload of each directive, the options. */
export declare function defineInterface<P extends Properties, D extends Directives, O = {}, I extends boolean = boolean>(descriptor: InterfaceDescriptor<P, D, O, I>): InterfaceDescriptor<P, D, O, I>;
/** A device, a capability or an interface name declared in a way Alexa would reject. Thrown where it is declared. */
export declare class DeclarationError extends Error {
readonly endpointId?: string;
readonly namespace?: string;
readonly instance?: string;
readonly problem: string;
constructor(where: {
endpointId?: string;
namespace?: string;
instance?: string;
}, problem: string);
}

20
dist/esm/registry/types.js vendored Normal file
View file

@ -0,0 +1,20 @@
/** Types a descriptor from what it is given: the property names, the payload of each directive, the options. */
export function defineInterface(descriptor) {
return descriptor;
}
/** A device, a capability or an interface name declared in a way Alexa would reject. Thrown where it is declared. */
export class DeclarationError extends Error {
constructor(where, problem) {
const capability = where.namespace && where.instance ? `${where.namespace} "${where.instance}"` : where.namespace;
super([where.endpointId && shortened(where.endpointId), capability, problem].filter(Boolean).join(": "));
this.name = "DeclarationError";
this.endpointId = where.endpointId;
this.namespace = where.namespace;
this.instance = where.instance || undefined;
this.problem = problem;
}
}
// An endpointId may be the very thing that is wrong, 300 characters of it.
function shortened(text) {
return text.length > 64 ? `${text.slice(0, 61)}...` : text;
}

View file

@ -101,7 +101,9 @@ export declare class AlexaInterface {
setInstance(name: string): void;
getType(): AlexaInterfaceType;
getTypeString(): string;
/** The version of the interface, from its descriptor. "UNKNOWN" for a name the registry does not have. */
getVersion(): string;
/** The names of the properties the interface reports, from its descriptor. */
getProps(): string[];
getJSON(): object;
}

View file

@ -51,6 +51,8 @@ export declare enum DisplayCategory {
THERMOSTAT = "THERMOSTAT",
TV = "TV",
VACUUM_CLEANER = "VACUUM_CLEANER",
VACUUM = "VACUUM",
/** Not on the list of display categories any more; kept for 1.x callers. */
VEHICLE = "VEHICLE",
WASHER = "WASHER",
WATER_HEATER = "WATER_HEATER",

View file

@ -8,3 +8,6 @@ export { DisplayCategory } from "./DisplayCategory.js";
export { AlexaStatusMessage, PowerController, EndpointHealth, TemperatureSensorScale, ThermostatMode } from "./AlexaStatusMessage.js";
export type { ChangeCause } from "./AlexaStatusMessage.js";
export { AlexaErrorType, AlexaErrorResponse } from "./AlexaErrorResponse.js";
export { registry, DeclarationError, SchemaError } from "./registry/index.js";
export { ASSETS as Assets, UNITS_OF_MEASURE as Units, ACTIONS as Actions, STATES as States, DISPLAY_CATEGORIES as DisplayCategories, } from "./registry/index.js";
export type { ActionId, AssetId, DisplayCategoryName, StateId, UnitOfMeasure, AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor, Label, PropertyDescriptor, Infer, Schema, Temperature, TimeInterval, } from "./registry/index.js";

40
dist/types/registry/catalog.d.ts vendored Normal file
View file

@ -0,0 +1,40 @@
/**
* The asset ids a friendly name can refer to: 103, the units of measure among them
* (resources-and-assets.html, "Global Alexa catalog").
*/
export declare const ASSETS: readonly ["Alexa.Actions.Charge", "Alexa.Actions.Clean", "Alexa.Actions.Dispense", "Alexa.Actions.Dock", "Alexa.Actions.Empty", "Alexa.Actions.Mop", "Alexa.Actions.Skip", "Alexa.Actions.Sweep", "Alexa.Actions.Vacuum", "Alexa.Button.OffButton", "Alexa.Button.OnButton", "Alexa.Button.BrightenButton", "Alexa.Button.DimButton", "Alexa.Button.MainButton", "Alexa.Button.TopButton", "Alexa.Button.BottomButton", "Alexa.Button.CenterButton", "Alexa.Button.MiddleButton", "Alexa.Button.UpButton", "Alexa.Button.DownButton", "Alexa.Button.LeftButton", "Alexa.Button.RightButton", "Alexa.Button.FirstButton", "Alexa.Button.SecondButton", "Alexa.Button.ThirdButton", "Alexa.Button.FourthButton", "Alexa.Button.FifthButton", "Alexa.Button.SixthButton", "Alexa.Button.SeventhButton", "Alexa.Button.EighthButton", "Alexa.Button.DoublePress", "Alexa.Button.DoublePush", "Alexa.Button.LongPress", "Alexa.Button.LongPush", "Alexa.Button.SinglePress", "Alexa.Button.SinglePush", "Alexa.DeviceName.AirPurifier", "Alexa.DeviceName.Camera", "Alexa.DeviceName.Fan", "Alexa.DeviceName.Router", "Alexa.DeviceName.Shade", "Alexa.DeviceName.Shower", "Alexa.DeviceName.SpaceHeater", "Alexa.DeviceName.Washer", "Alexa.Gesture.DoubleClick", "Alexa.Gesture.SingleClick", "Alexa.Gesture.SwipeDown", "Alexa.Gesture.SwipeLeft", "Alexa.Gesture.SwipeRight", "Alexa.Gesture.SwipeUp", "Alexa.Gesture.Tap", "Alexa.Gestures.DoubleTap", "Alexa.Setting.2GGuestWiFi", "Alexa.Setting.5GGuestWiFi", "Alexa.Setting.Auto", "Alexa.Setting.Direction", "Alexa.Setting.DryCycle", "Alexa.Setting.FanSpeed", "Alexa.Setting.GuestWiFi", "Alexa.Setting.Heat", "Alexa.Setting.Mode", "Alexa.Setting.Night", "Alexa.Setting.Opening", "Alexa.Setting.Oscillate", "Alexa.Setting.Preset", "Alexa.Setting.Quiet", "Alexa.Setting.Temperature", "Alexa.Setting.WashCycle", "Alexa.Setting.WaterTemperature", "Alexa.Shower.HandHeld", "Alexa.Shower.RainHead", "Alexa.Unit.Angle.Degrees", "Alexa.Unit.Angle.Radians", "Alexa.Unit.Distance.Feet", "Alexa.Unit.Distance.Inches", "Alexa.Unit.Distance.Kilometers", "Alexa.Unit.Distance.Meters", "Alexa.Unit.Distance.Miles", "Alexa.Unit.Distance.Yards", "Alexa.Unit.Mass.Grams", "Alexa.Unit.Mass.Kilograms", "Alexa.Unit.Percent", "Alexa.Unit.Temperature.Celsius", "Alexa.Unit.Temperature.Degrees", "Alexa.Unit.Temperature.Fahrenheit", "Alexa.Unit.Temperature.Kelvin", "Alexa.Unit.Volume.CubicFeet", "Alexa.Unit.Volume.CubicMeters", "Alexa.Unit.Volume.Gallons", "Alexa.Unit.Volume.Liters", "Alexa.Unit.Volume.Pints", "Alexa.Unit.Volume.Quarts", "Alexa.Unit.Weight.Ounces", "Alexa.Unit.Weight.Pounds", "Alexa.Value.Close", "Alexa.Value.Delicate", "Alexa.Value.High", "Alexa.Value.Low", "Alexa.Value.Maximum", "Alexa.Value.Medium", "Alexa.Value.Minimum", "Alexa.Value.Open", "Alexa.Value.QuickWash"];
export type AssetId = (typeof ASSETS)[number];
/** A unit of measure: the 23 Alexa.Unit.* assets, what a RangeController's unitOfMeasure takes. */
export type UnitOfMeasure = Extract<AssetId, `Alexa.Unit.${string}`>;
export declare const UNITS_OF_MEASURE: UnitOfMeasure[];
/** The phrases an action mapping gives to a directive (alexa-discovery-objects.html, "ActionMappings object"). */
export declare const ACTIONS: readonly ["Alexa.Actions.Open", "Alexa.Actions.Close", "Alexa.Actions.Raise", "Alexa.Actions.Lower", "Alexa.Actions.SetEcoOn", "Alexa.Actions.SetEcoOff"];
export type ActionId = (typeof ACTIONS)[number];
/** The states a state mapping gives to a property value (alexa-discovery-objects.html, "StateMappings object"). */
export declare const STATES: readonly ["Alexa.States.Open", "Alexa.States.Closed", "Alexa.States.EcoOn", "Alexa.States.EcoOff", "Alexa.States.Low", "Alexa.States.Empty", "Alexa.States.Full", "Alexa.States.Done", "Alexa.States.Stuck"];
export type StateId = (typeof STATES)[number];
/**
* The 56 display categories (alexa-discovery.html, "Display categories"). The DisplayCategory enum also keeps
* VEHICLE from 1.x, which the page no longer lists.
*/
export declare const DISPLAY_CATEGORIES: readonly ["ACTIVITY_TRIGGER", "AIR_CONDITIONER", "AIR_FRESHENER", "AIR_PURIFIER", "AIR_QUALITY_MONITOR", "ALEXA_VOICE_ENABLED", "AUTO_ACCESSORY", "BLUETOOTH_SPEAKER", "CAMERA", "CHRISTMAS_TREE", "COFFEE_MAKER", "COMPUTER", "CONTACT_SENSOR", "DISHWASHER", "DOOR", "DOORBELL", "DRYER", "EXTERIOR_BLIND", "FAN", "GAME_CONSOLE", "GARAGE_DOOR", "HEADPHONES", "HUB", "INTERIOR_BLIND", "LAPTOP", "LIGHT", "MICROWAVE", "MOBILE_PHONE", "MOTION_SENSOR", "MUSIC_SYSTEM", "NETWORK_HARDWARE", "OTHER", "OVEN", "PHONE", "PRINTER", "REMOTE", "ROUTER", "SCENE_TRIGGER", "SCREEN", "SECURITY_PANEL", "SECURITY_SYSTEM", "SLOW_COOKER", "SMARTLOCK", "SMARTPLUG", "SPEAKER", "STREAMING_DEVICE", "SWITCH", "TABLET", "TEMPERATURE_SENSOR", "THERMOSTAT", "TV", "VACUUM_CLEANER", "VACUUM", "WASHER", "WATER_HEATER", "WEARABLE"];
export type DisplayCategoryName = (typeof DISPLAY_CATEGORIES)[number];
/** Not to be used as a friendly name (resources-and-assets.html, "Reserved words"). */
export declare const RESERVED_WORDS: readonly ["alarm", "alarms", "all alarms", "away mode", "bass", "camera", "date", "date today", "day", "do not disturb", "drop in", "music", "night light", "notification", "playing", "sleep sounds", "time", "timer", "today in music", "treble", "volume", "way f. m."];
/**
* What a discovery answer may hold (alexa-discovery.html, "Interface limits"; alexa-discovery-objects.html,
* "Endpoint object details" and "AdditionalAttributes object details"; alexa-scenecontroller.html, "Discovery").
*/
export declare const LIMITS: {
readonly endpointsPerCustomer: 300;
readonly capabilitiesPerEndpoint: 100;
readonly endpointIdLength: 256;
readonly friendlyNameLength: 256;
readonly sceneFriendlyNameLength: 128;
readonly manufacturerNameLength: 128;
readonly descriptionLength: 128;
readonly additionalAttributeLength: 256;
readonly cookieBytes: 5000;
};
/** The 73 error types of the table, each with the namespace its ErrorResponse goes under. */
export declare const ERROR_TYPES: Readonly<Record<string, string>>;

20
dist/types/registry/index.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
import { Alexa } from "./interfaces/Alexa.js";
import { BrightnessController } from "./interfaces/BrightnessController.js";
import { EndpointHealth } from "./interfaces/EndpointHealth.js";
import { PowerController } from "./interfaces/PowerController.js";
import { TemperatureSensor } from "./interfaces/TemperatureSensor.js";
import type { AnyDescriptor } from "./types.js";
export declare const registry: {
/** Whether an interface of this name is known. */
has(namespace: string): boolean;
/** The descriptor of "Alexa.RangeController" or of AlexaInterfaceType.RANGE_CONTROLLER, which is that string. */
get(namespace: string): AnyDescriptor;
/** Every descriptor, ordered by namespace. */
list(): AnyDescriptor[];
};
export { Alexa, BrightnessController, EndpointHealth, PowerController, TemperatureSensor };
export { DeclarationError, defineInterface } from "./types.js";
export type { AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor, Label, PropertyDescriptor, } from "./types.js";
export { s, SchemaError } from "./schema.js";
export type { Infer, Schema, Temperature, TimeInterval } from "./schema.js";
export * from "./catalog.js";

View file

@ -0,0 +1,10 @@
/**
* The base interface: every endpoint lists it (alexa-interface.html, "Support the Alexa interface in all add-ons").
* Its one directive to a device is ReportState, answered with a StateReport.
*/
export declare const Alexa: import("../types.js").InterfaceDescriptor<{}, {
ReportState: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{}>>;
};
}, {}, false>;

View file

@ -0,0 +1,20 @@
/** Both directives turn a light that is off on, at the brightness asked for (alexa-brightnesscontroller.html). */
export declare const BrightnessController: import("../types.js").InterfaceDescriptor<{
brightness: {
name: string;
value: import("../schema.js").Schema<number>;
};
}, {
SetBrightness: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
brightness: import("../schema.js").Schema<number>;
}>>;
};
AdjustBrightness: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
brightnessDelta: import("../schema.js").Schema<number>;
}>>;
};
}, {}, false>;

View file

@ -0,0 +1,15 @@
export declare const CONNECTIVITY_REASONS: readonly ["WIFI_BAD_PASSWORD", "WIFI_AP_NOT_FOUND", "WIFI_ROUTER_UNREACHABLE", "WIFI_AP_CHANNEL_QUALITY_LOW", "INTERNET_UNREACHABLE", "CAPTIVE_PORTAL_CHECK_FAILED", "UNKNOWN"];
/**
* Version 3.1, as the page is titled. It defines connectivity only. Examples on other pages declare 3 and 3.2;
* alex2node 1.x declared 3.3, which no page mentions.
*/
export declare const EndpointHealth: import("../types.js").InterfaceDescriptor<{
connectivity: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").InferShape<{
value: import("../schema.js").EnumSchema<"OK" | "UNREACHABLE">;
reason: import("../schema.js").OptionalSchema<"WIFI_BAD_PASSWORD" | "WIFI_AP_NOT_FOUND" | "WIFI_ROUTER_UNREACHABLE" | "WIFI_AP_CHANNEL_QUALITY_LOW" | "INTERNET_UNREACHABLE" | "CAPTIVE_PORTAL_CHECK_FAILED" | "UNKNOWN">;
}>>;
note: string;
};
}, {}, {}, false>;

View file

@ -0,0 +1,17 @@
export declare const PowerController: import("../types.js").InterfaceDescriptor<{
powerState: {
name: string;
value: import("../schema.js").EnumSchema<"ON" | "OFF">;
};
}, {
TurnOn: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{}>>;
};
TurnOff: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{}>>;
};
}, import("../schema.js").InferShape<{
verificationsRequired: import("../schema.js").OptionalSchema<("TurnOn" | "TurnOff")[]>;
}>, false>;

View file

@ -0,0 +1,7 @@
/** A sensor: an endpoint that has it declares Alexa.EndpointHealth as well (alexa-temperaturesensor.html). */
export declare const TemperatureSensor: import("../types.js").InterfaceDescriptor<{
temperature: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").Temperature>;
};
}, {}, {}, false>;

View file

@ -0,0 +1,2 @@
import type { AnyDescriptor } from "../types.js";
export declare const STUBS: readonly AnyDescriptor[];

91
dist/types/registry/schema.d.ts vendored Normal file
View file

@ -0,0 +1,91 @@
/** A value that does not fit its schema: path is where ("payload.targetSetpoint.scale"), problem is what. */
export declare class SchemaError extends Error {
readonly path: string;
readonly problem: string;
constructor(path: string, problem: string);
}
export interface Schema<T> {
/** What the schema accepts, in the words of an error message: "an integer from 0 to 100", "ON | OFF". */
readonly expects: string;
/** The checked value. path names it in the error message. */
parse(input: unknown, path?: string): T;
}
/** A schema that accepts a missing value; s.object() makes its key optional in the inferred type. */
export interface OptionalSchema<T> extends Schema<T | undefined> {
readonly optional: true;
}
export interface EnumSchema<V> extends Schema<V> {
readonly values: readonly V[];
}
/** The type a schema produces. */
export type Infer<S> = S extends Schema<infer T> ? T : never;
export type Shape = Record<string, Schema<any>>;
type OptionalKeys<S extends Shape> = {
[K in keyof S]: S[K] extends OptionalSchema<any> ? K : never;
}[keyof S];
export type InferShape<S extends Shape> = {
[K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]>;
} & {
[K in OptionalKeys<S>]?: Infer<S[K]>;
};
export interface NumberRules {
min?: number;
max?: number;
/** Greater than, the bound itself excluded. */
gt?: number;
integer?: boolean;
}
export interface Temperature {
value: number;
scale: "CELSIUS" | "FAHRENHEIT" | "KELVIN";
}
export interface TimeInterval {
start?: string;
end?: string;
duration?: string;
}
declare function number(rules?: NumberRules): Schema<number>;
declare function string(rules?: {
min?: number;
max?: number;
pattern?: RegExp;
expects?: string;
}): Schema<string>;
declare function boolean(): Schema<boolean>;
declare function literal<V extends string | number | boolean | null>(value: V): Schema<V>;
declare function enumeration<V extends readonly string[]>(...values: V): EnumSchema<V[number]>;
declare function unknown(): Schema<unknown>;
declare function optional<T>(inner: Schema<T>): OptionalSchema<T>;
declare function nullable<T>(inner: Schema<T>): Schema<T | null>;
declare function array<T>(item: Schema<T>, rules?: {
min?: number;
max?: number;
}): Schema<T[]>;
/**
* An object with the keys of shape. Keys the shape does not name are kept as they are: a field Alexa adds to a
* directive reaches the handler. unknownKeys "reject" is for what a developer writes, where such a key is a typo.
*/
declare function object<S extends Shape>(shape: S, rules?: {
unknownKeys?: "keep" | "reject";
}): Schema<InferShape<S>>;
declare function temperature(): Schema<Temperature>;
declare function dateTime(): Schema<string>;
declare function duration(): Schema<string>;
declare function timeInterval(): Schema<TimeInterval>;
export declare const s: {
string: typeof string;
number: typeof number;
boolean: typeof boolean;
literal: typeof literal;
enum: typeof enumeration;
unknown: typeof unknown;
optional: typeof optional;
nullable: typeof nullable;
array: typeof array;
object: typeof object;
temperature: typeof temperature;
dateTime: typeof dateTime;
duration: typeof duration;
timeInterval: typeof timeInterval;
};
export {};

123
dist/types/registry/types.d.ts vendored Normal file
View file

@ -0,0 +1,123 @@
import type { Schema } from "./schema.js";
/**
* A friendly name: text in one locale, or an asset of the global Alexa catalog, which stands for several names in
* every language Alexa speaks (resources-and-assets.html, "Label object").
*/
export type Label = {
"@type": "text";
value: {
text: string;
locale: string;
};
} | {
"@type": "asset";
value: {
assetId: string;
};
};
export interface PropertyDescriptor<V> {
name: string;
value: Schema<V>;
note?: string;
}
export interface DirectiveDescriptor<T> {
name: string;
payload: Schema<T>;
/** Alexa sends the directive only to a capability declared like this (AdjustMode: an ordered mode). */
when?: (capability: Declared<any>) => boolean;
note?: string;
}
/** An event a device raises that is not a Response: ActivationStarted, DoorbellPress. */
export interface EventDescriptor {
name: string;
/** Default: the namespace of the interface. */
namespace?: string;
/** Default: the version of the interface. */
payloadVersion?: string;
payload: Schema<any>;
/** response: the answer to a directive. proactive: sent without one. */
topic: "response" | "proactive";
}
/** What an interface adds to the capability object in discovery, next to the fields every capability has. */
export interface CapabilityExtras {
configuration?: Record<string, unknown>;
/** Alexa.EqualizerController spells it in the plural. */
configurations?: Record<string, unknown>;
/** Fields of the capability object itself: supportsDeactivation, supportedOperations, inputs. */
topLevel?: Record<string, unknown>;
/** false: the capability has no properties object (the Alexa interface, a scene). */
properties?: false;
}
/** A capability as declared on an endpoint: what discovery() and validate() of its descriptor are given. */
export interface Declared<O = Record<string, unknown>> {
readonly endpointId: string;
readonly namespace: string;
/** "" when the interface is declared without an instance. */
readonly instance: string;
readonly friendlyNames: readonly Label[];
readonly retrievable: boolean;
readonly proactivelyReported: boolean;
readonly nonControllable?: boolean;
/** The options of the interface, as its options schema returned them. */
readonly options: O;
}
/** The endpoint a capability is declared on, as validate() sees it. */
export interface EndpointView {
readonly endpointId: string;
readonly friendlyName: string;
readonly description: string;
readonly displayCategories: readonly string[];
/** The other capabilities of the endpoint. */
readonly capabilities: readonly Declared[];
}
export type Properties = Record<string, PropertyDescriptor<any>>;
export type Directives = Record<string, DirectiveDescriptor<any>>;
export interface InterfaceDescriptor<P extends Properties = Properties, D extends Directives = Directives, O = {}, I extends boolean = boolean> {
/** "Alexa.RangeController" */
namespace: string;
/** As the title of the interface's page gives it: "3", "3.1", "1.0". */
version: string;
/** The page the descriptor was written from. */
doc: string;
kind: "base" | "controller" | "sensor" | "eventSource";
/** 1 and 2: described in full. 3: a stub, the namespace with its version and property names and nothing checked. */
tier: 1 | 2 | 3;
/** A generic controller: each capability needs an instance name and friendly names. */
instanced: I;
/** The properties the interface reports. */
properties: P;
/** The directives Alexa sends. */
directives: D;
events?: Record<string, EventDescriptor>;
/** What a declaration can set beyond the fields every capability has: a range, the supported modes. */
options?: Schema<O>;
discovery?: (capability: Declared<O>) => CapabilityExtras;
/** The event that answers a directive when it is not Alexa / Response (Arm -> Arm.Response). */
responseFor?: (directive: string) => {
namespace: string;
name: string;
payload?: Schema<any>;
} | undefined;
/** The namespace an ErrorResponse goes under when its type is one of errorTypes. */
errorNamespace?: string;
errorTypes?: readonly string[];
/** Amazon documents a DeferredResponse for the interface. */
deferrable?: boolean;
/** Rules a schema cannot state. Throws DeclarationError. */
validate?: (capability: Declared<O>, endpoint: EndpointView) => void;
}
export type AnyDescriptor = InterfaceDescriptor<any, any, any, boolean>;
/** Types a descriptor from what it is given: the property names, the payload of each directive, the options. */
export declare function defineInterface<P extends Properties, D extends Directives, O = {}, I extends boolean = boolean>(descriptor: InterfaceDescriptor<P, D, O, I>): InterfaceDescriptor<P, D, O, I>;
/** A device, a capability or an interface name declared in a way Alexa would reject. Thrown where it is declared. */
export declare class DeclarationError extends Error {
readonly endpointId?: string;
readonly namespace?: string;
readonly instance?: string;
readonly problem: string;
constructor(where: {
endpointId?: string;
namespace?: string;
instance?: string;
}, problem: string);
}

View file

@ -27,7 +27,7 @@
"build": "node scripts/build.mjs",
"check": "tsc --noEmit && tsc -p test/fixtures/tsconfig.json",
"prepare": "npm run build",
"test": "node --test test/*.test.js test/*.test.mjs"
"test": "node --test test/*.test.js test/*.test.mjs test/*/*.test.js"
},
"keywords": [
"alexa",

View file

@ -1,4 +1,5 @@
import type { ActionMapping } from "./ActionMapping.js";
import { registry } from "./registry/index.js";
export enum AlexaInterfaceType {
APPLICATION_STATE_REPORTER = "Alexa.ApplicationStateReporter",
@ -119,173 +120,14 @@ export class AlexaInterface {
return this.type;
}
/** The version of the interface, from its descriptor. "UNKNOWN" for a name the registry does not have. */
getVersion(): string {
switch (this.type) {
case AlexaInterfaceType.APPLICATION_STATE_REPORTER:
case AlexaInterfaceType.AUDIO_PLAY_QUEUE:
case AlexaInterfaceType.AUTHORIZATION_CONTROLLER:
case AlexaInterfaceType.AUTOMATION_MANAGEMENT:
case AlexaInterfaceType.AUTOMOTIVE_VEHICLE_DATA:
case AlexaInterfaceType.COMMISSIONABLE:
case AlexaInterfaceType.CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER:
case AlexaInterfaceType.COOKING:
case AlexaInterfaceType.COOKING_FOOD_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType.COOKING_FOOD_TEMPERATURE_SENSOR:
case AlexaInterfaceType.COOKING_PRESET_CONTROLLER:
case AlexaInterfaceType.COOKING_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType.COOKING_TEMPERATURE_SENSOR:
case AlexaInterfaceType.COOKING_TIME_CONTROLLER:
case AlexaInterfaceType.DATA_CONTROLLER:
case AlexaInterfaceType.DEVICE_USAGE_ESTIMATION:
case AlexaInterfaceType.DEVICE_USAGE_METER:
case AlexaInterfaceType.EQUALIZER_CONTROLLER:
case AlexaInterfaceType.INVENTORY_LEVEL_SENSOR:
case AlexaInterfaceType.INVENTORY_LEVEL_USAGE_SENSOR:
case AlexaInterfaceType.INVENTORY_USAGE_SENSOR:
case AlexaInterfaceType.KEYPAD_CONTROLLER:
case AlexaInterfaceType.MEDIA_PLAYBACK:
case AlexaInterfaceType.MEDIA_PLAY_QUEUE:
case AlexaInterfaceType.MEDIA_SEARCH:
case AlexaInterfaceType.PLAYBACK_STATE_REPORTER:
case AlexaInterfaceType.PROACTIVE_NOTIFICATION_SOURCE:
case AlexaInterfaceType.REMOTE_VIDEO_PLAYER:
case AlexaInterfaceType.RTC_SESSION_CONTROLLER:
case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER:
case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER_ALERT:
case AlexaInterfaceType.SIMPLE_EVENT_SOURCE:
case AlexaInterfaceType.SMART_VISION_OBJECT_DETECTION_SENSOR:
case AlexaInterfaceType.SMART_VISION_SNAPSHOT_PROVIDER:
case AlexaInterfaceType.SPEAKER:
case AlexaInterfaceType.STEP_SPEAKER:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_CONFIGURATION:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_HVAC_COMPONENTS:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_SCHEDULE:
case AlexaInterfaceType.UI_CONTROLLER:
case AlexaInterfaceType.USER_PREFERENCE:
case AlexaInterfaceType.VIDEO_RECORDER:
case AlexaInterfaceType.WAKE_ON_LAN_CONTROLLER:
return "1";
case AlexaInterfaceType.BRIGHTNESS_CONTROLLER:
case AlexaInterfaceType.CAMERA_STREAM_CONTROLLER:
case AlexaInterfaceType.CHANNEL_CONTROLLER:
case AlexaInterfaceType.COLOR_CONTROLLER:
case AlexaInterfaceType.COLOR_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType.CONTACT_SENSOR:
case AlexaInterfaceType.DOORBELL_EVENT_SOURCE:
case AlexaInterfaceType.INPUT_CONTROLLER:
case AlexaInterfaceType.LOCK_CONTROLLER:
case AlexaInterfaceType.MODE_CONTROLLER:
case AlexaInterfaceType.MOTION_SENSOR:
case AlexaInterfaceType.PERCENTAGE_CONTROLLER:
case AlexaInterfaceType.PLAYBACK_CONTROLLER:
case AlexaInterfaceType.POWER_CONTROLLER:
case AlexaInterfaceType.POWER_LEVEL_CONTROLLER:
case AlexaInterfaceType.RANGE_CONTROLLER:
case AlexaInterfaceType.RECORD_CONTROLLER:
case AlexaInterfaceType.SCENE_CONTROLLER:
case AlexaInterfaceType.SEEK_CONTROLLER:
case AlexaInterfaceType.TEMPERATURE_SENSOR:
case AlexaInterfaceType.TOGGLE_CONTROLLER:
return "3";
case AlexaInterfaceType.LAUNCHER:
return "1.1";
case AlexaInterfaceType.THERMOSTAT_CONTROLLER:
return "3.2";
case AlexaInterfaceType.ENDPOINT_HEALTH:
return "3.3";
default:
return "UNKNOWN";
}
return registry.has(this.type) ? registry.get(this.type).version : "UNKNOWN";
}
/** The names of the properties the interface reports, from its descriptor. */
getProps(): string[] {
switch (this.type) {
case AlexaInterfaceType.AUTOMATION_MANAGEMENT:
return ["automationStatuses"];
case AlexaInterfaceType.BRIGHTNESS_CONTROLLER:
return ["brightness"];
case AlexaInterfaceType.CHANNEL_CONTROLLER:
return ["channel"];
case AlexaInterfaceType.COLOR_CONTROLLER:
return ["color"];
case AlexaInterfaceType.COLOR_TEMPERATURE_CONTROLLER:
return ["colorTemperatureInKelvin"];
case AlexaInterfaceType.CONTACT_SENSOR:
return ["detectionState"];
case AlexaInterfaceType.POWER_CONTROLLER:
return ["powerState"];
case AlexaInterfaceType.ENDPOINT_HEALTH:
return ["connectivity"];
case AlexaInterfaceType.TOGGLE_CONTROLLER:
return ["toggleState"];
case AlexaInterfaceType.MODE_CONTROLLER:
return ["mode"];
case AlexaInterfaceType.TEMPERATURE_SENSOR:
return ["temperature"];
case AlexaInterfaceType.THERMOSTAT_CONTROLLER:
return [
"targetSetpoint",
"lowerSetpoint",
"upperSetpoint",
"thermostatMode",
];
case AlexaInterfaceType.APPLICATION_STATE_REPORTER:
case AlexaInterfaceType.AUDIO_PLAY_QUEUE:
case AlexaInterfaceType.AUTHORIZATION_CONTROLLER:
case AlexaInterfaceType.AUTOMOTIVE_VEHICLE_DATA:
case AlexaInterfaceType.CAMERA_LIVE_VIEW_CONTROLLER:
case AlexaInterfaceType.CAMERA_STREAM_CONTROLLER:
case AlexaInterfaceType.COMMISSIONABLE:
case AlexaInterfaceType.CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER:
case AlexaInterfaceType.COOKING:
case AlexaInterfaceType.DATA_CONTROLLER:
case AlexaInterfaceType.DEVICE_USAGE_ESTIMATION:
case AlexaInterfaceType.DEVICE_USAGE_METER:
case AlexaInterfaceType.DOORBELL_EVENT_SOURCE:
case AlexaInterfaceType.EQUALIZER_CONTROLLER:
case AlexaInterfaceType.INPUT_CONTROLLER:
case AlexaInterfaceType.INVENTORY_LEVEL_SENSOR:
case AlexaInterfaceType.INVENTORY_LEVEL_USAGE_SENSOR:
case AlexaInterfaceType.INVENTORY_USAGE_SENSOR:
case AlexaInterfaceType.KEYPAD_CONTROLLER:
case AlexaInterfaceType.LAUNCHER:
case AlexaInterfaceType.LOCK_CONTROLLER:
case AlexaInterfaceType.MEDIA_PLAYBACK:
case AlexaInterfaceType.MEDIA_SEARCH:
case AlexaInterfaceType.MOTION_SENSOR:
case AlexaInterfaceType.PERCENTAGE_CONTROLLER:
case AlexaInterfaceType.PLAYBACK_CONTROLLER:
case AlexaInterfaceType.PLAYBACK_STATE_REPORTER:
case AlexaInterfaceType.PROACTIVE_NOTIFICATION_SOURCE:
case AlexaInterfaceType.RANGE_CONTROLLER:
case AlexaInterfaceType.RECORD_CONTROLLER:
case AlexaInterfaceType.REMOTE_VIDEO_PLAYER:
case AlexaInterfaceType.RTC_SESSION_CONTROLLER:
case AlexaInterfaceType.SCENE_CONTROLLER:
return []; // scenes have no reportable properties: Activate/Deactivate answer with ActivationStarted (Device.sendSceneResponse)
case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER:
case AlexaInterfaceType.SEEK_CONTROLLER:
case AlexaInterfaceType.SIMPLE_EVENT_SOURCE:
case AlexaInterfaceType.SMART_VISION_OBJECT_DETECTION_SENSOR:
case AlexaInterfaceType.SMART_VISION_SNAPSHOT_PROVIDER:
case AlexaInterfaceType.SPEAKER:
case AlexaInterfaceType.STEP_SPEAKER:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_CONFIGURATION:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_HVAC_COMPONENTS:
case AlexaInterfaceType.THERMOSTAT_CONTROLLER_SCHEDULE:
case AlexaInterfaceType.TIME_HOLD_CONTROLLER:
case AlexaInterfaceType.UI_CONTROLLER:
case AlexaInterfaceType.USER_PREFERENCE:
case AlexaInterfaceType.VIDEO_RECORDER:
case AlexaInterfaceType.WAKE_ON_LAN_CONTROLLER:
// no property list known for these: discovery lists the capability with an empty "supported" (the bridge notes
// it through its log hook; 1.5.1 wrote "UNSUPORTED INTERFACE TYPE" to stderr on every discovery)
return [];
default:
return [];
}
return registry.has(this.type) ? Object.keys(registry.get(this.type).properties) : [];
}
getJSON(): object {

View file

@ -51,6 +51,8 @@ export enum DisplayCategory {
THERMOSTAT = "THERMOSTAT",
TV = "TV",
VACUUM_CLEANER = "VACUUM_CLEANER",
VACUUM = "VACUUM",
/** Not on the list of display categories any more; kept for 1.x callers. */
VEHICLE = "VEHICLE",
WASHER = "WASHER",
WATER_HEATER = "WATER_HEATER",

View file

@ -9,3 +9,15 @@ export { DisplayCategory } from "./DisplayCategory.js";
export { AlexaStatusMessage, PowerController, EndpointHealth, TemperatureSensorScale, ThermostatMode } from "./AlexaStatusMessage.js";
export type { ChangeCause } from "./AlexaStatusMessage.js";
export { AlexaErrorType, AlexaErrorResponse } from "./AlexaErrorResponse.js";
// The interface registry and the vocabularies of the Smart Home API
export { registry, DeclarationError, SchemaError } from "./registry/index.js";
export {
ASSETS as Assets, UNITS_OF_MEASURE as Units, ACTIONS as Actions, STATES as States,
DISPLAY_CATEGORIES as DisplayCategories,
} from "./registry/index.js";
export type {
ActionId, AssetId, DisplayCategoryName, StateId, UnitOfMeasure,
AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor,
Label, PropertyDescriptor, Infer, Schema, Temperature, TimeInterval,
} from "./registry/index.js";

146
src/registry/catalog.ts Normal file
View file

@ -0,0 +1,146 @@
// The fixed vocabularies of the Smart Home API, copied from Amazon's pages as read on 2026-09-28. The pages are under
// https://developer.amazon.com/docs/alexaplus/device-apis/. A value missing here is one Amazon added since.
/**
* The asset ids a friendly name can refer to: 103, the units of measure among them
* (resources-and-assets.html, "Global Alexa catalog").
*/
export const ASSETS = [
"Alexa.Actions.Charge", "Alexa.Actions.Clean", "Alexa.Actions.Dispense", "Alexa.Actions.Dock",
"Alexa.Actions.Empty", "Alexa.Actions.Mop", "Alexa.Actions.Skip", "Alexa.Actions.Sweep", "Alexa.Actions.Vacuum",
"Alexa.Button.OffButton", "Alexa.Button.OnButton", "Alexa.Button.BrightenButton", "Alexa.Button.DimButton",
"Alexa.Button.MainButton", "Alexa.Button.TopButton", "Alexa.Button.BottomButton", "Alexa.Button.CenterButton",
"Alexa.Button.MiddleButton", "Alexa.Button.UpButton", "Alexa.Button.DownButton", "Alexa.Button.LeftButton",
"Alexa.Button.RightButton", "Alexa.Button.FirstButton", "Alexa.Button.SecondButton", "Alexa.Button.ThirdButton",
"Alexa.Button.FourthButton", "Alexa.Button.FifthButton", "Alexa.Button.SixthButton", "Alexa.Button.SeventhButton",
"Alexa.Button.EighthButton", "Alexa.Button.DoublePress", "Alexa.Button.DoublePush", "Alexa.Button.LongPress",
"Alexa.Button.LongPush", "Alexa.Button.SinglePress", "Alexa.Button.SinglePush",
"Alexa.DeviceName.AirPurifier", "Alexa.DeviceName.Camera", "Alexa.DeviceName.Fan", "Alexa.DeviceName.Router",
"Alexa.DeviceName.Shade", "Alexa.DeviceName.Shower", "Alexa.DeviceName.SpaceHeater", "Alexa.DeviceName.Washer",
"Alexa.Gesture.DoubleClick", "Alexa.Gesture.SingleClick", "Alexa.Gesture.SwipeDown", "Alexa.Gesture.SwipeLeft",
"Alexa.Gesture.SwipeRight", "Alexa.Gesture.SwipeUp", "Alexa.Gesture.Tap",
// "Gestures", plural: the page spells this one id differently from the seven above
"Alexa.Gestures.DoubleTap",
"Alexa.Setting.2GGuestWiFi", "Alexa.Setting.5GGuestWiFi", "Alexa.Setting.Auto", "Alexa.Setting.Direction",
"Alexa.Setting.DryCycle", "Alexa.Setting.FanSpeed", "Alexa.Setting.GuestWiFi", "Alexa.Setting.Heat",
"Alexa.Setting.Mode", "Alexa.Setting.Night", "Alexa.Setting.Opening", "Alexa.Setting.Oscillate",
"Alexa.Setting.Preset", "Alexa.Setting.Quiet", "Alexa.Setting.Temperature", "Alexa.Setting.WashCycle",
"Alexa.Setting.WaterTemperature",
"Alexa.Shower.HandHeld", "Alexa.Shower.RainHead",
"Alexa.Unit.Angle.Degrees", "Alexa.Unit.Angle.Radians", "Alexa.Unit.Distance.Feet", "Alexa.Unit.Distance.Inches",
"Alexa.Unit.Distance.Kilometers", "Alexa.Unit.Distance.Meters", "Alexa.Unit.Distance.Miles",
"Alexa.Unit.Distance.Yards", "Alexa.Unit.Mass.Grams", "Alexa.Unit.Mass.Kilograms", "Alexa.Unit.Percent",
"Alexa.Unit.Temperature.Celsius", "Alexa.Unit.Temperature.Degrees", "Alexa.Unit.Temperature.Fahrenheit",
"Alexa.Unit.Temperature.Kelvin", "Alexa.Unit.Volume.CubicFeet", "Alexa.Unit.Volume.CubicMeters",
"Alexa.Unit.Volume.Gallons", "Alexa.Unit.Volume.Liters", "Alexa.Unit.Volume.Pints", "Alexa.Unit.Volume.Quarts",
"Alexa.Unit.Weight.Ounces", "Alexa.Unit.Weight.Pounds",
"Alexa.Value.Close", "Alexa.Value.Delicate", "Alexa.Value.High", "Alexa.Value.Low", "Alexa.Value.Maximum",
"Alexa.Value.Medium", "Alexa.Value.Minimum", "Alexa.Value.Open", "Alexa.Value.QuickWash",
] as const;
export type AssetId = (typeof ASSETS)[number];
/** A unit of measure: the 23 Alexa.Unit.* assets, what a RangeController's unitOfMeasure takes. */
export type UnitOfMeasure = Extract<AssetId, `Alexa.Unit.${string}`>;
export const UNITS_OF_MEASURE = ASSETS.filter((id): id is UnitOfMeasure => id.startsWith("Alexa.Unit."));
/** The phrases an action mapping gives to a directive (alexa-discovery-objects.html, "ActionMappings object"). */
export const ACTIONS = [
"Alexa.Actions.Open", "Alexa.Actions.Close", "Alexa.Actions.Raise", "Alexa.Actions.Lower",
"Alexa.Actions.SetEcoOn", "Alexa.Actions.SetEcoOff",
] as const;
export type ActionId = (typeof ACTIONS)[number];
/** The states a state mapping gives to a property value (alexa-discovery-objects.html, "StateMappings object"). */
export const STATES = [
"Alexa.States.Open", "Alexa.States.Closed", "Alexa.States.EcoOn", "Alexa.States.EcoOff", "Alexa.States.Low",
"Alexa.States.Empty", "Alexa.States.Full", "Alexa.States.Done", "Alexa.States.Stuck",
] as const;
export type StateId = (typeof STATES)[number];
/**
* The 56 display categories (alexa-discovery.html, "Display categories"). The DisplayCategory enum also keeps
* VEHICLE from 1.x, which the page no longer lists.
*/
export const DISPLAY_CATEGORIES = [
"ACTIVITY_TRIGGER", "AIR_CONDITIONER", "AIR_FRESHENER", "AIR_PURIFIER", "AIR_QUALITY_MONITOR",
"ALEXA_VOICE_ENABLED", "AUTO_ACCESSORY", "BLUETOOTH_SPEAKER", "CAMERA", "CHRISTMAS_TREE", "COFFEE_MAKER",
"COMPUTER", "CONTACT_SENSOR", "DISHWASHER", "DOOR", "DOORBELL", "DRYER", "EXTERIOR_BLIND", "FAN", "GAME_CONSOLE",
"GARAGE_DOOR", "HEADPHONES", "HUB", "INTERIOR_BLIND", "LAPTOP", "LIGHT", "MICROWAVE", "MOBILE_PHONE",
"MOTION_SENSOR", "MUSIC_SYSTEM", "NETWORK_HARDWARE", "OTHER", "OVEN", "PHONE", "PRINTER", "REMOTE", "ROUTER",
"SCENE_TRIGGER", "SCREEN", "SECURITY_PANEL", "SECURITY_SYSTEM", "SLOW_COOKER", "SMARTLOCK", "SMARTPLUG", "SPEAKER",
"STREAMING_DEVICE", "SWITCH", "TABLET", "TEMPERATURE_SENSOR", "THERMOSTAT", "TV", "VACUUM_CLEANER", "VACUUM",
"WASHER", "WATER_HEATER", "WEARABLE",
] as const;
export type DisplayCategoryName = (typeof DISPLAY_CATEGORIES)[number];
/** Not to be used as a friendly name (resources-and-assets.html, "Reserved words"). */
export const RESERVED_WORDS = [
"alarm", "alarms", "all alarms", "away mode", "bass", "camera", "date", "date today", "day", "do not disturb",
"drop in", "music", "night light", "notification", "playing", "sleep sounds", "time", "timer", "today in music",
"treble", "volume", "way f. m.",
] as const;
/**
* What a discovery answer may hold (alexa-discovery.html, "Interface limits"; alexa-discovery-objects.html,
* "Endpoint object details" and "AdditionalAttributes object details"; alexa-scenecontroller.html, "Discovery").
*/
export const LIMITS = {
endpointsPerCustomer: 300,
capabilitiesPerEndpoint: 100,
endpointIdLength: 256,
friendlyNameLength: 256,
sceneFriendlyNameLength: 128,
manufacturerNameLength: 128,
descriptionLength: 128,
additionalAttributeLength: 256,
cookieBytes: 5000,
} as const;
// alexa-errorresponse.html, "Error type values". The Interface column names <namespace>.ErrorResponse; the event
// header carries the namespace and the name ErrorResponse, as the examples on the ThermostatController,
// SecurityPanelController and Safety error pages show. The other namespaces are as the table spells them.
// INVALID_VALUE is listed under Alexa and under SmartVision.ObjectDetectionSensor, SUBSCRIPTION_REQUIRED under both
// SmartVision interfaces: the first one listed is kept.
const ERROR_TYPES_BY_NAMESPACE = {
"Alexa": [
"ALREADY_IN_OPERATION", "BRIDGE_UNREACHABLE", "CLOUD_CONTROL_DISABLED", "DEVICE_STUCK", "DO_NOT_DISTURB_MODE",
"ENDPOINT_BUSY", "ENDPOINT_CONTROL_UNAVAILABLE", "ENDPOINT_LOW_POWER", "ENDPOINT_UNREACHABLE",
"EXPIRED_AUTHORIZATION_CREDENTIAL", "FIRMWARE_OUT_OF_DATE", "HARDWARE_MALFUNCTION", "INSUFFICIENT_PERMISSIONS",
"INSUFFICIENT_RESOURCE", "INTERNAL_ERROR", "INVALID_AUTHORIZATION_CREDENTIAL", "INVALID_DIRECTIVE",
"INVALID_VALUE", "MAINTENANCE_REQUIRED", "NO_SUCH_ENDPOINT", "NOT_CALIBRATED", "NOT_IN_OPERATION",
"NOT_SUPPORTED_IN_CURRENT_MODE", "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE",
"PARTNER_APPLICATION_REDIRECTION", "POWER_LEVEL_NOT_SUPPORTED", "RATE_LIMIT_EXCEEDED",
"TEMPERATURE_VALUE_OUT_OF_RANGE", "TOO_MANY_FAILED_ATTEMPTS", "UNABLE_TO_CHARGE", "VALUE_OUT_OF_RANGE",
],
"Alexa.Commissionable.ReportCommissioningInformation": [
"FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS", "MAX_COMMISSIONING_LIMIT_REACHED",
],
"Alexa.Cooking": [
"CHILD_LOCK", "COOK_DURATION_TOO_LONG", "DOOR_CLOSED_TOO_LONG", "DOOR_OPEN", "PREHEAT_REQUIRED",
"PROBE_REQUIRED", "REMOTE_START_NOT_SUPPORTED", "REMOVE_PROBE", "REMOTE_START_DISABLED",
],
"Alexa.DataController": ["DATA_DELETION_NOT_SUPPORTED", "DATA_RETRIEVAL_NOT_SUPPORTED"],
"Alexa.Safety": ["OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED"],
"Alexa.SecurityPanelController": [
"AUTHORIZATION_REQUIRED", "BYPASS_NEEDED", "NOT_READY", "UNAUTHORIZED", "UNCLEARED_ALARM", "UNCLEARED_TROUBLE",
],
"Alexa.SmartVision.ObjectDetectionSensor": ["SUBSCRIPTION_REQUIRED"],
"Alexa.SmartVision.SnapshotProvider": ["DISABLED_BY_USER"],
"Alexa.ThermostatController": [
"DUAL_SETPOINTS_UNSUPPORTED", "REQUESTED_SETPOINTS_TOO_CLOSE", "THERMOSTAT_IS_OFF",
"TRIPLE_SETPOINTS_UNSUPPORTED", "UNSUPPORTED_THERMOSTAT_MODE", "UNWILLING_TO_SET_SCHEDULE",
"UNWILLING_TO_SET_VALUE",
],
"Alexa.ThermostatController.Configuration": [
"CONFIGURATION_UPDATE_NOT_ALLOWED", "COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE",
"COOLING_STAGES_EXCEEDS_LIMIT", "HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", "HEATING_STAGES_EXCEEDS_LIMIT",
"INVALID_AUXILIARY_HEATING_SYSTEM_TYPE", "INVALID_SYSTEM_TYPE", "INVALID_TARGET_STATE",
"INVALID_TEMPERATURE_SCALE", "INVALID_TERMINAL_CONNECTION", "MISSING_SETUP_INFORMATION",
],
"Alexa.ThermostatController.Schedule": ["INSUFFICIENT_SPACE"],
} as const;
/** The 73 error types of the table, each with the namespace its ErrorResponse goes under. */
export const ERROR_TYPES: Readonly<Record<string, string>> = Object.fromEntries(
Object.entries(ERROR_TYPES_BY_NAMESPACE).flatMap(([namespace, types]) => types.map((type) => [type, namespace]))
);

53
src/registry/index.ts Normal file
View file

@ -0,0 +1,53 @@
// The interfaces the library knows, by namespace.
import { Alexa } from "./interfaces/Alexa.js";
import { BrightnessController } from "./interfaces/BrightnessController.js";
import { EndpointHealth } from "./interfaces/EndpointHealth.js";
import { PowerController } from "./interfaces/PowerController.js";
import { TemperatureSensor } from "./interfaces/TemperatureSensor.js";
import { STUBS } from "./interfaces/stubs.js";
import { DeclarationError } from "./types.js";
import type { AnyDescriptor } from "./types.js";
const described: readonly AnyDescriptor[] = [
Alexa,
BrightnessController,
EndpointHealth,
PowerController,
TemperatureSensor,
];
const descriptors = new Map<string, AnyDescriptor>();
for (const descriptor of [...described, ...STUBS]) {
// A stub left in the table next to the descriptor that replaces it would win or lose by the order of this list
if (descriptors.has(descriptor.namespace)) throw new Error(`${descriptor.namespace} is described twice`);
descriptors.set(descriptor.namespace, descriptor);
}
export const registry = {
/** Whether an interface of this name is known. */
has(namespace: string): boolean {
return descriptors.has(namespace);
},
/** The descriptor of "Alexa.RangeController" or of AlexaInterfaceType.RANGE_CONTROLLER, which is that string. */
get(namespace: string): AnyDescriptor {
const descriptor = descriptors.get(namespace);
if (!descriptor) throw new DeclarationError({}, `${JSON.stringify(namespace)} is not an interface alex2node knows`);
return descriptor;
},
/** Every descriptor, ordered by namespace. */
list(): AnyDescriptor[] {
return [...descriptors.values()].sort((a, b) => (a.namespace < b.namespace ? -1 : 1));
},
};
export { Alexa, BrightnessController, EndpointHealth, PowerController, TemperatureSensor };
export { DeclarationError, defineInterface } from "./types.js";
export type {
AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor,
Label, PropertyDescriptor,
} from "./types.js";
export { s, SchemaError } from "./schema.js";
export type { Infer, Schema, Temperature, TimeInterval } from "./schema.js";
export * from "./catalog.js";

View file

@ -0,0 +1,21 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
/**
* The base interface: every endpoint lists it (alexa-interface.html, "Support the Alexa interface in all add-ons").
* Its one directive to a device is ReportState, answered with a StateReport.
*/
export const Alexa = defineInterface({
namespace: "Alexa",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-interface.html",
kind: "base",
tier: 1,
instanced: false,
properties: {},
directives: {
ReportState: { name: "ReportState", payload: s.object({}) },
},
// { type, interface, version } and nothing else, as in the discovery example of the page
discovery: () => ({ properties: false }),
});

View file

@ -0,0 +1,25 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
/** Both directives turn a light that is off on, at the brightness asked for (alexa-brightnesscontroller.html). */
export const BrightnessController = defineInterface({
namespace: "Alexa.BrightnessController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-brightnesscontroller.html",
kind: "controller",
tier: 1,
instanced: false,
properties: {
brightness: { name: "brightness", value: s.number({ min: 0, max: 100, integer: true }) },
},
directives: {
SetBrightness: {
name: "SetBrightness",
payload: s.object({ brightness: s.number({ min: 0, max: 100, integer: true }) }),
},
AdjustBrightness: {
name: "AdjustBrightness",
payload: s.object({ brightnessDelta: s.number({ min: -100, max: 100, integer: true }) }),
},
},
});

View file

@ -0,0 +1,31 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
export const CONNECTIVITY_REASONS = [
"WIFI_BAD_PASSWORD", "WIFI_AP_NOT_FOUND", "WIFI_ROUTER_UNREACHABLE", "WIFI_AP_CHANNEL_QUALITY_LOW",
"INTERNET_UNREACHABLE", "CAPTIVE_PORTAL_CHECK_FAILED", "UNKNOWN",
] as const;
/**
* Version 3.1, as the page is titled. It defines connectivity only. Examples on other pages declare 3 and 3.2;
* alex2node 1.x declared 3.3, which no page mentions.
*/
export const EndpointHealth = defineInterface({
namespace: "Alexa.EndpointHealth",
version: "3.1",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-endpointhealth.html",
kind: "sensor",
tier: 1,
instanced: false,
properties: {
connectivity: {
name: "connectivity",
value: s.object({
value: s.enum("OK", "UNREACHABLE"),
reason: s.optional(s.enum(...CONNECTIVITY_REASONS)),
}),
note: "in every Response, StateReport and ChangeReport; a change of it reported within three seconds",
},
},
directives: {},
});

View file

@ -0,0 +1,28 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
export const PowerController = defineInterface({
namespace: "Alexa.PowerController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-powercontroller.html",
kind: "controller",
tier: 1,
instanced: false,
properties: {
powerState: { name: "powerState", value: s.enum("ON", "OFF") },
},
directives: {
TurnOn: { name: "TurnOn", payload: s.object({}) },
TurnOff: { name: "TurnOff", payload: s.object({}) },
},
options: s.object({
// The directives Alexa asks the user to confirm before it sends them. Amazon supports this for devices in Japan
// only (alexa-discovery-objects.html, "VerificationsRequired object").
verificationsRequired: s.optional(s.array(s.enum("TurnOn", "TurnOff"), { min: 1 })),
}, { unknownKeys: "reject" }),
discovery({ options }) {
if (!options.verificationsRequired) return {};
const confirmed = options.verificationsRequired.map((directive) => ({ directive, methods: [{ "@type": "Confirmation" }] }));
return { topLevel: { verificationsRequired: confirmed } };
},
});

View file

@ -0,0 +1,16 @@
import { s } from "../schema.js";
import { defineInterface } from "../types.js";
/** A sensor: an endpoint that has it declares Alexa.EndpointHealth as well (alexa-temperaturesensor.html). */
export const TemperatureSensor = defineInterface({
namespace: "Alexa.TemperatureSensor",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-temperaturesensor.html",
kind: "sensor",
tier: 1,
instanced: false,
properties: {
temperature: { name: "temperature", value: s.temperature() },
},
directives: {},
});

View file

@ -0,0 +1,106 @@
// Interfaces without a descriptor of their own. A stub names the interface with the version and the properties
// alex2node 1.5.2 put in discovery, so an endpoint that declares one is announced as before. Nothing about it is
// checked, and the version is the one 1.5.2 sent, which is not always the one on the page of the interface
// (Alexa.Speaker is "3" there). A row leaves this table when a descriptor is written from that page.
import { s } from "../schema.js";
import type { AnyDescriptor } from "../types.js";
const DEVICE_APIS = "https://developer.amazon.com/docs/alexaplus/device-apis";
const LIST_OF_INTERFACES = `${DEVICE_APIS}/list-of-interfaces.html`;
// "As of August 7, 2026, Automotive skills and the associated Alexa.AuthorizationController and
// Alexa.Automotive.VehicleData APIs are no longer available."
const DEPRECATED_FEATURES = "https://developer.amazon.com/en-US/docs/alexa/ask-overviews/deprecated-features.html";
// namespace, version, properties, page (a name under device-apis/, or a URL)
type Row = readonly [string, string, readonly string[], string];
const TABLE: readonly Row[] = [
["Alexa.ApplicationStateReporter", "1", [], "https://developer.amazon.com/docs/alexaplus/alexa-voice-service/alexa-applicationstatereporter.html"],
["Alexa.Audio.PlayQueue", "1", [], "alexa-audio-playqueue.html"],
["Alexa.AuthorizationController", "1", [], DEPRECATED_FEATURES],
["Alexa.AutomationManagement", "1", ["automationStatuses"], "alexa-automationmanagement.html"],
["Alexa.Automotive.VehicleData", "1", [], DEPRECATED_FEATURES],
// 1.5.2 announced the version "UNKNOWN"; 1.7 is what the list of interfaces gives
["Alexa.Camera.LiveViewController", "1.7", [], LIST_OF_INTERFACES],
["Alexa.CameraStreamController", "3", [], "alexa-camerastreamcontroller.html"],
["Alexa.ChannelController", "3", ["channel"], "alexa-channelcontroller.html"],
["Alexa.ColorController", "3", ["color"], "alexa-colorcontroller.html"],
["Alexa.ColorTemperatureController", "3", ["colorTemperatureInKelvin"], "alexa-colortemperaturecontroller.html"],
["Alexa.Commissionable", "1", [], "alexa-commissionable.html"],
["Alexa.ConsentManagement.ConsentRequiredReporter", "1", [], "alexa-consentrequiredreporter.html"],
["Alexa.ContactSensor", "3", ["detectionState"], "alexa-contactsensor.html"],
["Alexa.Cooking", "1", [], "alexa-cooking.html"],
["Alexa.Cooking.FoodTemperatureController", "1", [], "alexa-cooking-foodtemperaturecontroller.html"],
["Alexa.Cooking.FoodTemperatureSensor", "1", [], "alexa-cooking-foodtemperaturesensor.html"],
["Alexa.Cooking.PresetController", "1", [], "alexa-cooking-presetcontroller.html"],
["Alexa.Cooking.TemperatureController", "1", [], "alexa-cooking-temperaturecontroller.html"],
["Alexa.Cooking.TemperatureSensor", "1", [], "alexa-cooking-temperaturesensor.html"],
["Alexa.Cooking.TimeController", "1", [], "alexa-cooking-timecontroller.html"],
["Alexa.DataController", "1", [], "alexa-datacontroller.html"],
["Alexa.DeviceUsage.Estimation", "1", [], "alexa-deviceusage-estimation.html"],
["Alexa.DeviceUsage.Meter", "1", [], "alexa-deviceusage-meter.html"],
["Alexa.DoorbellEventSource", "3", [], "alexa-doorbelleventsource.html"],
["Alexa.EqualizerController", "1", [], "alexa-equalizercontroller.html"],
["Alexa.InputController", "3", [], "alexa-inputcontroller.html"],
["Alexa.InventoryLevelSensor", "1", [], "alexa-inventorylevelsensor.html"],
["Alexa.InventoryLevelUsageSensor", "1", [], "alexa-inventorylevelusagesensor.html"],
["Alexa.InventoryUsageSensor", "1", [], "alexa-inventoryusagesensor.html"],
["Alexa.KeypadController", "1", [], "alexa-keypadcontroller.html"],
["Alexa.Launcher", "1.1", [], "alexa-launcher.html"],
["Alexa.LockController", "3", [], "alexa-lockcontroller.html"],
["Alexa.Media.PlayQueue", "1", [], "alexa-media-playqueue.html"],
["Alexa.Media.Playback", "1", [], "alexa-media-playback.html"],
["Alexa.Media.Search", "1", [], "alexa-media-search.html"],
["Alexa.ModeController", "3", ["mode"], "alexa-modecontroller.html"],
["Alexa.MotionSensor", "3", [], "alexa-motionsensor.html"],
["Alexa.PercentageController", "3", [], "alexa-percentagecontroller.html"],
["Alexa.PlaybackController", "3", [], "alexa-playbackcontroller.html"],
["Alexa.PlaybackStateReporter", "1", [], "alexa-playbackcontroller.html"],
["Alexa.PowerLevelController", "3", [], "alexa-powerlevelcontroller.html"],
["Alexa.ProactiveNotificationSource", "1", [], "alexa-proactivenotificationsource.html"],
["Alexa.RTCSessionController", "1", [], "alexa-rtcsessioncontroller.html"],
["Alexa.RangeController", "3", [], "alexa-rangecontroller.html"],
["Alexa.RecordController", "3", [], "alexa-recordcontroller.html"],
["Alexa.RemoteVideoPlayer", "1", [], "alexa-remotevideoplayer.html"],
["Alexa.SceneController", "3", [], "alexa-scenecontroller.html"],
["Alexa.SecurityPanelController", "1", [], "alexa-securitypanelcontroller.html"],
["Alexa.SecurityPanelController.Alert", "1", [], "alexa-securitypanelcontroller-alert.html"],
["Alexa.SeekController", "3", [], "alexa-seekcontroller.html"],
["Alexa.SimpleEventSource", "1", [], "alexa-simpleeventsource.html"],
["Alexa.SmartVision.ObjectDetectionSensor", "1", [], "alexa-smartvision-objectdetectionsensor.html"],
["Alexa.SmartVision.SnapshotProvider", "1", [], "alexa-smartvision-snapshotprovider.html"],
["Alexa.Speaker", "1", [], "alexa-speaker.html"],
["Alexa.StepSpeaker", "1", [], "alexa-stepspeaker.html"],
["Alexa.ThermostatController", "3.2", ["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"], "alexa-thermostatcontroller.html"],
["Alexa.ThermostatController.Configuration", "1", [], "alexa-thermostatcontroller-configuration.html"],
["Alexa.ThermostatController.HVAC.Components", "1", [], "alexa-thermostatcontroller-hvac-components.html"],
["Alexa.ThermostatController.Schedule", "1", [], "alexa-thermostatcontroller-schedule.html"],
// "UNKNOWN" in 1.5.2 as well; the page is titled "Interface 3"
["Alexa.TimeHoldController", "3", [], "alexa-timeholdcontroller.html"],
["Alexa.ToggleController", "3", ["toggleState"], "alexa-togglecontroller.html"],
["Alexa.UIController", "1", [], "alexa-uicontroller.html"],
["Alexa.UserPreference", "1", [], "alexa-userpreference.html"],
["Alexa.VideoRecorder", "1", [], "alexa-videorecorder.html"],
["Alexa.WakeOnLANController", "1", [], "alexa-wakeonlancontroller.html"],
];
function kindOf(namespace: string): AnyDescriptor["kind"] {
if (namespace.endsWith("Sensor")) return "sensor";
if (namespace.endsWith("EventSource")) return "eventSource";
return "controller";
}
function stub([namespace, version, properties, page]: Row): AnyDescriptor {
return {
namespace,
version,
doc: page.startsWith("https://") ? page : `${DEVICE_APIS}/${page}`,
kind: kindOf(namespace),
tier: 3,
instanced: false,
properties: Object.fromEntries(properties.map((name) => [name, { name, value: s.unknown() }])),
directives: {},
};
}
export const STUBS: readonly AnyDescriptor[] = TABLE.map(stub);

241
src/registry/schema.ts Normal file
View file

@ -0,0 +1,241 @@
// Values checked at run time and typed at compile time. A descriptor states its property values, directive payloads
// and declaration options with these; parse() returns the value or throws a SchemaError that names where it went
// wrong. Written here rather than taken from a package: mqtt stays the only runtime dependency.
/** A value that does not fit its schema: path is where ("payload.targetSetpoint.scale"), problem is what. */
export class SchemaError extends Error {
constructor(
readonly path: string,
readonly problem: string
) {
super(path ? `${path}: ${problem}` : problem);
this.name = "SchemaError";
}
}
export interface Schema<T> {
/** What the schema accepts, in the words of an error message: "an integer from 0 to 100", "ON | OFF". */
readonly expects: string;
/** The checked value. path names it in the error message. */
parse(input: unknown, path?: string): T;
}
/** A schema that accepts a missing value; s.object() makes its key optional in the inferred type. */
export interface OptionalSchema<T> extends Schema<T | undefined> {
readonly optional: true;
}
export interface EnumSchema<V> extends Schema<V> {
readonly values: readonly V[];
}
/** The type a schema produces. */
export type Infer<S> = S extends Schema<infer T> ? T : never;
export type Shape = Record<string, Schema<any>>;
type OptionalKeys<S extends Shape> = { [K in keyof S]: S[K] extends OptionalSchema<any> ? K : never }[keyof S];
export type InferShape<S extends Shape> = { [K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]> } & {
[K in OptionalKeys<S>]?: Infer<S[K]>;
};
export interface NumberRules {
min?: number;
max?: number;
/** Greater than, the bound itself excluded. */
gt?: number;
integer?: boolean;
}
export interface Temperature {
value: number;
scale: "CELSIUS" | "FAHRENHEIT" | "KELVIN";
}
export interface TimeInterval {
start?: string;
end?: string;
duration?: string;
}
// The input as an error message shows it: short, and quoted when it is text.
function shown(input: unknown): string {
if (input === undefined) return "nothing";
if (typeof input === "function") return "a function";
const text = JSON.stringify(input) ?? String(input);
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
}
function mismatch(path: string, expects: string, input: unknown): SchemaError {
return new SchemaError(path, `expected ${expects}, got ${shown(input)}`);
}
const at = (path: string, key: string): string => (path ? `${path}.${key}` : key);
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null && !Array.isArray(input);
}
function schema<T>(expects: string, accepts: (input: unknown) => boolean): Schema<T> {
return {
expects,
parse(input, path = "") {
if (!accepts(input)) throw mismatch(path, expects, input);
return input as T;
},
};
}
function numberText({ min, max, gt, integer }: NumberRules): string {
const kind = integer ? "an integer" : "a number";
if (min !== undefined && max !== undefined) return `${kind} from ${min} to ${max}`;
if (min !== undefined) return `${kind} of ${min} or more`;
if (max !== undefined) return `${kind} of ${max} or less`;
if (gt !== undefined) return `${kind} greater than ${gt}`;
return kind;
}
function number(rules: NumberRules = {}): Schema<number> {
const { min = -Infinity, max = Infinity, gt = -Infinity, integer = false } = rules;
return schema(numberText(rules), (input) =>
typeof input === "number" && Number.isFinite(input) && input >= min && input <= max && input > gt
&& (!integer || Number.isInteger(input)));
}
function string(rules: { min?: number; max?: number; pattern?: RegExp; expects?: string } = {}): Schema<string> {
const { min = 0, max = Infinity, pattern } = rules;
const length = max === Infinity ? (min > 0 ? ` of ${min} or more characters` : "") : ` of ${min} to ${max} characters`;
return schema(rules.expects ?? `a string${length}`, (input) =>
typeof input === "string" && input.length >= min && input.length <= max && (!pattern || pattern.test(input)));
}
function boolean(): Schema<boolean> {
return schema("true or false", (input) => typeof input === "boolean");
}
function literal<V extends string | number | boolean | null>(value: V): Schema<V> {
return schema(JSON.stringify(value), (input) => input === value);
}
function enumeration<V extends readonly string[]>(...values: V): EnumSchema<V[number]> {
return { ...schema<V[number]>(values.join(" | "), (input) => values.includes(input as string)), values };
}
function unknown(): Schema<unknown> {
return schema("any value", () => true);
}
function optional<T>(inner: Schema<T>): OptionalSchema<T> {
return {
expects: inner.expects,
optional: true,
parse: (input, path = "") => (input === undefined ? undefined : inner.parse(input, path)),
};
}
function nullable<T>(inner: Schema<T>): Schema<T | null> {
const expects = `${inner.expects} or null`;
return {
expects,
parse(input, path = "") {
if (input === null) return null;
try {
return inner.parse(input, path);
} catch (err) {
// The value itself is of the wrong kind: null was a choice too. An error further in keeps its own path.
if (err instanceof SchemaError && err.path === path) throw mismatch(path, expects, input);
throw err;
}
},
};
}
function array<T>(item: Schema<T>, rules: { min?: number; max?: number } = {}): Schema<T[]> {
const { min = 0, max = Infinity } = rules;
const count = max === Infinity ? (min > 0 ? ` with ${min} or more entries` : "") : ` with ${min} to ${max} entries`;
const expects = `a list${count}`;
return {
expects,
parse(input, path = "") {
if (!Array.isArray(input) || input.length < min || input.length > max) throw mismatch(path, expects, input);
return input.map((entry, i) => item.parse(entry, `${path}[${i}]`));
},
};
}
/**
* An object with the keys of shape. Keys the shape does not name are kept as they are: a field Alexa adds to a
* directive reaches the handler. unknownKeys "reject" is for what a developer writes, where such a key is a typo.
*/
function object<S extends Shape>(shape: S, rules: { unknownKeys?: "keep" | "reject" } = {}): Schema<InferShape<S>> {
const known = Object.keys(shape);
const expects = known.length > 0 ? `an object with ${known.join(", ")}` : "an object";
return {
expects,
parse(input, path = "") {
if (!isRecord(input)) throw mismatch(path, expects, input);
const others = Object.keys(input).filter((key) => !known.includes(key));
if (others.length > 0 && rules.unknownKeys === "reject") {
throw new SchemaError(at(path, others[0]), `unknown key, the known ones are ${known.join(", ") || "none"}`);
}
const parsed: Record<string, unknown> = {};
for (const key of known) {
const value = shape[key].parse(input[key], at(path, key));
if (value !== undefined) parsed[key] = value;
}
for (const key of others) parsed[key] = input[key];
return parsed as InferShape<S>;
},
};
}
// alexa-property-schemas.html "Temperature", "Temperature scales"
function temperature(): Schema<Temperature> {
return object({ value: number(), scale: enumeration("CELSIUS", "FAHRENHEIT", "KELVIN") });
}
// alexa-property-schemas.html "DateTime": UTC, no offsets. The seconds are optional here because the TimeInterval
// examples on the same page leave them out ("2017-10-04T14:00Z").
const DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?Z$/;
function dateTime(): Schema<string> {
return schema("a UTC time like 2017-08-30T01:18:21Z", (input) =>
typeof input === "string" && DATE_TIME.test(input) && !Number.isNaN(Date.parse(input)));
}
// alexa-property-schemas.html "Duration": the time portion of ISO 8601, negative for a delta ("PT-30S")
const DURATION = /^PT(?=.)(-?\d+H)?(-?\d+M)?(-?\d+S)?$/;
function duration(): Schema<string> {
return string({ pattern: DURATION, expects: "a duration like PT3M15S" });
}
// alexa-property-schemas.html "TimeInterval": "Specify one or two of the time interval fields. If you specify all
// three fields, an error occurs."
function timeInterval(): Schema<TimeInterval> {
const fields = object({ start: optional(dateTime()), end: optional(dateTime()), duration: optional(duration()) });
const expects = "a time interval with one or two of start, end, duration";
return {
expects,
parse(input, path = "") {
const interval = fields.parse(input, path);
const given = [interval.start, interval.end, interval.duration].filter((field) => field !== undefined).length;
if (given < 1 || given > 2) throw mismatch(path, expects, input);
return interval;
},
};
}
export const s = {
string,
number,
boolean,
literal,
enum: enumeration,
unknown,
optional,
nullable,
array,
object,
temperature,
dateTime,
duration,
timeInterval,
};

143
src/registry/types.ts Normal file
View file

@ -0,0 +1,143 @@
// What the library knows about one Alexa interface, as data. Discovery JSON, property values and directive payloads
// are produced and checked from a descriptor, so adding an interface is adding one file under interfaces/.
import type { Schema } from "./schema.js";
/**
* A friendly name: text in one locale, or an asset of the global Alexa catalog, which stands for several names in
* every language Alexa speaks (resources-and-assets.html, "Label object").
*/
export type Label =
| { "@type": "text"; value: { text: string; locale: string } }
| { "@type": "asset"; value: { assetId: string } };
export interface PropertyDescriptor<V> {
name: string;
value: Schema<V>;
note?: string;
}
export interface DirectiveDescriptor<T> {
name: string;
payload: Schema<T>;
/** Alexa sends the directive only to a capability declared like this (AdjustMode: an ordered mode). */
when?: (capability: Declared<any>) => boolean;
note?: string;
}
/** An event a device raises that is not a Response: ActivationStarted, DoorbellPress. */
export interface EventDescriptor {
name: string;
/** Default: the namespace of the interface. */
namespace?: string;
/** Default: the version of the interface. */
payloadVersion?: string;
payload: Schema<any>;
/** response: the answer to a directive. proactive: sent without one. */
topic: "response" | "proactive";
}
/** What an interface adds to the capability object in discovery, next to the fields every capability has. */
export interface CapabilityExtras {
configuration?: Record<string, unknown>;
/** Alexa.EqualizerController spells it in the plural. */
configurations?: Record<string, unknown>;
/** Fields of the capability object itself: supportsDeactivation, supportedOperations, inputs. */
topLevel?: Record<string, unknown>;
/** false: the capability has no properties object (the Alexa interface, a scene). */
properties?: false;
}
/** A capability as declared on an endpoint: what discovery() and validate() of its descriptor are given. */
export interface Declared<O = Record<string, unknown>> {
readonly endpointId: string;
readonly namespace: string;
/** "" when the interface is declared without an instance. */
readonly instance: string;
readonly friendlyNames: readonly Label[];
readonly retrievable: boolean;
readonly proactivelyReported: boolean;
readonly nonControllable?: boolean;
/** The options of the interface, as its options schema returned them. */
readonly options: O;
}
/** The endpoint a capability is declared on, as validate() sees it. */
export interface EndpointView {
readonly endpointId: string;
readonly friendlyName: string;
readonly description: string;
readonly displayCategories: readonly string[];
/** The other capabilities of the endpoint. */
readonly capabilities: readonly Declared[];
}
export type Properties = Record<string, PropertyDescriptor<any>>;
export type Directives = Record<string, DirectiveDescriptor<any>>;
export interface InterfaceDescriptor<
P extends Properties = Properties,
D extends Directives = Directives,
O = {},
I extends boolean = boolean
> {
/** "Alexa.RangeController" */
namespace: string;
/** As the title of the interface's page gives it: "3", "3.1", "1.0". */
version: string;
/** The page the descriptor was written from. */
doc: string;
kind: "base" | "controller" | "sensor" | "eventSource";
/** 1 and 2: described in full. 3: a stub, the namespace with its version and property names and nothing checked. */
tier: 1 | 2 | 3;
/** A generic controller: each capability needs an instance name and friendly names. */
instanced: I;
/** The properties the interface reports. */
properties: P;
/** The directives Alexa sends. */
directives: D;
events?: Record<string, EventDescriptor>;
/** What a declaration can set beyond the fields every capability has: a range, the supported modes. */
options?: Schema<O>;
discovery?: (capability: Declared<O>) => CapabilityExtras;
/** The event that answers a directive when it is not Alexa / Response (Arm -> Arm.Response). */
responseFor?: (directive: string) => { namespace: string; name: string; payload?: Schema<any> } | undefined;
/** The namespace an ErrorResponse goes under when its type is one of errorTypes. */
errorNamespace?: string;
errorTypes?: readonly string[];
/** Amazon documents a DeferredResponse for the interface. */
deferrable?: boolean;
/** Rules a schema cannot state. Throws DeclarationError. */
validate?: (capability: Declared<O>, endpoint: EndpointView) => void;
}
export type AnyDescriptor = InterfaceDescriptor<any, any, any, boolean>;
/** Types a descriptor from what it is given: the property names, the payload of each directive, the options. */
export function defineInterface<P extends Properties, D extends Directives, O = {}, I extends boolean = boolean>(
descriptor: InterfaceDescriptor<P, D, O, I>
): InterfaceDescriptor<P, D, O, I> {
return descriptor;
}
/** A device, a capability or an interface name declared in a way Alexa would reject. Thrown where it is declared. */
export class DeclarationError extends Error {
readonly endpointId?: string;
readonly namespace?: string;
readonly instance?: string;
readonly problem: string;
constructor(where: { endpointId?: string; namespace?: string; instance?: string }, problem: string) {
const capability = where.namespace && where.instance ? `${where.namespace} "${where.instance}"` : where.namespace;
super([where.endpointId && shortened(where.endpointId), capability, problem].filter(Boolean).join(": "));
this.name = "DeclarationError";
this.endpointId = where.endpointId;
this.namespace = where.namespace;
this.instance = where.instance || undefined;
this.problem = problem;
}
}
// An endpointId may be the very thing that is wrong, 300 characters of it.
function shortened(text: string): string {
return text.length > 64 ? `${text.slice(0, 61)}...` : text;
}

View file

@ -102,3 +102,15 @@ test("ThermostatController discovery: version 3.2, targetSetpoint listed with bo
["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"]
);
});
test("EndpointHealth discovery: version 3.1 with connectivity (1.x announced 3.3, which no Alexa page mentions)", async () => {
const { alexa, bridge } = await setup(Alex2MQTT, "root");
const lamp = bridge.registerDevice("Lamp", "lamp-1", DisplayCategory.LIGHT);
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
lamp.addCapability(AlexaInterfaceType.ENDPOINT_HEALTH, { proactivelyReported: true });
const [endpoint] = await discover(alexa, "root");
const health = endpoint.capabilities.find((c) => c.interface === "Alexa.EndpointHealth");
assert.equal(health.version, "3.1");
assert.deepEqual(health.properties, { retrievable: true, proactivelyReported: true, supported: [{ name: "connectivity" }] });
});

View file

@ -0,0 +1,22 @@
{
"directive": {
"header": {
"namespace": "Alexa.BrightnessController",
"name": "AdjustBrightness",
"messageId": "Unique version 4 UUID",
"correlationToken": "Opaque correlation token",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID",
"cookie": {}
},
"payload": {
"brightnessDelta": -25
}
}
}

View file

@ -0,0 +1,30 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "Response",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.BrightnessController",
"name": "brightness",
"value": 75,
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 1000
}
]
}
}

View file

@ -0,0 +1,42 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "ChangeReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {
"change": {
"cause": {
"type": "PHYSICAL_INTERACTION"
},
"properties": [
{
"namespace": "Alexa.BrightnessController",
"name": "brightness",
"value": 75,
"timeOfSample": "2024-02-03T16:17:00.00Z",
"uncertaintyInMilliseconds": 0
}
]
}
}
},
"context": {
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": "OK"
},
"timeOfSample": "2024-02-03T16:15:00.00Z",
"uncertaintyInMilliseconds": 0
}
}

View file

@ -0,0 +1,22 @@
{
"directive": {
"header": {
"namespace": "Alexa.BrightnessController",
"name": "SetBrightness",
"messageId": "Unique version 4 UUID",
"correlationToken": "Opaque correlation token",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID",
"cookie": {}
},
"payload": {
"brightness": 50
}
}
}

View file

@ -0,0 +1,30 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "Response",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.BrightnessController",
"name": "brightness",
"value": 50,
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 500
}
]
}
}

View file

@ -0,0 +1,30 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "StateReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.BrightnessController",
"name": "brightness",
"value": 75,
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 1000
}
]
}
}

View file

@ -0,0 +1,81 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Manufacturer of the endpoint",
"description": "Description to be shown in the Alexa app",
"friendlyName": "Living Room Light",
"displayCategories": [
"LIGHT"
],
"additionalAttributes": {
"manufacturer": "Manufacturer of the endpoint",
"model": "Model of the device",
"serialNumber": "Serial number of the device",
"firmwareVersion": "Firmware version of the device",
"softwareVersion": "Software version of the device",
"customIdentifier": "Optional custom identifier for the device"
},
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.BrightnessController",
"version": "3",
"properties": {
"supported": [
{
"name": "brightness"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.ColorController",
"version": "3",
"properties": {
"supported": [
{
"name": "color"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3",
"properties": {
"supported": [
{
"name": "connectivity"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -0,0 +1,7 @@
{
"name": "connectivity",
"value": {
"value": "UNREACHABLE",
"reason": "WIFI_BAD_PASSWORD"
}
}

View file

@ -0,0 +1,67 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Sample Manufacturer",
"description": "Description to be shown in the Alexa app",
"friendlyName": "Your device name, displayed in the Alexa app, for example Front Door>",
"displayCategories": [
"SMARTLOCK"
],
"additionalAttributes": {
"manufacturer": "Sample Manufacturer",
"model": "Sample Model",
"serialNumber": "Serial number of the device",
"firmwareVersion": "Firmware version of the device",
"softwareVersion": "Software version of the device",
"customIdentifier": "Optional custom identifier for the device"
},
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.LockController",
"version": "3",
"properties": {
"supported": [
{
"name": "lockState"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3.1",
"properties": {
"supported": [
{
"name": "connectivity"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -0,0 +1,45 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Manufacturer of the endpoint",
"description": "Description to be shown in the Alexa app",
"friendlyName": "Device name, displayed in the Alexa app",
"displayCategories": [
"LIGHT"
],
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [
{
"name": "powerState"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -0,0 +1,42 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "ChangeReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {
"change": {
"cause": {
"type": "PHYSICAL_INTERACTION"
},
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "ON",
"timeOfSample": "2024-05-01T09:32:05.05Z",
"uncertaintyInMilliseconds": 0
}
]
}
}
},
"context": {
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": "OK"
},
"timeOfSample": "2024-05-01T09:31:00.00Z",
"uncertaintyInMilliseconds": 0
}
}

View file

@ -0,0 +1,30 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "StateReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "OFF",
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 0
}
]
}
}

View file

@ -0,0 +1,20 @@
{
"directive": {
"header": {
"namespace": "Alexa.PowerController",
"name": "TurnOff",
"messageId": "Unique version 4 UUID",
"correlationToken": "Opaque correlation token",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID",
"cookie": {}
},
"payload": {}
}
}

View file

@ -0,0 +1,30 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "Response",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "OFF",
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 500
}
]
}
}

View file

@ -0,0 +1,20 @@
{
"directive": {
"header": {
"namespace": "Alexa.PowerController",
"name": "TurnOn",
"messageId": "Unique version 4 UUID",
"correlationToken": "Opaque correlation token",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID",
"cookie": {}
},
"payload": {}
}
}

View file

@ -0,0 +1,30 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "Response",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "ON",
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 500
}
]
}
}

View file

@ -0,0 +1,81 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Manufacturer of the endpoint",
"description": "Description to be shown in the Alexa app",
"friendlyName": "Living Room Light",
"displayCategories": [
"LIGHT"
],
"additionalAttributes": {
"manufacturer": "Manufacturer of the endpoint",
"model": "Model of the device",
"serialNumber": "Serial number of the device",
"firmwareVersion": "Firmware version of the device",
"softwareVersion": "Software version of the device",
"customIdentifier": "Optional custom identifier for the device"
},
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [
{
"name": "powerState"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.BrightnessController",
"version": "3",
"properties": {
"supported": [
{
"name": "brightness"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3",
"properties": {
"supported": [
{
"name": "connectivity"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

View file

@ -0,0 +1,66 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "ChangeReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {
"change": {
"cause": {
"type": "PHYSICAL_INTERACTION"
},
"properties": [
{
"namespace": "Alexa.ThermostatController",
"name": "targetSetpoint",
"value": {
"value": 18,
"scale": "CELSIUS"
},
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 500
}
]
}
}
},
"context": {
"properties": [
{
"namespace": "Alexa.TemperatureSensor",
"name": "temperature",
"value": {
"value": 19.1,
"scale": "CELSIUS"
},
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 1000
},
{
"namespace": "Alexa.ThermostatController",
"name": "thermostatMode",
"value": "COOL",
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 500
},
{
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": "OK"
},
"timeOfSample": "2017-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 0
}
]
}
}

View file

@ -0,0 +1,59 @@
{
"event": {
"header": {
"namespace": "Alexa",
"name": "StateReport",
"messageId": "Unique identifier, preferably a version 4 UUID",
"correlationToken": "Opaque correlation token that matches the request",
"payloadVersion": "3"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "OAuth2.0 bearer token"
},
"endpointId": "Endpoint ID"
},
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.TemperatureSensor",
"name": "temperature",
"value": {
"value": 19.9,
"scale": "CELSIUS"
},
"timeOfSample": "2024-01-06T09:00:00.05Z",
"uncertaintyInMilliseconds": 1000
},
{
"namespace": "Alexa.ThermostatController",
"name": "thermostatMode",
"value": "HEAT",
"timeOfSample": "2024-01-01T08:00:00.05Z",
"uncertaintyInMilliseconds": 500
},
{
"namespace": "Alexa.ThermostatController",
"name": "targetSetpoint",
"value": {
"value": 20,
"scale": "CELSIUS"
},
"timeOfSample": "2024-01-01T08:00:00.05Z",
"uncertaintyInMilliseconds": 500
},
{
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": "OK"
},
"timeOfSample": "2024-01-06T09:00:00.05Z",
"uncertaintyInMilliseconds": 0
}
]
}
}

View file

@ -0,0 +1,91 @@
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "Unique identifier, preferably a version 4 UUID"
},
"payload": {
"endpoints": [
{
"endpointId": "Unique ID of the endpoint",
"manufacturerName": "Manufacturer of the endpoint",
"description": "Smart Thermostat by Thermostat Maker",
"friendlyName": "Hallway Thermostat",
"displayCategories": [
"THERMOSTAT",
"TEMPERATURE_SENSOR"
],
"cookie": {},
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.TemperatureSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "temperature"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.ThermostatController",
"version": "3.1",
"properties": {
"supported": [
{
"name": "targetSetpoint"
},
{
"name": "lowerSetpoint"
},
{
"name": "upperSetpoint"
},
{
"name": "thermostatMode"
}
],
"proactivelyReported": true,
"retrievable": true
},
"configuration": {
"supportedModes": [
"HEAT",
"COOL",
"AUTO"
],
"supportsScheduling": false
}
},
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3",
"properties": {
"supported": [
{
"name": "connectivity"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
}
]
}
]
}
}
}

11
test/fixtures/interfaces.js vendored Normal file
View file

@ -0,0 +1,11 @@
"use strict";
// One entry per interface that is described in full: the page its examples were saved from (test/helpers/fixtures.js).
// test/registry/descriptors.test.js holds every entry against those examples and fails for a descriptor of tier 1 or
// 2 that has no entry here.
module.exports = [
{ namespace: "Alexa", page: "alexa-interface" },
{ namespace: "Alexa.BrightnessController", page: "alexa-brightnesscontroller" },
{ namespace: "Alexa.EndpointHealth", page: "alexa-endpointhealth" },
{ namespace: "Alexa.PowerController", page: "alexa-powercontroller" },
{ namespace: "Alexa.TemperatureSensor", page: "alexa-temperaturesensor" },
];

View file

@ -1,8 +1,14 @@
// Compiled, never run (test/typings.test.js, npm run check): a CommonJS TypeScript consumer of the declarations in
// dist/types, resolved through the package's "exports". A "@ts-expect-error" line fails the compile when the
// declaration stops rejecting what follows it.
import { ActionMapping, AlexaActions, AlexaInterfaceType, DisplayCategory, EndpointHealth, PowerController } from "alex2node";
import type { Alex2MQTT, Alex2MQTTOptions, AlexaInterface, AlexaStatusMessage, ChangeCause, Device, SupportedMode } from "alex2node";
import {
ActionMapping, AlexaActions, AlexaInterfaceType, Assets, DeclarationError, DisplayCategory, EndpointHealth, PowerController,
registry,
} from "alex2node";
import type {
Alex2MQTT, Alex2MQTTOptions, AlexaInterface, AlexaStatusMessage, AssetId, ChangeCause, Device, DisplayCategoryName, Infer,
InterfaceDescriptor, Schema, SupportedMode, Temperature, UnitOfMeasure,
} from "alex2node";
declare const bridge: Alex2MQTT;
declare const message: AlexaStatusMessage;
@ -33,4 +39,24 @@ const sent: Promise<string> = device.getChangeReport(cause).addPowerControllerPr
// @ts-expect-error not a cause Alexa knows
device.getChangeReport("BUTTON");
export { options, added, chained, sent };
// The registry: a descriptor by its namespace or by the 1.x enum member
const described: InterfaceDescriptor = registry.get("Alexa.PowerController");
const version: string = registry.get(AlexaInterfaceType.ENDPOINT_HEALTH).version;
const refused: Error = new DeclarationError({ endpointId: "lamp-1", namespace: described.namespace }, "declared twice");
// The vocabularies are unions of what the pages list
const opening: AssetId = Assets[0];
const percent: UnitOfMeasure = "Alexa.Unit.Percent";
const vacuum: DisplayCategoryName = "VACUUM";
// @ts-expect-error not in the global Alexa catalog
const misspelt: AssetId = "Alexa.Setting.Openning";
// @ts-expect-error an asset, but not a unit of measure
const notAUnit: UnitOfMeasure = "Alexa.Setting.Opening";
// A schema types what it parses
declare const temperature: Schema<Temperature>;
const measured: Infer<typeof temperature> = { value: 20, scale: "CELSIUS" };
// @ts-expect-error not a temperature scale
const rankine: Infer<typeof temperature> = { value: 20, scale: "RANKINE" };
export { options, added, chained, sent, version, refused, opening, percent, vacuum, misspelt, notAUnit, measured, rankine };

47
test/helpers/fixtures.js Normal file
View file

@ -0,0 +1,47 @@
"use strict";
// The JSON examples of Amazon's interface pages. test/fixtures/alexa-docs/<page>/ holds the examples of
// https://developer.amazon.com/docs/alexaplus/device-apis/<page>.html as the page printed them on 2026-09-28, one
// file per example, named after the heading it stands under.
const fs = require("node:fs");
const path = require("node:path");
const DOCS = path.join(__dirname, "..", "fixtures", "alexa-docs");
/** One example: doc("alexa-powercontroller", "TurnOn.directive"). */
function doc(page, name) {
return JSON.parse(fs.readFileSync(path.join(DOCS, page, `${name}.json`), "utf8"));
}
/** The names of the examples of a page that end in suffix: examples("alexa-powercontroller", ".directive"). */
function examples(page, suffix = "") {
return fs.readdirSync(path.join(DOCS, page))
.filter((file) => file.endsWith(`${suffix}.json`))
.map((file) => file.slice(0, -".json".length))
.sort();
}
/** The capability objects of one interface in a discovery example, in the order of the example. */
function capabilities(page, namespace, name = "discovery") {
const [endpoint] = doc(page, name).event.payload.endpoints;
return endpoint.capabilities.filter((capability) => capability.interface === namespace);
}
/**
* Every value of a property of one interface that the examples of a page show, as { example, property }: what the
* Response, StateReport and ChangeReport examples report, and the property examples of the page itself.
*/
function reported(page, namespace) {
const found = [];
for (const example of examples(page)) {
const { event, context, ...property } = doc(page, example);
if (example.endsWith(".property")) found.push({ example, property: { namespace, ...property } });
if (!event) continue;
const change = event.payload && event.payload.change;
// Several ChangeReport examples print the context as one property instead of { properties: [...] }
const contextProperties = context && (context.properties || (context.namespace ? [context] : []));
found.push(...[...(change ? change.properties : []), ...(contextProperties || [])].map((each) => ({ example, property: each })));
}
return found.filter(({ property }) => property.namespace === namespace);
}
module.exports = { doc, examples, capabilities, reported };

View file

@ -0,0 +1,72 @@
"use strict";
// The vocabularies copied from Amazon's pages: their sizes as counted on the pages on 2026-09-28, and how they
// relate to the 1.x enums.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const {
Actions, AlexaActions, AlexaErrorType, Assets, DisplayCategories, DisplayCategory, States, Units,
} = require("alex2node");
const { ERROR_TYPES, RESERVED_WORDS, LIMITS } = require("../../dist/cjs/registry/catalog.js");
const unique = (list) => new Set(list).size === list.length;
test("assets: the 103 ids of the global Alexa catalog, 23 of them units of measure", () => {
assert.equal(Assets.length, 103);
assert.ok(unique(Assets));
for (const id of Assets) assert.match(id, /^Alexa\.(Actions|Button|DeviceName|Gestures?|Setting|Shower|Unit|Value)\.[A-Za-z0-9.]+$/);
assert.equal(Units.length, 23);
assert.deepEqual(Units, Assets.filter((id) => id.startsWith("Alexa.Unit.")));
assert.ok(Units.includes("Alexa.Unit.Percent"));
// The page has "Gesture" seven times and "Gestures" once
assert.deepEqual(Assets.filter((id) => id.startsWith("Alexa.Gestures.")), ["Alexa.Gestures.DoubleTap"]);
});
test("semantics: six actions, the ones the AlexaActions enum of 1.x has, and nine states", () => {
assert.deepEqual([...Actions].sort(), Object.values(AlexaActions).sort());
assert.deepEqual(States.map((id) => id.replace("Alexa.States.", "")), ["Open", "Closed", "EcoOn", "EcoOff", "Low", "Empty", "Full", "Done", "Stuck"]);
});
test("display categories: the 56 of the page; the enum has them all and keeps VEHICLE", () => {
assert.equal(DisplayCategories.length, 56);
assert.ok(unique(DisplayCategories));
const inEnum = Object.values(DisplayCategory);
assert.deepEqual(DisplayCategories.filter((category) => !inEnum.includes(category)), []);
assert.deepEqual(inEnum.filter((category) => !DisplayCategories.includes(category)), ["VEHICLE"]);
assert.equal(DisplayCategory.VACUUM, "VACUUM");
for (const [key, value] of Object.entries(DisplayCategory)) assert.equal(key, value);
});
test("error types: the 73 of the error type table, each under the namespace of its interface", () => {
assert.equal(Object.keys(ERROR_TYPES).length, 73);
assert.equal(ERROR_TYPES.ENDPOINT_UNREACHABLE, "Alexa");
assert.equal(ERROR_TYPES.INVALID_VALUE, "Alexa");
assert.equal(ERROR_TYPES.THERMOSTAT_IS_OFF, "Alexa.ThermostatController");
assert.equal(ERROR_TYPES.UNAUTHORIZED, "Alexa.SecurityPanelController");
assert.equal(ERROR_TYPES.OBSTACLE_DETECTED, "Alexa.Safety");
assert.equal(ERROR_TYPES.TEMPERATURE_VALUE_OUT_OF_RANGE, "Alexa", "the generic one, not a thermostat error");
const namespaces = [...new Set(Object.values(ERROR_TYPES))].sort();
assert.equal(namespaces.length, 11);
for (const namespace of namespaces) assert.match(namespace, /^Alexa(\.[A-Za-z]+)*$/);
// AlexaErrorType of 1.x has every type of the table and two that the table does not list
const inEnum = Object.values(AlexaErrorType);
assert.deepEqual(Object.keys(ERROR_TYPES).filter((type) => !inEnum.includes(type)), []);
assert.deepEqual(inEnum.filter((type) => !(type in ERROR_TYPES)).sort(), ["EXCEEDED_PIN_ATTEMPTS", "PIN_SETUP_REQUIRED"]);
});
test("reserved words and limits", () => {
assert.equal(RESERVED_WORDS.length, 22);
assert.ok(unique(RESERVED_WORDS));
for (const word of RESERVED_WORDS) assert.equal(word, word.toLowerCase());
assert.deepEqual(LIMITS, {
endpointsPerCustomer: 300,
capabilitiesPerEndpoint: 100,
endpointIdLength: 256,
friendlyNameLength: 256,
sceneFriendlyNameLength: 128,
manufacturerNameLength: 128,
descriptionLength: 128,
additionalAttributeLength: 256,
cookieBytes: 5000,
});
});

View file

@ -0,0 +1,127 @@
"use strict";
// The registry: every descriptor is well formed, the ones described in full agree with the examples of their page,
// and every interface name of 1.x resolves.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { registry, AlexaInterface, AlexaInterfaceType, DeclarationError } = require("alex2node");
const interfaces = require("../fixtures/interfaces.js");
const { doc, examples, capabilities, reported } = require("../helpers/fixtures.js");
const described = registry.list().filter((descriptor) => descriptor.tier !== 3);
const stubs = registry.list().filter((descriptor) => descriptor.tier === 3);
test("every descriptor has a namespace, a version, a kind and the page it was written from", () => {
const namespaces = registry.list().map((descriptor) => descriptor.namespace);
assert.deepEqual(namespaces, [...namespaces].sort(), "list() is ordered by namespace");
for (const descriptor of registry.list()) {
const { namespace } = descriptor;
assert.match(namespace, /^Alexa(\.[A-Za-z]+)*$/);
assert.match(descriptor.version, /^\d+(\.\d+)?$/, namespace);
assert.match(descriptor.doc, /^https:\/\/developer\.amazon\.com\/[\w\-./]+\.html$/, namespace);
assert.ok(["base", "controller", "sensor", "eventSource"].includes(descriptor.kind), namespace);
assert.ok([1, 2, 3].includes(descriptor.tier), namespace);
assert.equal(typeof descriptor.instanced, "boolean", namespace);
for (const [key, property] of Object.entries(descriptor.properties)) assert.equal(property.name, key, namespace);
for (const [key, directive] of Object.entries(descriptor.directives)) assert.equal(directive.name, key, namespace);
}
});
test("a descriptor of tier 1 or 2 has an entry in test/fixtures/interfaces.js, and every entry a descriptor", () => {
assert.deepEqual(described.map((descriptor) => descriptor.namespace), interfaces.map((entry) => entry.namespace).sort());
});
for (const { namespace, page } of interfaces) {
test(`${namespace}: version, properties, directive payloads and property values of ${page}.html`, () => {
const descriptor = registry.get(namespace);
assert.ok(descriptor.doc.endsWith(`/${page}.html`), descriptor.doc);
for (const name of examples(page).filter((example) => example.startsWith("discovery"))) {
for (const capability of capabilities(page, namespace, name)) {
assert.equal(capability.version, descriptor.version, `${name}: version`);
const supported = capability.properties ? capability.properties.supported.map((property) => property.name) : [];
for (const property of supported) assert.ok(property in descriptor.properties, `${name}: ${property}`);
}
}
const directives = examples(page, ".directive").map((name) => doc(page, name).directive);
assert.deepEqual(
directives.map((directive) => directive.header.name).sort(),
Object.keys(descriptor.directives).filter((name) => name !== "ReportState").sort(),
"the page has an example of every directive"
);
for (const { header, payload } of directives) {
assert.equal(header.namespace, namespace);
assert.deepEqual(descriptor.directives[header.name].payload.parse(payload, "payload"), payload);
}
const values = reported(page, namespace);
for (const { example, property } of values) {
const known = descriptor.properties[property.name];
assert.ok(known, `${example}: ${property.name} is not a property of the descriptor`);
assert.deepEqual(known.value.parse(property.value, property.name), property.value, example);
}
if (Object.keys(descriptor.properties).length > 0) assert.ok(values.length > 0, "the page reports no property of the interface");
});
}
test("Alexa.EndpointHealth is version 3.1 and reports connectivity, with a reason when it has one", () => {
const { version, properties } = registry.get("Alexa.EndpointHealth");
assert.equal(version, "3.1");
assert.deepEqual(Object.keys(properties), ["connectivity"]);
const unreachable = doc("alexa-endpointhealth", "connectivity.property").value;
assert.deepEqual(properties.connectivity.value.parse(unreachable), { value: "UNREACHABLE", reason: "WIFI_BAD_PASSWORD" });
assert.deepEqual(properties.connectivity.value.parse({ value: "OK" }), { value: "OK" });
assert.throws(
() => properties.connectivity.value.parse({ value: "UNREACHABLE", reason: "UNPLUGGED" }, "connectivity"),
/^SchemaError: connectivity\.reason: expected WIFI_BAD_PASSWORD \| .* \| UNKNOWN, got "UNPLUGGED"$/
);
});
test("a payload or a value that does not fit is refused with its path", () => {
const { directives, properties } = registry.get("Alexa.BrightnessController");
assert.throws(
() => directives.SetBrightness.payload.parse({ brightness: "lots" }, "payload"),
/^SchemaError: payload\.brightness: expected an integer from 0 to 100, got "lots"$/
);
assert.throws(() => directives.AdjustBrightness.payload.parse({ brightnessDelta: 101 }, "payload"), /from -100 to 100/);
assert.throws(() => properties.brightness.value.parse(-1, "brightness"), /^SchemaError: brightness: expected an integer/);
assert.throws(() => registry.get("Alexa.PowerController").properties.powerState.value.parse("on", "powerState"), /expected ON \| OFF/);
});
test("registry.get: the namespace and the AlexaInterfaceType member give the same descriptor; an unknown name throws", () => {
assert.equal(registry.get(AlexaInterfaceType.POWER_CONTROLLER), registry.get("Alexa.PowerController"));
assert.equal(registry.has("Alexa.PowerController"), true);
assert.equal(registry.has("Alexa.TeleportController"), false);
assert.throws(
() => registry.get("Alexa.TeleportController"),
(err) => err instanceof DeclarationError && err.message === '"Alexa.TeleportController" is not an interface alex2node knows'
);
assert.throws(() => registry.get(AlexaInterfaceType.UNKNOWN), DeclarationError);
});
test("every AlexaInterfaceType of 1.x is in the registry: described, or a stub with the version and properties of 1.5.2", () => {
const names = Object.values(AlexaInterfaceType).filter((name) => name !== AlexaInterfaceType.UNKNOWN);
assert.equal(names.length, 69);
assert.deepEqual(names.filter((name) => !registry.has(name)), []);
// "Alexa" is the one interface that was never in the enum
assert.deepEqual(registry.list().map((descriptor) => descriptor.namespace).filter((name) => !names.includes(name)), ["Alexa"]);
for (const stub of stubs) {
assert.deepEqual(stub.directives, {}, stub.namespace);
assert.equal(stub.instanced, false, stub.namespace);
}
const thermostat = registry.get("Alexa.ThermostatController");
assert.equal(thermostat.version, "3.2");
assert.deepEqual(Object.keys(thermostat.properties), ["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"]);
});
test("AlexaInterface.getVersion() and getProps() read the registry", () => {
for (const descriptor of registry.list().filter(({ namespace }) => namespace !== "Alexa")) {
const capability = new AlexaInterface(descriptor.namespace);
assert.equal(capability.getVersion(), descriptor.version, descriptor.namespace);
assert.deepEqual(capability.getProps(), Object.keys(descriptor.properties), descriptor.namespace);
}
// 1.5.2 had no version for these two and put "UNKNOWN" in discovery
assert.equal(new AlexaInterface(AlexaInterfaceType.TIME_HOLD_CONTROLLER).getVersion(), "3");
assert.equal(new AlexaInterface(AlexaInterfaceType.CAMERA_LIVE_VIEW_CONTROLLER).getVersion(), "1.7");
});

View file

@ -0,0 +1,111 @@
"use strict";
// The schema helper of the registry: what parse() returns, and what the error says when the value does not fit.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { SchemaError } = require("alex2node");
// The helper is not part of the public surface; the descriptors are built with it.
const { s } = require("../../dist/cjs/registry/schema.js");
// The message of the SchemaError that parse() throws.
function refusal(schema, input, path) {
try {
schema.parse(input, path);
} catch (err) {
assert.ok(err instanceof SchemaError, `threw ${err}`);
return err.message;
}
return assert.fail(`${JSON.stringify(input)} was accepted`);
}
test("number: bounds, integers, and no NaN or text", () => {
const percent = s.number({ min: 0, max: 100, integer: true });
assert.equal(percent.parse(0), 0);
assert.equal(percent.parse(100), 100);
assert.equal(refusal(percent, 101, "payload.brightness"), "payload.brightness: expected an integer from 0 to 100, got 101");
assert.equal(refusal(percent, 50.5, "payload.brightness"), "payload.brightness: expected an integer from 0 to 100, got 50.5");
assert.equal(refusal(percent, "lots", "payload.brightness"), 'payload.brightness: expected an integer from 0 to 100, got "lots"');
assert.equal(refusal(s.number(), NaN, "value"), "value: expected a number, got null");
assert.equal(refusal(s.number({ gt: 0 }), 0, "range.precision"), "range.precision: expected a number greater than 0, got 0");
assert.equal(s.number({ gt: 0 }).parse(0.5), 0.5);
});
test("enum and literal: the value itself, the choices in the message", () => {
const power = s.enum("ON", "OFF");
assert.equal(power.parse("ON"), "ON");
assert.deepEqual(power.values, ["ON", "OFF"]);
assert.equal(refusal(power, "on", "powerState"), 'powerState: expected ON | OFF, got "on"');
assert.equal(s.literal("FOUR_DIGIT_PIN").parse("FOUR_DIGIT_PIN"), "FOUR_DIGIT_PIN");
assert.equal(refusal(s.literal(3), "3", "version"), 'version: expected 3, got "3"');
});
test("object: the path of the key that is wrong; a key that is not in the shape is kept", () => {
const payload = s.object({ rangeValueDelta: s.number(), rangeValueDeltaDefault: s.boolean() });
assert.deepEqual(payload.parse({ rangeValueDelta: -5, rangeValueDeltaDefault: false }), { rangeValueDelta: -5, rangeValueDeltaDefault: false });
assert.equal(
refusal(payload, { rangeValueDelta: -5 }, "payload"),
"payload.rangeValueDeltaDefault: expected true or false, got nothing"
);
assert.equal(refusal(payload, [1, 2], "payload"), "payload: expected an object with rangeValueDelta, rangeValueDeltaDefault, got [1,2]");
assert.equal(refusal(payload, null, "payload"), "payload: expected an object with rangeValueDelta, rangeValueDeltaDefault, got null");
// A field Alexa adds to a directive later reaches the handler
assert.deepEqual(s.object({}).parse({ added: 1 }), { added: 1 });
});
test("object with unknownKeys \"reject\": a misspelt option is named, with the ones that exist", () => {
const options = s.object({ range: s.unknown(), presets: s.optional(s.unknown()) }, { unknownKeys: "reject" });
assert.equal(refusal(options, { range: {}, preset: [] }), "preset: unknown key, the known ones are range, presets");
assert.deepEqual(options.parse({ range: {} }), { range: {} });
});
test("optional, nullable, array", () => {
assert.equal(s.optional(s.number()).parse(undefined), undefined);
assert.equal(refusal(s.optional(s.number()), null, "delta"), "delta: expected a number, got null");
assert.equal(s.nullable(s.string()).parse(null), null);
assert.equal(refusal(s.nullable(s.string()), 5, "mode"), "mode: expected a string or null, got 5");
const modes = s.array(s.object({ value: s.string() }), { min: 2 });
assert.deepEqual(modes.parse([{ value: "a" }, { value: "b" }]), [{ value: "a" }, { value: "b" }]);
assert.equal(refusal(modes, [{ value: "a" }], "supportedModes"), 'supportedModes: expected a list with 2 or more entries, got [{"value":"a"}]');
assert.equal(refusal(modes, [{ value: "a" }, { value: 2 }], "supportedModes"), "supportedModes[1].value: expected a string, got 2");
});
test("temperature: the three scales of alexa-property-schemas.html, the scale named when it is wrong", () => {
const temperature = s.temperature();
assert.deepEqual(temperature.parse({ value: 68.0, scale: "FAHRENHEIT" }), { value: 68, scale: "FAHRENHEIT" });
assert.deepEqual(temperature.parse({ value: 293.15, scale: "KELVIN" }), { value: 293.15, scale: "KELVIN" });
assert.equal(
refusal(temperature, { value: 20, scale: "Celsius" }, "payload.targetSetpoint"),
'payload.targetSetpoint.scale: expected CELSIUS | FAHRENHEIT | KELVIN, got "Celsius"'
);
assert.equal(refusal(temperature, 20, "temperature"), "temperature: expected an object with value, scale, got 20");
});
test("dateTime and duration: the examples of alexa-property-schemas.html", () => {
for (const time of ["2017-08-30T01:18:21Z", "2017-08-30T01:18:21.123Z", "2017-10-04T14:00Z"]) assert.equal(s.dateTime().parse(time), time);
for (const time of ["2017-08-30T01:18:21+02:00", "2017-08-30 01:18:21Z", "2017-13-40T01:18:21Z", 1504055901]) {
assert.match(refusal(s.dateTime(), time, "start"), /^start: expected a UTC time like 2017-08-30T01:18:21Z, got /);
}
for (const length of ["PT3M15S", "PT-30S", "PT30M", "PT1H"]) assert.equal(s.duration().parse(length), length);
for (const length of ["PT", "3M", "P1D", "PT3M15"]) {
assert.match(refusal(s.duration(), length, "duration"), /^duration: expected a duration like PT3M15S, got /);
}
});
test("timeInterval: one or two of start, end and duration", () => {
const interval = s.timeInterval();
for (const example of [
{ start: "2017-10-04T14:00Z", end: "2017-10-04T14:15Z" },
{ start: "2017-10-04T14:00Z", duration: "PT30M" },
{ duration: "PT30M" },
]) assert.deepEqual(interval.parse(example), example);
const all = { start: "2017-10-04T14:00Z", end: "2017-10-04T14:15Z", duration: "PT15M" };
assert.match(refusal(interval, all, "holdUntil"), /^holdUntil: expected a time interval with one or two of start, end, duration, got /);
assert.match(refusal(interval, {}, "holdUntil"), /^holdUntil: expected a time interval with one or two of start, end, duration, got \{\}$/);
assert.equal(refusal(interval, { end: "tomorrow" }, "holdUntil"), 'holdUntil.end: expected a UTC time like 2017-08-30T01:18:21Z, got "tomorrow"');
});
test("an error without a path is the problem alone, and a long value is cut short", () => {
assert.equal(refusal(s.boolean(), "yes"), 'expected true or false, got "yes"');
const message = refusal(s.number(), "x".repeat(200), "value");
assert.equal(message.length, "value: expected a number, got ".length + 60);
assert.ok(message.endsWith("..."));
});