src/dispatcher.ts routes a directive to capability.on(name | "*"), device.onDirective() or device.onReportState(), with the payload checked by the descriptor of the interface. The DirectiveContext answers with respond/report/defer/error; respond() and report() start from device.state(). A handler or 1.x listener that throws or rejects is answered with INTERNAL_ERROR (an AlexaError with itself) and reported through "error", never as an unhandled rejection. Undeclared interface, unknown directive, AdjustMode on an unordered mode and a missing handler get INVALID_DIRECTIVE, a bad payload INVALID_VALUE; a device with an "Event" or "ReportState" listener keeps the answer to itself. Every answer passes the dispatcher: the first one per correlationToken is published, a second is refused. No answer within answerWithinMs (6500, 0 = off, unref'd timer) sends INTERNAL_ERROR and emits "unanswered". A messageId that arrived in the last 60 s is dropped. New: "unknownEndpoint", options answerUnknownEndpoints, publisher, timers, and bridge.receive() to run a bridge without a broker. 168 tests pass (18 new). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
171 lines
9.4 KiB
TypeScript
171 lines
9.4 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 });
|
|
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());
|