registry: RangeController, ModeController, ToggleController with resources, presets and semantics

The three generic controllers are described in full and leave the stub table (62 stubs remain). A declaration
takes an instance name, friendly names from text() and asset(), and the options of the interface: range, unit and
presets for a range; supportedModes and ordered for a mode; semantics() for all three, with actions mapped to
directives and states mapped to a value or a range of values.

Refused at device.add(), each with the endpoint, interface and instance in the message: no instance name or no
friendly name; an instance declared twice; a first friendly name or a phrase used by another capability of the
endpoint; a reserved word, an asset id that is not in the catalog; range.max not above range.min; a preset off the
precision grid or outside the range; fewer than two modes or a mode listed twice; a mapping to a directive the
interface does not have, to a payload that does not fit, to a mode that is not listed, to AdjustMode on modes
that are not ordered; SetEcoOn, SetEcoOff, EcoOn and EcoOff on anything but a toggle.

For 1.x callers: Alexa.RangeController lists rangeValue (1.5.2 sent supported: []); addSupportedModes() announces
a mode given as a string as { value } and notes it, and takes { ordered: false } as a second argument, the default
stays true as 1.5.2 sent it (device.add() defaults to false). Declared the 1.x way, the zoo fan and curtain give
the same JSON as before.

Tests: the zoo blind, whose range, unit and semantics were written by hand into the JSON of 1.5.2, is declared
with device.add() and equals that JSON plus the Alexa capability; five capability objects of Amazon's examples
are reproduced the same way. 27 examples of four pages added. npm test: 108 pass (was 85) in 10.5-11.3 s, also on
Node 18.20.8 and 20.20.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 15:50:25 +00:00
parent c492ef74d1
commit 2558e21787
80 changed files with 3131 additions and 107 deletions

View file

@ -1,6 +1,6 @@
import { Capability } from "../device/Capability.js";
import type { CapabilityJson } from "../device/Capability.js";
import type { Semantics } from "../registry/types.js";
import type { Label, Semantics } from "../registry/types.js";
import type { ActionMapping } from "./ActionMapping.js";
import type { AlexaInterfaceType } from "./enums.js";
/** One ModeController mode as discovery lists it (configuration.supportedModes): the value plus its friendly names. */
@ -19,7 +19,11 @@ export interface SupportedMode {
}
interface Options {
semantics?: Semantics;
supportedModes?: Array<string | SupportedMode>;
supportedModes?: Array<{
value: string;
friendlyNames: Label[];
}>;
ordered?: boolean;
}
/**
* A capability as 1.x declares it: device.addCapability(type, options), then the add and set methods below. It is a
@ -33,8 +37,14 @@ export declare class AlexaInterface extends Capability<any, any, Options> {
get type(): AlexaInterfaceType;
addActionMapping(mapping: ActionMapping): void;
addFriendlyName(name: string, locale: string): void;
/** The modes of a ModeController, as { value, modeResources } objects. */
addSupportedModes(modes: Array<string | SupportedMode>): void;
/**
* The modes of a ModeController, as { value, modeResources } objects. A mode given as a string is announced as
* { value } and noted: Alexa wants friendly names for it. ordered is true unless options.ordered says otherwise,
* as 1.x announced every mode list; with device.add() the default is false.
*/
addSupportedModes(modes: Array<string | SupportedMode>, options?: {
ordered?: boolean;
}): void;
setInstance(name: string): void;
getType(): AlexaInterfaceType;
/** @deprecated The same as getType(). */

View file

@ -5,11 +5,11 @@ export type { EndpointDefinition, EndpointJson } from "./device/Device.js";
export { Capability } from "./device/Capability.js";
export type { CapabilityJson, CommonOptions, Declaration } from "./device/Capability.js";
export { registry, DeclarationError, SchemaError } from "./registry/index.js";
export { Alexa, BrightnessController, TemperatureSensor } from "./registry/index.js";
export { Alexa, BrightnessController, ModeController, RangeController, TemperatureSensor, ToggleController, } from "./registry/index.js";
export { PowerController, EndpointHealth } from "./compat/enums.js";
export { asset, text } from "./registry/resources.js";
export { asset, text, semantics, SemanticsBuilder } 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, ActionsToDirective, AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor, Label, PropertyDescriptor, Semantics, StatesToRange, StatesToValue, Infer, Schema, Temperature, TimeInterval, } from "./registry/index.js";
export type { ActionId, AssetId, DisplayCategoryName, StateId, UnitOfMeasure, ActionsToDirective, AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor, Label, PropertyDescriptor, Semantics, StatesToRange, StatesToValue, ActionName, StateName, Mode, Infer, Schema, Temperature, TimeInterval, } from "./registry/index.js";
export { AlexaInterface } from "./compat/AlexaInterface.js";
export type { SupportedMode } from "./compat/AlexaInterface.js";
export { ActionMapping } from "./compat/ActionMapping.js";

