Six descriptors written from their pages replace the last stubs of tiers 1 and 2. A scene answers Activate and Deactivate through ctx.respond() with ActivationStarted and DeactivationStarted, the time and the cause filled in. device.raise(descriptor, name, payload) publishes DoorbellPress and the Event of a button on <root>/event with the endpoint and a new messageId; it throws a MessageError for an interface or instance the device did not declare, an event that answers a directive, a payload that does not fit and a message over 16000 bytes. TurnOn of a device with WakeOnLANController is deferred without the warning. On the wire: a doorbell has no properties object and proactivelyReported on the capability; SimpleEventSource is version 1.0, InventoryLevelSensor and WakeOnLANController version 3 (1.5.2: 1). A scene declared without options is announced as before. Alex2MQTT has no topic yet for the WakeUp event. 24 examples of the six pages are saved as fixtures. 267 tests pass, 241 before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
380 lines
17 KiB
JavaScript
380 lines
17 KiB
JavaScript
"use strict";
|
|
// One entry per interface that is described in full: the page its examples were saved from (test/helpers/fixtures.js)
|
|
// and declarations that give capability objects of Amazon's discovery examples.
|
|
//
|
|
// namespace the interface
|
|
// page the page of the interface; test/fixtures/alexa-docs/<page>/ has its examples
|
|
// declared [{ options, example, page, n }]: device.add(descriptor, options) gives the n-th capability object of
|
|
// the interface in that discovery example. n counts from 0 and defaults to it, page defaults to the
|
|
// page of the interface. The pages of RangeController and ModeController fold their discovery
|
|
// examples away, so they were not saved: the range is taken from an example of the ToggleController
|
|
// page, and ModeController has none.
|
|
// refused [[directive, payload, message]]: a payload the directive does not take, and the message of the
|
|
// INVALID_VALUE that answers it
|
|
// unfit [[property, value]]: a value the property does not take
|
|
// chosen true: a declaration lists the properties the device has, an example some of those of the interface
|
|
// folded the directives whose example the page folds away, so that it was not saved
|
|
// endpoint { categories, description } of the endpoint the interface is declared on, where it has rules for them
|
|
//
|
|
// A directive is answered with a Response, or with the event responseFor() of its descriptor names.
|
|
//
|
|
// test/registry/descriptors.test.js, test/device/discovery.test.js and test/dispatch/interfaces.test.js run the
|
|
// entries. The first one fails for a descriptor of tier 1 or 2 that has no entry.
|
|
const { asset, semantics, text } = require("alex2node");
|
|
|
|
const reported = { retrievable: true, proactivelyReported: true };
|
|
const declared = [{ options: reported, example: "discovery" }];
|
|
const inThreeLanguages = (english, spanish, french) => [text(english, "en-US"), text(spanish, "es-MX"), text(french, "fr-CA")];
|
|
|
|
module.exports = [
|
|
{ namespace: "Alexa", page: "alexa-interface", declared: [{ options: {}, example: "discovery" }] },
|
|
{
|
|
namespace: "Alexa.ChannelController",
|
|
page: "alexa-channelcontroller",
|
|
// The page folds its two discovery examples away; the one of its AddOrUpdateReport has the same endpoint
|
|
declared: [{ options: reported, example: "discovery-addOrUpdateReport" }],
|
|
refused: [
|
|
["ChangeChannel", { channel: { number: 9 } }, "payload.channel.number: expected a string, got 9"],
|
|
["ChangeChannel", { channel: "KCTS" }, 'payload.channel: expected an object with number, callSign, affiliateCallSign, uri, got "KCTS"'],
|
|
["SkipChannels", { channelCount: 1.5 }, "payload.channelCount: expected an integer, got 1.5"],
|
|
],
|
|
unfit: [["channel", "9"], ["channel", { number: 9 }]],
|
|
},
|
|
{ namespace: "Alexa.BrightnessController", page: "alexa-brightnesscontroller", declared: [{ options: reported, example: "discovery" }] },
|
|
{
|
|
namespace: "Alexa.ColorController",
|
|
page: "alexa-colorcontroller",
|
|
declared,
|
|
refused: [
|
|
["SetColor", { color: { hue: 361, saturation: 1, brightness: 1 } }, "payload.color.hue: expected a number from 0 to 360, got 361"],
|
|
["SetColor", { color: "red" }, 'payload.color: expected an object with hue, saturation, brightness, got "red"'],
|
|
],
|
|
unfit: [["color", { hue: 120, saturation: 50, brightness: 1 }], ["color", { hue: 120, saturation: 1 }]],
|
|
},
|
|
{
|
|
namespace: "Alexa.ColorTemperatureController",
|
|
page: "alexa-colortemperaturecontroller",
|
|
declared,
|
|
refused: [
|
|
["SetColorTemperature", { colorTemperatureInKelvin: 900 }, "payload.colorTemperatureInKelvin: expected an integer from 1000 to 10000, got 900"],
|
|
["IncreaseColorTemperature", "a lot", 'payload: expected an object, got "a lot"'],
|
|
],
|
|
unfit: [["colorTemperatureInKelvin", 10001], ["colorTemperatureInKelvin", "warm"]],
|
|
},
|
|
{ namespace: "Alexa.ContactSensor", page: "alexa-contactsensor", declared, unfit: [["detectionState", "OPEN"], ["detectionState", true]] },
|
|
{
|
|
namespace: "Alexa.DoorbellEventSource",
|
|
page: "alexa-doorbelleventsource",
|
|
endpoint: { categories: ["CAMERA", "DOORBELL"] },
|
|
declared: [{ options: {}, example: "discovery" }],
|
|
},
|
|
{ namespace: "Alexa.EndpointHealth", page: "alexa-endpointhealth", declared: [{ options: reported, example: "discovery" }] },
|
|
{
|
|
namespace: "Alexa.EqualizerController",
|
|
page: "alexa-equalizercontroller",
|
|
declared: [{
|
|
example: "discovery",
|
|
options: {
|
|
...reported,
|
|
bands: { supported: ["BASS", "MIDRANGE", "TREBLE"], range: { minimum: -6, maximum: 6 } },
|
|
modes: ["MOVIE", "MUSIC", "SPORT"],
|
|
},
|
|
}],
|
|
refused: [
|
|
["SetMode", { mode: "OPERA" }, 'payload.mode: expected MOVIE | MUSIC | NIGHT | SPORT | TV, got "OPERA"'],
|
|
["SetBands", { bands: [{ name: "BASS", value: 1.5 }] }, "payload.bands[0].value: expected an integer, got 1.5"],
|
|
["AdjustBands", { bands: [{ name: "BASS", levelDelta: 3 }] }, "payload.bands[0].levelDirection: expected UP | DOWN, got nothing"],
|
|
["ResetBands", { bands: [] }, "payload.bands: expected a list with 1 or more entries, got []"],
|
|
],
|
|
unfit: [["mode", "OPERA"], ["bands", { name: "BASS", value: 0 }], ["bands", [{ name: "SUB", value: 0 }]]],
|
|
},
|
|
{ namespace: "Alexa.HumiditySensor", page: "alexa-humiditysensor", declared, unfit: [["relativeHumidity", 45], ["relativeHumidity", { value: 101 }]] },
|
|
{
|
|
namespace: "Alexa.InputController",
|
|
page: "alexa-inputcontroller",
|
|
// The two discovery examples of the page spell the friendlyNames of HDMI 1 "friendNames". The fixtures have the
|
|
// name of the table of the page.
|
|
declared: [{
|
|
example: "discovery",
|
|
options: {
|
|
...reported,
|
|
inputs: [{ name: "HDMI 1", friendlyNames: ["AnyCompany TV"] }, { name: "HDMI 2", friendlyNames: ["Cable", "AnyCompany Cable"] }],
|
|
},
|
|
}],
|
|
refused: [
|
|
["SelectInput", { input: "HDMI 11" }, 'payload.input: expected an input like HDMI 1, got "HDMI 11"'],
|
|
["SelectInput", {}, "payload.input: expected an input like HDMI 1, got nothing"],
|
|
],
|
|
unfit: [["input", "hdmi 1"], ["input", 1]],
|
|
},
|
|
{
|
|
namespace: "Alexa.InventoryLevelSensor",
|
|
page: "alexa-inventorylevelsensor",
|
|
declared: [
|
|
{
|
|
example: "discovery",
|
|
options: {
|
|
retrievable: false,
|
|
proactivelyReported: true,
|
|
instance: "InkSensor.Cyan",
|
|
friendlyNames: [text("Cyan ink", "en-US"), text("Encre cyan", "fr-FR")],
|
|
measurement: { "@type": "Volume", unit: "MILLILITER" },
|
|
replenishment: { "@type": "DashReplenishmentId", value: "replenishment ID for refill options" },
|
|
},
|
|
},
|
|
{
|
|
example: "discovery",
|
|
n: 1,
|
|
options: {
|
|
retrievable: false,
|
|
proactivelyReported: true,
|
|
instance: "PaperSensor.FrontTray",
|
|
friendlyNames: [text("Front tray", "en-US")],
|
|
measurement: { "@type": "Count" },
|
|
replenishment: { "@type": "DashReplenishmentId", value: "replenishment ID for refill options" },
|
|
},
|
|
},
|
|
],
|
|
unfit: [
|
|
["level", 5],
|
|
["level", { "@type": "Volume", value: 5 }],
|
|
["level", { "@type": "Count", value: 200, unit: "SHEET" }],
|
|
["level", { "@type": "Length", value: 5, unit: "METER" }],
|
|
],
|
|
},
|
|
{
|
|
namespace: "Alexa.LockController",
|
|
page: "alexa-lockcontroller",
|
|
declared,
|
|
refused: [["Lock", "now", 'payload: expected an object, got "now"']],
|
|
unfit: [["lockState", "OPEN"], ["lockState", "locked"]],
|
|
},
|
|
{ namespace: "Alexa.ModeController", page: "alexa-modecontroller", declared: [] },
|
|
{ namespace: "Alexa.MotionSensor", page: "alexa-motionsensor", declared, unfit: [["detectionState", "MOTION"], ["detectionState", 1]] },
|
|
{
|
|
namespace: "Alexa.PercentageController",
|
|
page: "alexa-percentagecontroller",
|
|
declared,
|
|
refused: [
|
|
["SetPercentage", { percentage: 101 }, "payload.percentage: expected an integer from 0 to 100, got 101"],
|
|
["AdjustPercentage", { percentageDelta: 0.5 }, "payload.percentageDelta: expected an integer from -100 to 100, got 0.5"],
|
|
],
|
|
unfit: [["percentage", -1], ["percentage", 50.5]],
|
|
},
|
|
{
|
|
namespace: "Alexa.PlaybackController",
|
|
page: "alexa-playbackcontroller",
|
|
declared: [{
|
|
example: "discovery",
|
|
options: { supportedOperations: ["Play", "Pause", "Stop", "Next", "Previous", "FastForward", "Rewind", "StartOver"] },
|
|
}],
|
|
refused: [["Play", "now", 'payload: expected an object, got "now"']],
|
|
},
|
|
{
|
|
namespace: "Alexa.PlaybackStateReporter",
|
|
page: "alexa-playbackcontroller",
|
|
declared,
|
|
unfit: [["playbackState", "PLAYING"], ["playbackState", { state: "REWINDING" }]],
|
|
},
|
|
{ namespace: "Alexa.PowerController", page: "alexa-powercontroller", declared: [{ options: reported, example: "discovery" }] },
|
|
{
|
|
namespace: "Alexa.PowerLevelController",
|
|
page: "alexa-powerlevelcontroller",
|
|
declared,
|
|
refused: [
|
|
["SetPowerLevel", { powerLevel: "half" }, 'payload.powerLevel: expected an integer from 0 to 100, got "half"'],
|
|
["AdjustPowerLevel", {}, "payload.powerLevelDelta: expected an integer from -100 to 100, got nothing"],
|
|
],
|
|
unfit: [["powerLevel", 101], ["powerLevel", null]],
|
|
},
|
|
{
|
|
namespace: "Alexa.RangeController",
|
|
page: "alexa-rangecontroller",
|
|
declared: [{
|
|
page: "alexa-togglecontroller",
|
|
example: "discovery-range-and-toggle",
|
|
options: {
|
|
...reported,
|
|
instance: "SampleManufacturer.Fan.Speed",
|
|
friendlyNames: [asset("Alexa.Setting.FanSpeed"), ...inThreeLanguages("Speed", "Velocidad", "Vitesse")],
|
|
range: { min: 1, max: 10, precision: 1 },
|
|
presets: [
|
|
{
|
|
value: 10,
|
|
friendlyNames: [
|
|
asset("Alexa.Value.Maximum"), asset("Alexa.Value.High"), text("Highest"), text("Fast"), text("Alta", "es-MX"),
|
|
text("Élevée", "fr-CA"),
|
|
],
|
|
},
|
|
{
|
|
value: 1,
|
|
friendlyNames: [
|
|
asset("Alexa.Value.Minimum"), asset("Alexa.Value.Low"), text("Lowest"), text("Slow"), text("Baja", "es-MX"),
|
|
text("Faible", "fr-CA"),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}],
|
|
},
|
|
{
|
|
namespace: "Alexa.SceneController",
|
|
page: "alexa-scenecontroller",
|
|
endpoint: { categories: ["SCENE_TRIGGER"], description: "Party scene by Sample Vendor" },
|
|
declared: [{ options: { supportsDeactivation: false }, example: "discovery" }],
|
|
},
|
|
{
|
|
namespace: "Alexa.SecurityPanelController",
|
|
page: "alexa-securitypanelcontroller",
|
|
chosen: true,
|
|
declared: [{
|
|
example: "discovery",
|
|
options: {
|
|
...reported,
|
|
supportedArmStates: ["ARMED_AWAY", "ARMED_STAY", "DISARMED"],
|
|
supportedAuthorizationTypes: ["FOUR_DIGIT_PIN"],
|
|
alarms: ["burglaryAlarm", "fireAlarm"],
|
|
},
|
|
}],
|
|
refused: [
|
|
["Arm", { armState: "ARMED" }, 'payload.armState: expected ARMED_AWAY | ARMED_STAY | ARMED_NIGHT | DISARMED, got "ARMED"'],
|
|
["Arm", { armState: "ARMED_STAY", bypassType: "BYPASS_SOME" }, 'payload.bypassType: expected BYPASS_ALL, got "BYPASS_SOME"'],
|
|
["Disarm", { authorization: { type: "FOUR_DIGIT_PIN", value: "12345" } }, 'payload.authorization.value: expected a PIN of four digits, got "12345"'],
|
|
["Disarm", { authorization: { type: "VOICE", value: "1234" } }, 'payload.authorization.type: expected FOUR_DIGIT_PIN, got "VOICE"'],
|
|
],
|
|
unfit: [["armState", "ARMED"], ["burglaryAlarm", "ALARM"], ["fireAlarm", { value: "FIRE" }]],
|
|
},
|
|
// The page folds its discovery example away
|
|
{ namespace: "Alexa.SimpleEventSource", page: "alexa-simpleeventsource", declared: [] },
|
|
{
|
|
namespace: "Alexa.Speaker",
|
|
page: "alexa-speaker",
|
|
declared,
|
|
refused: [
|
|
["SetVolume", { volume: 101 }, "payload.volume: expected an integer from 0 to 100, got 101"],
|
|
["AdjustVolume", { volume: -20 }, "payload.volumeDefault: expected true or false, got nothing"],
|
|
["SetMute", { mute: "yes" }, 'payload.mute: expected true or false, got "yes"'],
|
|
],
|
|
unfit: [["volume", 50.5], ["volume", -1], ["muted", "no"]],
|
|
},
|
|
{
|
|
namespace: "Alexa.StepSpeaker",
|
|
page: "alexa-stepspeaker",
|
|
declared: [{ options: {}, example: "discovery" }],
|
|
refused: [
|
|
["AdjustVolume", { volumeSteps: 101 }, "payload.volumeSteps: expected an integer from -100 to 100, got 101"],
|
|
["SetMute", {}, "payload.mute: expected true or false, got nothing"],
|
|
],
|
|
},
|
|
{ namespace: "Alexa.TemperatureSensor", page: "alexa-temperaturesensor", declared: [{ options: reported, example: "discovery" }] },
|
|
{
|
|
namespace: "Alexa.ThermostatController",
|
|
page: "alexa-thermostatcontroller",
|
|
chosen: true,
|
|
declared: [
|
|
{
|
|
example: "discovery",
|
|
options: {
|
|
...reported,
|
|
supportedModes: ["HEAT", "COOL", "AUTO", "EM_HEAT"],
|
|
supportsScheduling: true,
|
|
properties: ["lowerSetpoint", "upperSetpoint", "thermostatMode", "adaptiveRecoveryStatus"],
|
|
},
|
|
},
|
|
{
|
|
page: "alexa-thermostatcontroller-schedule",
|
|
example: "discovery",
|
|
options: { ...reported, supportedModes: ["HEAT", "COOL", "AUTO", "OFF"], supportsScheduling: true, properties: ["thermostatMode"] },
|
|
},
|
|
],
|
|
refused: [
|
|
["SetTargetTemperature", { targetSetpoint: { value: 20, scale: "RANKINE" } }, 'payload.targetSetpoint.scale: expected CELSIUS | FAHRENHEIT | KELVIN, got "RANKINE"'],
|
|
[
|
|
"SetTargetTemperature",
|
|
{ holdUntil: { duration: "PT30M" } },
|
|
'payload: expected one or more of targetSetpoint, lowerSetpoint, upperSetpoint, got {"holdUntil":{"duration":"PT30M"}}',
|
|
],
|
|
[
|
|
"SetTargetTemperature",
|
|
{ targetSetpoint: { value: 20, scale: "CELSIUS" }, holdUntil: { duration: "30 minutes" } },
|
|
'payload.holdUntil.duration: expected a duration like PT3M15S, got "30 minutes"',
|
|
],
|
|
["AdjustTargetTemperature", { targetSetpointDelta: 2 }, "payload.targetSetpointDelta: expected an object with value, scale, got 2"],
|
|
["SetThermostatMode", { thermostatMode: { value: "WARM" } }, 'payload.thermostatMode.value: expected AUTO | COOL | ECO | EM_HEAT | HEAT | OFF, got "WARM"'],
|
|
["SetThermostatMode", { thermostatMode: "HEAT" }, 'payload.thermostatMode: expected an object with value, got "HEAT"'],
|
|
],
|
|
unfit: [["thermostatMode", "EMERGENCY"], ["targetSetpoint", 20], ["upperSetpoint", { value: 20 }], ["adaptiveRecoveryStatus", "ACTIVE"]],
|
|
},
|
|
{
|
|
namespace: "Alexa.ThermostatController.Schedule",
|
|
page: "alexa-thermostatcontroller-schedule",
|
|
folded: ["SetWeeklySchedule"],
|
|
declared: [{
|
|
example: "discovery",
|
|
options: { ...reported, supportedFanModes: ["ON", "AUTO"], supportsAdaptiveRecovery: true, maxEntryPerDay: 4 },
|
|
}],
|
|
refused: [
|
|
["SetScheduleState", { scheduleEnabled: "yes" }, 'payload.scheduleEnabled: expected true or false, got "yes"'],
|
|
["SetAdaptiveRecovery", {}, "payload.adaptiveRecoveryEnabled: expected true or false, got nothing"],
|
|
["SetWeeklySchedule", { weeklySchedule: { temperatureScale: "CELSIUS" } }, "payload.weeklySchedule.Monday: expected a list, got nothing"],
|
|
],
|
|
unfit: [["scheduleEnabled", "ON"], ["adaptiveRecoveryEnabled", 1]],
|
|
},
|
|
{
|
|
// The ChangeReport example of the page reports "2018-05-31T14:02.32Z", which is no time: it was not saved
|
|
namespace: "Alexa.TimeHoldController",
|
|
page: "alexa-timeholdcontroller",
|
|
declared: [{ options: { ...reported, allowRemoteResume: true }, example: "discovery" }],
|
|
refused: [["Hold", 10, "payload: expected an object, got 10"]],
|
|
unfit: [["holdStartTime", "14:30"], ["holdEndTime", "2018-05-31T14:40:00+02:00"]],
|
|
},
|
|
{
|
|
namespace: "Alexa.ToggleController",
|
|
page: "alexa-togglecontroller",
|
|
declared: [
|
|
{
|
|
example: "discovery-oven",
|
|
options: {
|
|
...reported,
|
|
nonControllable: false,
|
|
instance: "Oven.OvenLight",
|
|
friendlyNames: inThreeLanguages("Oven light", "Luz del horno", "Lumière du four"),
|
|
},
|
|
},
|
|
{
|
|
example: "discovery-oven",
|
|
n: 1,
|
|
options: {
|
|
...reported,
|
|
nonControllable: true,
|
|
instance: "Stovetop.ResidualHeat",
|
|
friendlyNames: inThreeLanguages("Stovetop is still hot", "Todavía está caliente", "Encore chaude"),
|
|
},
|
|
},
|
|
{
|
|
example: "discovery-semantics",
|
|
options: {
|
|
...reported,
|
|
instance: "GarbageCan.Lid",
|
|
friendlyNames: inThreeLanguages("Garbage can lid", "Tapa del bote de basura", "Couvercle de poubelle"),
|
|
semantics: semantics().action("Close", "TurnOff", {}).action("Open", "TurnOn", {}).state("Closed", "OFF").state("Open", "ON"),
|
|
},
|
|
},
|
|
{
|
|
example: "discovery-range-and-toggle",
|
|
options: {
|
|
...reported,
|
|
instance: "SampleManufacturer.Fan.Oscillate",
|
|
friendlyNames: [
|
|
asset("Alexa.Setting.Oscillate"), text("Rotate"), text("Rotation"), text("Girar", "es-MX"), text("Rotation", "fr-CA"),
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
namespace: "Alexa.WakeOnLANController",
|
|
page: "alexa-wakeonlancontroller",
|
|
declared: [{ options: { macAddresses: ["00-14-22-01-23-45"] }, example: "discovery" }],
|
|
},
|
|
];
|