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>
This commit is contained in:
parent
4a3dd81bcf
commit
94b13f733a
36 changed files with 874 additions and 138 deletions
|
|
@ -28,15 +28,16 @@ export type {
|
|||
ActionId, AssetId, DisplayCategoryName, StateId, UnitOfMeasure,
|
||||
ActionsToDirective, AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor,
|
||||
InterfaceDescriptor, Label, PropertyDescriptor, Semantics, StatesToRange, StatesToValue,
|
||||
ActionName, StateName, Cause, InputName, InventoryLevel, Mode, ThermostatModeName, Infer, Schema, Temperature, TimeInterval,
|
||||
ActionName, StateName, Cause, ErrorType, InputName, InventoryLevel, Mode, ThermostatModeName, Infer, Schema, Temperature, TimeInterval,
|
||||
} from "./registry/index.js";
|
||||
|
||||
// The messages: what the bridge publishes, built from plain values
|
||||
export * as messages from "./messages/index.js";
|
||||
export { AlexaError, AlexaErrors, MessageError, StateBuilder, property } from "./messages/index.js";
|
||||
export type {
|
||||
ChangeCause, ChangeReportMessage, DeferredResponseMessage, ErrorResponseMessage, Header, ProactiveEventFields,
|
||||
ProactiveEventMessage, Property, PropertyOptions, ResponseMessage, SceneEventMessage,
|
||||
ChangeCause, ChangeReportMessage, ChargeState, ControlUnavailableReason, CurrentDeviceMode, DeferredResponseMessage,
|
||||
EndpointNeedingBypass, ErrorResponseMessage, Header, MaintenanceAction, ProactiveEventFields, ProactiveEventMessage,
|
||||
Property, PropertyOptions, ResourceType, ResponseMessage, SceneEventMessage, ValidRange,
|
||||
} from "./messages/index.js";
|
||||
|
||||
// Publishing: the topics of the Alex2MQTT contract, and a publisher that needs no broker for tests
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// The errors a device answers a directive with (alexa-errorresponse.html). A helper for each type whose payload
|
||||
// has fields of its own, so that the fields cannot be misspelt.
|
||||
// The errors a device answers a directive with (alexa-errorresponse.html): a helper for each type of the table.
|
||||
// The helper of a type whose payload has fields of its own takes them, so that they cannot be misspelt or left out.
|
||||
import { ERROR_TYPES } from "../registry/catalog.js";
|
||||
import type { ErrorType } from "../registry/catalog.js";
|
||||
import type { Temperature } from "../registry/schema.js";
|
||||
|
||||
/**
|
||||
|
|
@ -33,33 +34,69 @@ export class AlexaError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export type CurrentDeviceMode = "ASLEEP" | "COLOR" | "DEMAND_RESPONSE" | "ECO" | "NOT_PROVISIONED" | "VACATION" | "OTHER";
|
||||
export type ControlUnavailableReason = "DEEP_SLEEP_MODE" | "OUT_OF_NETWORK_CONNECTIVITY" | "NO_CONNECTIVITY_PACKAGE_ENABLED" | "UNKNOWN";
|
||||
export type ChargeState = "ALREADY_CHARGED_TO_REQUIRED_LEVEL" | "CURRENTLY_CHARGING" | "FULLY_CHARGED" | "NOT_CONNECTED_TO_POWER";
|
||||
// alexa-errorresponse.html, "Properties and objects": the values a field takes
|
||||
const CURRENT_DEVICE_MODES = ["ASLEEP", "COLOR", "DEMAND_RESPONSE", "ECO", "NOT_PROVISIONED", "VACATION", "OTHER"] as const;
|
||||
const REASONS = ["DEEP_SLEEP_MODE", "OUT_OF_NETWORK_CONNECTIVITY", "NO_CONNECTIVITY_PACKAGE_ENABLED", "UNKNOWN"] as const;
|
||||
const CHARGE_STATES = ["ALREADY_CHARGED_TO_REQUIRED_LEVEL", "CURRENTLY_CHARGING", "FULLY_CHARGED", "NOT_CONNECTED_TO_POWER"] as const;
|
||||
const RESOURCE_TYPES = ["WATER"] as const;
|
||||
const MAINTENANCE_ACTIONS = ["EMPTY_BIN"] as const;
|
||||
|
||||
export type CurrentDeviceMode = (typeof CURRENT_DEVICE_MODES)[number];
|
||||
export type ControlUnavailableReason = (typeof REASONS)[number];
|
||||
export type ChargeState = (typeof CHARGE_STATES)[number];
|
||||
export type ResourceType = (typeof RESOURCE_TYPES)[number];
|
||||
export type MaintenanceAction = (typeof MAINTENANCE_ACTIONS)[number];
|
||||
|
||||
/** The acceptable values of a setting: both ends, as numbers or as temperatures. */
|
||||
export interface ValidRange<T> {
|
||||
minimumValue: T;
|
||||
maximumValue: T;
|
||||
}
|
||||
|
||||
/** A sensor that is open while the panel is armed: Alexa says the name, and bypasses the ones with an endpointId. */
|
||||
export interface EndpointNeedingBypass {
|
||||
friendlyName: string;
|
||||
endpointId?: string;
|
||||
}
|
||||
|
||||
// A field that was not given is left out of the payload
|
||||
function given(fields: Record<string, unknown>): Record<string, unknown> {
|
||||
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
||||
}
|
||||
|
||||
export const AlexaErrors = {
|
||||
/** An error of any type. */
|
||||
of(type: string, message: string, extra: Record<string, unknown> = {}): AlexaError {
|
||||
return new AlexaError(type, message, extra);
|
||||
// TypeScript refuses a call without a required field. A JavaScript caller hears of it here, where the helper is
|
||||
// called: the payload would reach Alexa without the field.
|
||||
function required<T extends string>(helper: string, field: string, value: T, values: readonly T[]): T {
|
||||
if (values.includes(value)) return value;
|
||||
throw new TypeError(`AlexaErrors.${helper}() got ${JSON.stringify(value)} as ${field}: pass one of ${values.join(", ")}`);
|
||||
}
|
||||
|
||||
function range<T>(helper: string, validRange: ValidRange<T> | undefined): ValidRange<T> | undefined {
|
||||
if (validRange === undefined) return undefined;
|
||||
const { minimumValue, maximumValue } = validRange;
|
||||
if (minimumValue === undefined || maximumValue === undefined) {
|
||||
throw new TypeError(`AlexaErrors.${helper}() got a validRange without ${minimumValue === undefined ? "minimumValue" : "maximumValue"}: pass both ends, or no range`);
|
||||
}
|
||||
return { minimumValue, maximumValue };
|
||||
}
|
||||
|
||||
// The helpers of the types whose payload has fields next to type and message. A required field is a required
|
||||
// argument; a field the page calls optional can be left out.
|
||||
const withFields = {
|
||||
/** The value is outside what the endpoint takes. For a temperature: temperatureValueOutOfRange(). */
|
||||
valueOutOfRange(message: string, validRange?: ValidRange<number>): AlexaError {
|
||||
return new AlexaError("VALUE_OUT_OF_RANGE", message, given({ validRange: range("valueOutOfRange", validRange) }));
|
||||
},
|
||||
|
||||
/** The value is outside what the endpoint takes. For a temperature: temperatureOutOfRange(). */
|
||||
valueOutOfRange(message: string, validRange: { minimumValue: number; maximumValue: number }): AlexaError {
|
||||
return new AlexaError("VALUE_OUT_OF_RANGE", message, { validRange });
|
||||
},
|
||||
|
||||
temperatureOutOfRange(message: string, validRange: { minimumValue: Temperature; maximumValue: Temperature }): AlexaError {
|
||||
return new AlexaError("TEMPERATURE_VALUE_OUT_OF_RANGE", message, { validRange });
|
||||
temperatureValueOutOfRange(message: string, validRange?: ValidRange<Temperature>): AlexaError {
|
||||
const helper = "temperatureValueOutOfRange";
|
||||
return new AlexaError("TEMPERATURE_VALUE_OUT_OF_RANGE", message, given({ validRange: range(helper, validRange) }));
|
||||
},
|
||||
|
||||
/** A light showing a color asked for a color temperature: "COLOR". */
|
||||
notSupportedInCurrentMode(message: string, currentDeviceMode: CurrentDeviceMode): AlexaError {
|
||||
return new AlexaError("NOT_SUPPORTED_IN_CURRENT_MODE", message, { currentDeviceMode });
|
||||
const mode = required("notSupportedInCurrentMode", "currentDeviceMode", currentDeviceMode, CURRENT_DEVICE_MODES);
|
||||
return new AlexaError("NOT_SUPPORTED_IN_CURRENT_MODE", message, { currentDeviceMode: mode });
|
||||
},
|
||||
|
||||
/** percentageState: what is left of the battery, 0 to 100. */
|
||||
|
|
@ -68,38 +105,86 @@ export const AlexaErrors = {
|
|||
},
|
||||
|
||||
endpointControlUnavailable(message: string, reason: ControlUnavailableReason): AlexaError {
|
||||
return new AlexaError("ENDPOINT_CONTROL_UNAVAILABLE", message, { reason });
|
||||
return new AlexaError("ENDPOINT_CONTROL_UNAVAILABLE", message, {
|
||||
reason: required("endpointControlUnavailable", "reason", reason, REASONS),
|
||||
});
|
||||
},
|
||||
|
||||
/** currentChargeLevelInPercentage: the battery level, 0 to 100. */
|
||||
notSupportedWithCurrentBatteryChargeState(
|
||||
message: string,
|
||||
currentChargeState: ChargeState,
|
||||
currentChargeLevelInPercentage?: number
|
||||
): AlexaError {
|
||||
return new AlexaError(
|
||||
"NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE",
|
||||
message,
|
||||
given({ currentChargeState, currentChargeLevelInPercentage })
|
||||
);
|
||||
const helper = "notSupportedWithCurrentBatteryChargeState";
|
||||
return new AlexaError("NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE", message, given({
|
||||
currentChargeState: required(helper, "currentChargeState", currentChargeState, CHARGE_STATES),
|
||||
currentChargeLevelInPercentage,
|
||||
}));
|
||||
},
|
||||
|
||||
/** What has to be refilled. */
|
||||
insufficientResource(message: string, resourceType: "WATER"): AlexaError {
|
||||
return new AlexaError("INSUFFICIENT_RESOURCE", message, { resourceType });
|
||||
insufficientResource(message: string, resourceType: ResourceType): AlexaError {
|
||||
return new AlexaError("INSUFFICIENT_RESOURCE", message, {
|
||||
resourceType: required("insufficientResource", "resourceType", resourceType, RESOURCE_TYPES),
|
||||
});
|
||||
},
|
||||
|
||||
/** What the user has to do first. */
|
||||
maintenanceRequired(message: string, maintenanceAction?: "EMPTY_BIN"): AlexaError {
|
||||
return new AlexaError("MAINTENANCE_REQUIRED", message, given({ maintenanceAction }));
|
||||
maintenanceRequired(message: string, maintenanceAction?: MaintenanceAction): AlexaError {
|
||||
const action = maintenanceAction === undefined
|
||||
? undefined
|
||||
: required("maintenanceRequired", "maintenanceAction", maintenanceAction, MAINTENANCE_ACTIONS);
|
||||
return new AlexaError("MAINTENANCE_REQUIRED", message, given({ maintenanceAction: action }));
|
||||
},
|
||||
|
||||
/** Goes under Alexa.ThermostatController. minimumTemperatureDelta: how far apart the setpoints have to be. */
|
||||
setpointsTooClose(message: string, minimumTemperatureDelta?: Temperature): AlexaError {
|
||||
requestedSetpointsTooClose(message: string, minimumTemperatureDelta?: Temperature): AlexaError {
|
||||
return new AlexaError("REQUESTED_SETPOINTS_TOO_CLOSE", message, given({ minimumTemperatureDelta }));
|
||||
},
|
||||
|
||||
/** Goes under Alexa.SecurityPanelController. With the endpoints listed, the user can bypass them by voice. */
|
||||
bypassNeeded(message: string, endpointsNeedingBypass?: Array<{ friendlyName: string; endpointId?: string }>): AlexaError {
|
||||
bypassNeeded(message: string, endpointsNeedingBypass?: EndpointNeedingBypass[]): AlexaError {
|
||||
return new AlexaError("BYPASS_NEEDED", message, given({ endpointsNeedingBypass }));
|
||||
},
|
||||
};
|
||||
|
||||
// "NOT_IN_OPERATION" -> "notInOperation", for the compiler and at run time
|
||||
type CamelCase<S extends string> = S extends `${infer First}_${infer Rest}`
|
||||
? `${Lowercase<First>}${Capitalize<CamelCase<Rest>>}`
|
||||
: Lowercase<S>;
|
||||
const camelCase = (type: string): string => type.toLowerCase().replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
||||
|
||||
/** The helpers of the types whose payload is the type and the message: AlexaErrors.endpointUnreachable(message). */
|
||||
export type PlainErrorHelpers = {
|
||||
[T in ErrorType as CamelCase<T> extends keyof typeof withFields ? never : CamelCase<T>]: (message: string) => AlexaError;
|
||||
};
|
||||
|
||||
const plain = Object.fromEntries(
|
||||
Object.keys(ERROR_TYPES)
|
||||
.filter((type) => !Object.prototype.hasOwnProperty.call(withFields, camelCase(type)))
|
||||
.map((type) => [camelCase(type), (message: string): AlexaError => new AlexaError(type, message)])
|
||||
) as PlainErrorHelpers;
|
||||
|
||||
/**
|
||||
* An error for every type of the table of alexa-errorresponse.html, under the name of the type in camel case:
|
||||
*
|
||||
* throw AlexaErrors.endpointUnreachable("the lamp does not answer");
|
||||
* throw AlexaErrors.valueOutOfRange("the lift goes from 0 to 100", { minimumValue: 0, maximumValue: 100 });
|
||||
* throw AlexaErrors.thermostatIsOff("the thermostat is off"); // sent under Alexa.ThermostatController
|
||||
*/
|
||||
export const AlexaErrors = {
|
||||
...plain,
|
||||
...withFields,
|
||||
|
||||
/** An error of any type, one the table does not have too. */
|
||||
of(type: string, message: string, extra: Record<string, unknown> = {}): AlexaError {
|
||||
return new AlexaError(type, message, extra);
|
||||
},
|
||||
|
||||
/** temperatureValueOutOfRange() under its shorter name. */
|
||||
temperatureOutOfRange: withFields.temperatureValueOutOfRange,
|
||||
|
||||
/** requestedSetpointsTooClose() under its shorter name. */
|
||||
setpointsTooClose: withFields.requestedSetpointsTooClose,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ export type {
|
|||
SimpleEventFields,
|
||||
} from "./build.js";
|
||||
export { AlexaError, AlexaErrors, errorNamespace } from "./errors.js";
|
||||
export type { ChargeState, ControlUnavailableReason, CurrentDeviceMode } from "./errors.js";
|
||||
export type {
|
||||
ChargeState, ControlUnavailableReason, CurrentDeviceMode, EndpointNeedingBypass, MaintenanceAction,
|
||||
PlainErrorHelpers, ResourceType, ValidRange,
|
||||
} from "./errors.js";
|
||||
export { property } from "./property.js";
|
||||
export type { PropertyOptions } from "./property.js";
|
||||
export { StateBuilder } from "./StateBuilder.js";
|
||||
|
|
|
|||
|
|
@ -147,6 +147,9 @@ const ERROR_TYPES_BY_NAMESPACE = {
|
|||
"Alexa.ThermostatController.Schedule": ["INSUFFICIENT_SPACE"],
|
||||
} as const;
|
||||
|
||||
/** "ENDPOINT_UNREACHABLE": a type of the table. */
|
||||
export type ErrorType = (typeof ERROR_TYPES_BY_NAMESPACE)[keyof typeof ERROR_TYPES_BY_NAMESPACE][number];
|
||||
|
||||
/** 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]))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue