Alex2Node/test/fixtures/types.ts
David 94b13f733a errors: typed helpers for every documented error type
AlexaErrors has a helper for each of the 73 types of the table on
alexa-errorresponse.html, named after the type in camel case: 63 take the
message only, 10 take the payload fields their page documents. A field the
page requires (reason, currentDeviceMode, currentChargeState, resourceType)
is a required argument, so leaving it out does not compile; from JavaScript
the helper throws a TypeError that lists the values. validRange is optional,
as the page says, and has both ends when given.
temperatureOutOfRange and setpointsTooClose stay as shorter names.
The helpers are compared with the 15 examples of the four error pages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 21:14:55 +00:00

189 lines
10 KiB
TypeScript

// 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, Assets, BrightnessController, DeclarationError, DisplayCategory, EndpointHealth,
ModeController, PowerController, PowerState, RangeController, TemperatureSensor, ToggleController, asset, registry, semantics, text,
AlexaErrors, StateBuilder, messages,
} from "alex2node";
import type {
Alex2MQTT, Alex2MQTTOptions, AlexaInterface, AlexaStatusMessage, AssetId, Capability, CapabilityJson, ChangeCause, Device,
DisplayCategoryName, EndpointJson, Infer, InterfaceDescriptor, Label, Schema, SupportedMode, Temperature, UnitOfMeasure,
} from "alex2node";
declare const bridge: Alex2MQTT;
declare const message: AlexaStatusMessage;
declare const capability: AlexaInterface;
const options: Alex2MQTTOptions = { host: "mqtt://127.0.0.1:1883", mqtt: { reconnectPeriod: 1000 }, log: (line) => line.length };
const device: Device = bridge.registerDevice("Lamp", "lamp-1", [DisplayCategory.LIGHT, DisplayCategory.SWITCH]);
const added: AlexaInterface = device.addCapability(AlexaInterfaceType.POWER_CONTROLLER, { proactivelyReported: true });
// ActionMapping: the payload is optional, and an object
const close = new ActionMapping([AlexaActions.Close], "TurnOn");
capability.addActionMapping(close);
capability.addActionMapping(new ActionMapping([AlexaActions.Open], "SetRangeValue", { rangeValue: 100 }));
// @ts-expect-error a payload is an object or, as in 1.x, a string
capability.addActionMapping(new ActionMapping([AlexaActions.Open], "SetRangeValue", 100));
// addHealthProp: the enum or its string values, chainable
const chained: AlexaStatusMessage = message.addHealthProp(EndpointHealth.OK).addHealthProp("UNREACHABLE", 50);
// @ts-expect-error not a connectivity value
message.addHealthProp("ASLEEP");
// @ts-expect-error a power state is ON or OFF
message.addPowerControllerProp("ON ");
message.addPowerControllerProp(PowerController.ON);
// PowerController is the descriptor of the interface as well; the enum alone is PowerState
const on: PowerController = PowerState.ON;
const namespace: string = PowerController.namespace;
const reachable: EndpointHealth = EndpointHealth.OK;
// addSupportedModes: Mode objects, plain strings still accepted
const mode: SupportedMode = { value: "Fan.Auto", modeResources: { friendlyNames: [{ "@type": "text", value: { text: "Auto", locale: "en-US" } }] } };
capability.addSupportedModes([mode, "Fan.On"]);
const cause: ChangeCause = "PHYSICAL_INTERACTION";
const sent: Promise<string> = device.getChangeReport(cause).addPowerControllerProp(PowerController.OFF).send();
// @ts-expect-error not a cause Alexa knows
device.getChangeReport("BUTTON");
// bridge.addDevice() and device.add(): the options of the interface and the ones every capability has
const blinds: Device = bridge.addDevice({
endpointId: "bedroom-blinds",
name: "Bedroom Blinds",
categories: ["INTERIOR_BLIND", DisplayCategory.OTHER],
cookie: { room: "bedroom" },
endpointHealth: false,
});
// @ts-expect-error an endpoint has categories
bridge.addDevice({ endpointId: "lamp-2", name: "Lamp" });
// @ts-expect-error not a display category
bridge.addDevice({ endpointId: "lamp-2", name: "Lamp", categories: ["LAMP"] });
const power: Capability = blinds.add(PowerController, { proactivelyReported: true, verificationsRequired: ["TurnOff"] });
blinds.add(BrightnessController);
blinds.add(TemperatureSensor, { retrievable: true, nonControllable: true });
// @ts-expect-error not a directive of the interface
blinds.add(PowerController, { verificationsRequired: ["Toggle"] });
// @ts-expect-error not an option of the interface
blinds.add(BrightnessController, { range: { min: 0, max: 100 } });
const announced: CapabilityJson = power.toJSON();
const endpoint: EndpointJson = blinds.getJSON();
const problems: string[] = blinds.check();
// Friendly names
const names: Label[] = [asset("Alexa.Setting.Opening"), text("Position"), text("Posición", "es-MX")];
// @ts-expect-error not in the global Alexa catalog
asset("Alexa.Setting.Openning");
// The generic controllers: an instance name, friendly names and the options of the interface
const lift = blinds.add(RangeController, {
instance: "Blind.Lift",
friendlyNames: names,
range: { min: 0, max: 100, precision: 1 },
unit: "Alexa.Unit.Percent",
presets: [{ value: 100, friendlyNames: [asset("Alexa.Value.Maximum")] }],
semantics: semantics()
.action("Close", "SetRangeValue", { rangeValue: 0 })
.action(["Open", AlexaActions.Raise], "SetRangeValue", { rangeValue: 100 })
.state("Closed", 0)
.stateRange("Open", 1, 100),
retrievable: true,
proactivelyReported: true,
});
blinds.add(ModeController, {
instance: "Blind.Position",
friendlyNames: [text("Position")],
supportedModes: [{ value: "Position.Up", friendlyNames: [asset("Alexa.Value.Open")] }, { value: "Position.Down", friendlyNames: [asset("Alexa.Value.Close")] }],
ordered: false,
});
blinds.add(ToggleController, { instance: "Blind.Tilt", friendlyNames: [text("Tilt")], nonControllable: true });
// @ts-expect-error a range controller has a range
blinds.add(RangeController, { instance: "Blind.Tilt", friendlyNames: [text("Tilt")] });
// @ts-expect-error a generic controller has an instance name and friendly names
blinds.add(ToggleController, { proactivelyReported: true });
// @ts-expect-error and so it cannot be declared without options
blinds.add(ToggleController);
// @ts-expect-error not a unit of measure
blinds.add(RangeController, { instance: "Blind.Tilt", friendlyNames: [text("Tilt")], range: { min: 0, max: 90, precision: 1 }, unit: "Alexa.Unit.Percentage" });
// @ts-expect-error not a phrase Alexa has
semantics().action("Shut", "TurnOff");
// What a directive carries and what a property holds are typed by the descriptor
const lower: Infer<typeof RangeController.directives.AdjustRangeValue.payload> = { rangeValueDelta: -10, rangeValueDeltaDefault: false };
const position: Schema<number> = lift.descriptor.properties.rangeValue.value;
// @ts-expect-error a mode is a string, or null when none is set
const current: Infer<typeof ModeController.properties.mode.value> = 3;
// 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, on, namespace, reachable, announced, endpoint, problems, names, lower, position, current, version,
refused, opening, percent, vacuum, misspelt, notAUnit, measured, rankine,
};
// StateBuilder.set(): the properties of the interface and the type of each value
const state = new StateBuilder()
.set(PowerController, "powerState", "ON")
.set(TemperatureSensor, "temperature", { value: 70, scale: "FAHRENHEIT" })
.set(RangeController, "rangeValue", 3, { instance: "Blind.Lift", timeOfSample: new Date() })
.health("UNREACHABLE", "WIFI_AP_NOT_FOUND");
// @ts-expect-error a power state is ON or OFF
state.set(PowerController, "powerState", "on");
// @ts-expect-error not a property of the interface
state.set(PowerController, "power", "ON");
// The builders take what the builder of the state collected; a message is typed down to its payload
const report = messages.changeReport({ endpointId: "lamp-1", cause: "APP_INTERACTION", changed: state.change, context: state.context });
const changedNames: string[] = report.event.payload.change.properties.map((property) => property.name);
// @ts-expect-error a DeferredResponse has no context
messages.deferredResponse({ endpointId: "lamp-1", correlationToken: "ct" }).context;
// @ts-expect-error the range is two numbers
AlexaErrors.valueOutOfRange("too high", { minimumValue: 1 });
// @ts-expect-error a temperature has a scale
AlexaErrors.temperatureValueOutOfRange("too cold", { minimumValue: 15, maximumValue: 30 });
// @ts-expect-error the reason is required
AlexaErrors.endpointControlUnavailable("no connectivity package");
// @ts-expect-error the mode is one of the page's
AlexaErrors.notSupportedInCurrentMode("the lamp shows a color", "PARTY");
// @ts-expect-error the state of the battery is required
AlexaErrors.notSupportedWithCurrentBatteryChargeState("charging");
// @ts-expect-error the resource is required
AlexaErrors.insufficientResource("the tank is empty");
// @ts-expect-error a type without fields takes the message only
AlexaErrors.thermostatIsOff("the thermostat is off", { validRange: { minimumValue: 1, maximumValue: 2 } });
// @ts-expect-error not a type of the table
AlexaErrors.thermostatIsOn("the thermostat is on");
const plainErrors: string[] = [
AlexaErrors.endpointUnreachable("the lamp does not answer"), AlexaErrors.obstacleDetected("something is in the way"),
AlexaErrors.valueOutOfRange("too high"), AlexaErrors.maintenanceRequired("the bin is full", "EMPTY_BIN"),
].map((error) => error.type);
const errorNamespace: string = AlexaErrors.setpointsTooClose("too close", { value: 2, scale: "CELSIUS" }).namespace;
// Handlers: the directives of the interface by name, the payload of each typed by its descriptor
lift.on("SetRangeValue", (ctx) => {
const target: number = ctx.payload.rangeValue;
return ctx.respond((s) => s.set(lift, "rangeValue", target));
});
lift.on("*", (ctx) => ctx.error("INVALID_DIRECTIVE", `${ctx.name} is not supported`));
// @ts-expect-error not a directive of the interface
lift.on("SetRange", (ctx) => ctx.respond());
// @ts-expect-error the payload of SetRangeValue has no brightness
lift.on("SetRangeValue", (ctx) => ctx.payload.brightness);
device.state((s) => s.health("OK")).onReportState((ctx) => ctx.report());