View file

@ -1,8 +1,11 @@
import { Alexa } from "./interfaces/Alexa.js";
import { BrightnessController } from "./interfaces/BrightnessController.js";
import { EndpointHealth } from "./interfaces/EndpointHealth.js";
import { ModeController } from "./interfaces/ModeController.js";
import { PowerController } from "./interfaces/PowerController.js";
import { RangeController } from "./interfaces/RangeController.js";
import { TemperatureSensor } from "./interfaces/TemperatureSensor.js";
import { ToggleController } from "./interfaces/ToggleController.js";
import type { AnyDescriptor } from "./types.js";
export declare const registry: {
/** Whether an interface of this name is known. */
@ -12,7 +15,11 @@ export declare const registry: {
/** Every descriptor, ordered by namespace. */
list(): AnyDescriptor[];
};
export { Alexa, BrightnessController, EndpointHealth, PowerController, TemperatureSensor };
export { Alexa, BrightnessController, EndpointHealth, ModeController, PowerController, RangeController, TemperatureSensor, ToggleController, };
export type { Mode } from "./interfaces/ModeController.js";
export { asset, text } from "./resources.js";
export { semantics, SemanticsBuilder } from "./semantics.js";
export type { ActionName, StateName } from "./semantics.js";
export { DeclarationError, defineInterface } from "./types.js";
export type { ActionsToDirective, AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor, Label, PropertyDescriptor, Semantics, StatesToRange, StatesToValue, } from "./types.js";
export { s, SchemaError } from "./schema.js";

View file

@ -0,0 +1,43 @@
import type { Schema } from "../schema.js";
import type { Label } from "../types.js";
/** One mode: its value and what the user calls it. */
export interface Mode {
value: string;
friendlyNames: Label[];
}
/**
* One of a list of values, under an instance name: the wash cycle of a washer, the position of a garage door. Amazon
* certifies a garage door only with this interface and the display category GARAGE_DOOR.
*/
export declare const ModeController: import("../types.js").InterfaceDescriptor<{
mode: {
name: string;
value: Schema<string | null>;
note: string;
};
}, {
SetMode: {
name: string;
payload: Schema<import("../schema.js").InferShape<{
mode: Schema<string>;
}>>;
};
AdjustMode: {
name: string;
payload: Schema<import("../schema.js").InferShape<{
modeDelta: import("../schema.js").OptionalSchema<number>;
}>>;
when: ({ options }: {
options: {
ordered?: boolean;
};
}) => boolean;
note: string;
};
}, import("../schema.js").InferShape<{
/** At least two. When the modes have an order, in increasing order. */
supportedModes: Schema<Mode[]>;
/** The modes have an order, cold to hot, and the user can say "increase": Alexa sends AdjustMode. Default: false. */
ordered: import("../schema.js").OptionalSchema<boolean>;
semantics: import("../schema.js").OptionalSchema<import("../types.js").Semantics>;
}>, true>;

View file

@ -0,0 +1,39 @@
/**
* A number in a range, under an instance name: the position of a blind, the speed of a fan. With nonControllable it
* is a reading the user can ask for, like an air quality index.
*/
export declare const RangeController: import("../types.js").InterfaceDescriptor<{
rangeValue: {
name: string;
value: import("../schema.js").Schema<number>;
};
}, {
SetRangeValue: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
rangeValue: import("../schema.js").Schema<number>;
}>>;
};
AdjustRangeValue: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
rangeValueDelta: import("../schema.js").Schema<number>;
rangeValueDeltaDefault: import("../schema.js").Schema<boolean>;
}>>;
note: string;
};
}, import("../schema.js").InferShape<{
/** precision is the step of the range and what "turn up the fan speed" changes it by. */
range: import("../schema.js").Schema<import("../schema.js").InferShape<{
min: import("../schema.js").Schema<number>;
max: import("../schema.js").Schema<number>;
precision: import("../schema.js").Schema<number>;
}>>;
unit: import("../schema.js").OptionalSchema<import("../catalog.js").UnitOfMeasure>;
/** Names for values: "set the fan speed to maximum". */
presets: import("../schema.js").OptionalSchema<import("../schema.js").InferShape<{
value: import("../schema.js").Schema<number>;
friendlyNames: import("../schema.js").Schema<import("../types.js").Label[]>;
}>[]>;
semantics: import("../schema.js").OptionalSchema<import("../types.js").Semantics>;
}>, true>;

View file

@ -0,0 +1,21 @@
/**
* Something that is on or off, under an instance name: the oscillation of a fan, the light of an oven. The one
* generic controller that Alexa hunches work with (SetEcoOn, SetEcoOff, EcoOn, EcoOff).
*/
export declare const ToggleController: import("../types.js").InterfaceDescriptor<{
toggleState: {
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<{
semantics: import("../schema.js").OptionalSchema<import("../types.js").Semantics>;
}>, true>;

47
dist/types/registry/semantics.d.ts vendored Normal file
View file

@ -0,0 +1,47 @@
import type { ActionId, StateId } from "./catalog.js";
import type { Schema } from "./schema.js";
import type { ActionsToDirective, Directives, Semantics, StatesToRange, StatesToValue } from "./types.js";
type Short<Id extends string, Prefix extends string> = Id extends `${Prefix}${infer Name}` ? Name : never;
/** A phrase by its id or by its name: "Alexa.Actions.Open" or "Open". */
export type ActionName = ActionId | Short<ActionId, "Alexa.Actions.">;
/** A state by its id or by its name: "Alexa.States.Closed" or "Closed". */
export type StateName = StateId | Short<StateId, "Alexa.States.">;
/**
* Collects the mappings of one capability:
*
* semantics()
* .action("Close", "SetRangeValue", { rangeValue: 0 })
* .action(["Open", "Raise"], "SetRangeValue", { rangeValue: 100 })
* .state("Closed", 0)
* .stateRange("Open", 1, 100)
*
* What is collected is checked when the capability is declared, against the directives and the property of its
* interface.
*/
export declare class SemanticsBuilder implements Semantics {
readonly actionMappings: ActionsToDirective[];
readonly stateMappings: Array<StatesToValue | StatesToRange>;
/** The phrases send the directive. payload is its payload; leave it out for a directive that has none. */
action(actions: ActionName | readonly ActionName[], directive: string, payload?: Record<string, unknown>): this;
/** The property at this value is in these states: "is the garage door open?" */
state(states: StateName | readonly StateName[], value: unknown): this;
/** The property between the two values, both included, is in these states. For an interface with a numeric property. */
stateRange(states: StateName | readonly StateName[], minimumValue: number, maximumValue: number): this;
}
export declare function semantics(): SemanticsBuilder;
export interface SemanticsRules {
/** The directives of the interface: a mapping names one of them and gives its payload. */
directives: Directives;
/** The value of the property a state stands for. */
stateValue: Schema<unknown>;
/** States can be mapped to a range of values: the property is a number. */
ranges?: boolean;
/**
* SetEcoOn, SetEcoOff, EcoOn and EcoOff can be mapped. "Semantics for Alexa hunches is supported only for
* Alexa.ToggleController" (generic-controllers.html).
*/
hunches?: boolean;
}
/** The schema of the semantics option of one interface. It takes a semantics() builder or the object as discovery has it. */
export declare function semanticsOf({ directives, stateValue, ranges, hunches }: SemanticsRules): Schema<Semantics>;
export {};

View file

@ -120,7 +120,11 @@ export interface InterfaceDescriptor<P extends Properties = Properties, D extend
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;
/**
* What the interface adds to the capability object. The options may be incomplete: a capability declared the
* 1.x way is announced with what it has, and check() of its device says what is missing.
*/
discovery?: (capability: Declared<Partial<O>>) => CapabilityExtras;
/** The event that answers a directive when it is not Alexa / Response (Arm -> Arm.Response). */
responseFor?: (directive: string) => {
namespace: string;
@ -132,7 +136,7 @@ export interface InterfaceDescriptor<P extends Properties = Properties, D extend
errorTypes?: readonly string[];
/** Amazon documents a DeferredResponse for the interface. */
deferrable?: boolean;
/** Rules a schema cannot state. Throws DeclarationError. */
/** Rules a schema cannot state, for options that passed the schema. Throws DeclarationError. */
validate?: (capability: Declared<O>, endpoint: EndpointView) => void;
}
export type AnyDescriptor = InterfaceDescriptor<any, any, any, boolean>;