From 94b13f733af02e1c89f473064a0677ac6b1cdcfe Mon Sep 17 00:00:00 2001 From: David Date: Mon, 28 Sep 2026 21:14:55 +0000 Subject: [PATCH] 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 --- dist/cjs/messages/errors.js | 91 +++++++++-- dist/esm/compat/enums.d.ts | 4 +- dist/esm/index.d.ts | 4 +- dist/esm/messages/errors.d.ts | 154 +++++++++++++++--- dist/esm/messages/errors.js | 91 +++++++++-- dist/esm/messages/index.d.ts | 2 +- dist/esm/registry/catalog.d.ts | 16 ++ .../registry/interfaces/EndpointHealth.d.ts | 2 +- .../registry/interfaces/PowerController.d.ts | 2 +- .../interfaces/SecurityPanelController.d.ts | 8 +- .../interfaces/ThermostatController.d.ts | 2 +- .../registry/interfaces/ToggleController.d.ts | 2 +- dist/types/compat/enums.d.ts | 4 +- dist/types/index.d.ts | 4 +- dist/types/messages/errors.d.ts | 154 +++++++++++++++--- dist/types/messages/index.d.ts | 2 +- dist/types/registry/catalog.d.ts | 16 ++ .../registry/interfaces/EndpointHealth.d.ts | 2 +- .../registry/interfaces/PowerController.d.ts | 2 +- .../interfaces/SecurityPanelController.d.ts | 8 +- .../interfaces/ThermostatController.d.ts | 2 +- .../registry/interfaces/ToggleController.d.ts | 2 +- src/index.ts | 7 +- src/messages/errors.ts | 143 ++++++++++++---- src/messages/index.ts | 5 +- src/registry/catalog.ts | 3 + .../ENDPOINT_CONTROL_UNAVAILABLE.error.json | 19 +++ .../ENDPOINT_LOW_POWER.error.json | 19 +++ .../NOT_SUPPORTED_IN_CURRENT_MODE.error.json | 19 +++ ...TH_CURRENT_BATTERY_CHARGE_STATE.error.json | 20 +++ .../TEMPERATURE_VALUE_OUT_OF_RANGE.error.json | 28 ++++ .../VALUE_OUT_OF_RANGE.error.json | 22 +++ .../OBSTACLE_DETECTED.error.json | 18 ++ .../SAFETY_BEAM_BREACHED.error.json | 18 ++ test/fixtures/types.ts | 18 ++ test/messages/errors.test.js | 99 ++++++++++- 36 files changed, 874 insertions(+), 138 deletions(-) create mode 100644 test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_CONTROL_UNAVAILABLE.error.json create mode 100644 test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_LOW_POWER.error.json create mode 100644 test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_IN_CURRENT_MODE.error.json create mode 100644 test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE.error.json create mode 100644 test/fixtures/alexa-docs/alexa-errorresponse/TEMPERATURE_VALUE_OUT_OF_RANGE.error.json create mode 100644 test/fixtures/alexa-docs/alexa-errorresponse/VALUE_OUT_OF_RANGE.error.json create mode 100644 test/fixtures/alexa-docs/alexa-safety-errorresponse/OBSTACLE_DETECTED.error.json create mode 100644 test/fixtures/alexa-docs/alexa-safety-errorresponse/SAFETY_BEAM_BREACHED.error.json diff --git a/dist/cjs/messages/errors.js b/dist/cjs/messages/errors.js index b14ff4a..e993fed 100644 --- a/dist/cjs/messages/errors.js +++ b/dist/cjs/messages/errors.js @@ -2,8 +2,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.AlexaErrors = exports.AlexaError = void 0; exports.errorNamespace = errorNamespace; -// 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. const catalog_js_1 = require("../registry/catalog.js"); /** * The namespace an ErrorResponse of this type goes under: the one the table of error types lists it with @@ -25,46 +25,80 @@ class AlexaError extends Error { } } exports.AlexaError = AlexaError; +// alexa-errorresponse.html, "Properties and objects": the values a field takes +const CURRENT_DEVICE_MODES = ["ASLEEP", "COLOR", "DEMAND_RESPONSE", "ECO", "NOT_PROVISIONED", "VACATION", "OTHER"]; +const REASONS = ["DEEP_SLEEP_MODE", "OUT_OF_NETWORK_CONNECTIVITY", "NO_CONNECTIVITY_PACKAGE_ENABLED", "UNKNOWN"]; +const CHARGE_STATES = ["ALREADY_CHARGED_TO_REQUIRED_LEVEL", "CURRENTLY_CHARGING", "FULLY_CHARGED", "NOT_CONNECTED_TO_POWER"]; +const RESOURCE_TYPES = ["WATER"]; +const MAINTENANCE_ACTIONS = ["EMPTY_BIN"]; // A field that was not given is left out of the payload function given(fields) { return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)); } -exports.AlexaErrors = { - /** An error of any type. */ - of(type, message, extra = {}) { - return new AlexaError(type, message, extra); - }, - /** The value is outside what the endpoint takes. For a temperature: temperatureOutOfRange(). */ +// 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(helper, field, value, values) { + if (values.includes(value)) + return value; + throw new TypeError(`AlexaErrors.${helper}() got ${JSON.stringify(value)} as ${field}: pass one of ${values.join(", ")}`); +} +function range(helper, validRange) { + 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, validRange) { - return new AlexaError("VALUE_OUT_OF_RANGE", message, { validRange }); + return new AlexaError("VALUE_OUT_OF_RANGE", message, given({ validRange: range("valueOutOfRange", validRange) })); }, - temperatureOutOfRange(message, validRange) { - return new AlexaError("TEMPERATURE_VALUE_OUT_OF_RANGE", message, { validRange }); + temperatureValueOutOfRange(message, validRange) { + 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, currentDeviceMode) { - 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. */ endpointLowPower(message, percentageState) { return new AlexaError("ENDPOINT_LOW_POWER", message, given({ percentageState })); }, endpointControlUnavailable(message, reason) { - 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, currentChargeState, currentChargeLevelInPercentage) { - 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, resourceType) { - return new AlexaError("INSUFFICIENT_RESOURCE", message, { resourceType }); + return new AlexaError("INSUFFICIENT_RESOURCE", message, { + resourceType: required("insufficientResource", "resourceType", resourceType, RESOURCE_TYPES), + }); }, /** What the user has to do first. */ maintenanceRequired(message, maintenanceAction) { - return new AlexaError("MAINTENANCE_REQUIRED", message, given({ maintenanceAction })); + 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, minimumTemperatureDelta) { + requestedSetpointsTooClose(message, minimumTemperatureDelta) { 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. */ @@ -72,3 +106,26 @@ exports.AlexaErrors = { return new AlexaError("BYPASS_NEEDED", message, given({ endpointsNeedingBypass })); }, }; +const camelCase = (type) => type.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); +const plain = Object.fromEntries(Object.keys(catalog_js_1.ERROR_TYPES) + .filter((type) => !Object.prototype.hasOwnProperty.call(withFields, camelCase(type))) + .map((type) => [camelCase(type), (message) => new AlexaError(type, message)])); +/** + * 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 + */ +exports.AlexaErrors = { + ...plain, + ...withFields, + /** An error of any type, one the table does not have too. */ + of(type, message, extra = {}) { + return new AlexaError(type, message, extra); + }, + /** temperatureValueOutOfRange() under its shorter name. */ + temperatureOutOfRange: withFields.temperatureValueOutOfRange, + /** requestedSetpointsTooClose() under its shorter name. */ + setpointsTooClose: withFields.requestedSetpointsTooClose, +}; diff --git a/dist/esm/compat/enums.d.ts b/dist/esm/compat/enums.d.ts index 62b8701..64caa79 100644 --- a/dist/esm/compat/enums.d.ts +++ b/dist/esm/compat/enums.d.ts @@ -159,7 +159,7 @@ declare const Connectivity: { export declare const PowerController: import("../index.js").InterfaceDescriptor<{ powerState: { name: string; - value: import("../registry/schema.js").EnumSchema<"ON" | "OFF">; + value: import("../registry/schema.js").EnumSchema<"OFF" | "ON">; }; }, { TurnOn: { @@ -182,7 +182,7 @@ export declare const EndpointHealth: import("../index.js").InterfaceDescriptor<{ connectivity: { name: string; value: import("../index.js").Schema; + value: import("../registry/schema.js").EnumSchema<"UNREACHABLE" | "OK">; reason: import("../registry/schema.js").OptionalSchema<"UNKNOWN" | "WIFI_BAD_PASSWORD" | "WIFI_AP_NOT_FOUND" | "WIFI_ROUTER_UNREACHABLE" | "WIFI_AP_CHANNEL_QUALITY_LOW" | "INTERNET_UNREACHABLE" | "CAPTIVE_PORTAL_CHECK_FAILED">; }>>; note: string; diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts index 75ec406..e143350 100644 --- a/dist/esm/index.d.ts +++ b/dist/esm/index.d.ts @@ -9,10 +9,10 @@ export { Alexa, BrightnessController, ChannelController, ColorController, ColorT export { PowerController, EndpointHealth } from "./compat/enums.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, CAUSES as Causes, DISPLAY_CATEGORIES as DisplayCategories, INPUTS as Inputs, THERMOSTAT_MODES as ThermostatModes, } 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, Cause, InputName, InventoryLevel, Mode, ThermostatModeName, 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, Cause, ErrorType, InputName, InventoryLevel, Mode, ThermostatModeName, Infer, Schema, Temperature, TimeInterval, } from "./registry/index.js"; 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, } from "./messages/index.js"; +export type { ChangeCause, ChangeReportMessage, ChargeState, ControlUnavailableReason, CurrentDeviceMode, DeferredResponseMessage, EndpointNeedingBypass, ErrorResponseMessage, Header, MaintenanceAction, ProactiveEventFields, ProactiveEventMessage, Property, PropertyOptions, ResourceType, ResponseMessage, SceneEventMessage, ValidRange, } from "./messages/index.js"; export * as topics from "./topics.js"; export { MemoryPublisher } from "./transport.js"; export type { Publisher, PublishResult } from "./transport.js"; diff --git a/dist/esm/messages/errors.d.ts b/dist/esm/messages/errors.d.ts index 1fad696..2412a15 100644 --- a/dist/esm/messages/errors.d.ts +++ b/dist/esm/messages/errors.d.ts @@ -1,3 +1,4 @@ +import type { ErrorType } from "../registry/catalog.js"; import type { Temperature } from "../registry/schema.js"; /** * The namespace an ErrorResponse of this type goes under: the one the table of error types lists it with @@ -17,36 +18,145 @@ export declare class AlexaError extends Error { readonly namespace: string; constructor(type: string, message: string, extra?: Record, namespace?: string); } -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"; -export declare const AlexaErrors: { - /** An error of any type. */ - of(type: string, message: string, extra?: Record): AlexaError; - /** The value is outside what the endpoint takes. For a temperature: temperatureOutOfRange(). */ - valueOutOfRange(message: string, validRange: { - minimumValue: number; - maximumValue: number; - }): AlexaError; - temperatureOutOfRange(message: string, validRange: { - minimumValue: Temperature; - maximumValue: Temperature; - }): AlexaError; +declare const CURRENT_DEVICE_MODES: readonly ["ASLEEP", "COLOR", "DEMAND_RESPONSE", "ECO", "NOT_PROVISIONED", "VACATION", "OTHER"]; +declare const REASONS: readonly ["DEEP_SLEEP_MODE", "OUT_OF_NETWORK_CONNECTIVITY", "NO_CONNECTIVITY_PACKAGE_ENABLED", "UNKNOWN"]; +declare const CHARGE_STATES: readonly ["ALREADY_CHARGED_TO_REQUIRED_LEVEL", "CURRENTLY_CHARGING", "FULLY_CHARGED", "NOT_CONNECTED_TO_POWER"]; +declare const RESOURCE_TYPES: readonly ["WATER"]; +declare const MAINTENANCE_ACTIONS: readonly ["EMPTY_BIN"]; +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 { + 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; +} +declare const withFields: { + /** The value is outside what the endpoint takes. For a temperature: temperatureValueOutOfRange(). */ + valueOutOfRange(message: string, validRange?: ValidRange): AlexaError; + temperatureValueOutOfRange(message: string, validRange?: ValidRange): AlexaError; /** A light showing a color asked for a color temperature: "COLOR". */ notSupportedInCurrentMode(message: string, currentDeviceMode: CurrentDeviceMode): AlexaError; /** percentageState: what is left of the battery, 0 to 100. */ endpointLowPower(message: string, percentageState?: number): AlexaError; endpointControlUnavailable(message: string, reason: ControlUnavailableReason): AlexaError; + /** currentChargeLevelInPercentage: the battery level, 0 to 100. */ notSupportedWithCurrentBatteryChargeState(message: string, currentChargeState: ChargeState, currentChargeLevelInPercentage?: number): AlexaError; /** What has to be refilled. */ - insufficientResource(message: string, resourceType: "WATER"): AlexaError; + insufficientResource(message: string, resourceType: ResourceType): AlexaError; /** What the user has to do first. */ - maintenanceRequired(message: string, maintenanceAction?: "EMPTY_BIN"): AlexaError; + maintenanceRequired(message: string, maintenanceAction?: MaintenanceAction): AlexaError; /** 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; /** 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; }; +type CamelCase = S extends `${infer First}_${infer Rest}` ? `${Lowercase}${Capitalize>}` : Lowercase; +/** The helpers of the types whose payload is the type and the message: AlexaErrors.endpointUnreachable(message). */ +export type PlainErrorHelpers = { + [T in ErrorType as CamelCase extends keyof typeof withFields ? never : CamelCase]: (message: string) => AlexaError; +}; +/** + * 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 declare const AlexaErrors: { + /** An error of any type, one the table does not have too. */ + of(type: string, message: string, extra?: Record): AlexaError; + /** temperatureValueOutOfRange() under its shorter name. */ + temperatureOutOfRange: (message: string, validRange?: ValidRange) => AlexaError; + /** requestedSetpointsTooClose() under its shorter name. */ + setpointsTooClose: (message: string, minimumTemperatureDelta?: Temperature) => AlexaError; + /** The value is outside what the endpoint takes. For a temperature: temperatureValueOutOfRange(). */ + valueOutOfRange(message: string, validRange?: ValidRange): AlexaError; + temperatureValueOutOfRange(message: string, validRange?: ValidRange): AlexaError; + /** A light showing a color asked for a color temperature: "COLOR". */ + notSupportedInCurrentMode(message: string, currentDeviceMode: CurrentDeviceMode): AlexaError; + /** percentageState: what is left of the battery, 0 to 100. */ + endpointLowPower(message: string, percentageState?: number): AlexaError; + endpointControlUnavailable(message: string, reason: ControlUnavailableReason): AlexaError; + /** currentChargeLevelInPercentage: the battery level, 0 to 100. */ + notSupportedWithCurrentBatteryChargeState(message: string, currentChargeState: ChargeState, currentChargeLevelInPercentage?: number): AlexaError; + /** What has to be refilled. */ + insufficientResource(message: string, resourceType: ResourceType): AlexaError; + /** What the user has to do first. */ + maintenanceRequired(message: string, maintenanceAction?: MaintenanceAction): AlexaError; + /** Goes under Alexa.ThermostatController. minimumTemperatureDelta: how far apart the setpoints have to be. */ + requestedSetpointsTooClose(message: string, minimumTemperatureDelta?: Temperature): AlexaError; + /** Goes under Alexa.SecurityPanelController. With the endpoints listed, the user can bypass them by voice. */ + bypassNeeded(message: string, endpointsNeedingBypass?: EndpointNeedingBypass[]): AlexaError; + alreadyInOperation: (message: string) => AlexaError; + bridgeUnreachable: (message: string) => AlexaError; + cloudControlDisabled: (message: string) => AlexaError; + deviceStuck: (message: string) => AlexaError; + doNotDisturbMode: (message: string) => AlexaError; + endpointBusy: (message: string) => AlexaError; + endpointUnreachable: (message: string) => AlexaError; + expiredAuthorizationCredential: (message: string) => AlexaError; + firmwareOutOfDate: (message: string) => AlexaError; + hardwareMalfunction: (message: string) => AlexaError; + insufficientPermissions: (message: string) => AlexaError; + internalError: (message: string) => AlexaError; + invalidAuthorizationCredential: (message: string) => AlexaError; + invalidDirective: (message: string) => AlexaError; + invalidValue: (message: string) => AlexaError; + noSuchEndpoint: (message: string) => AlexaError; + notCalibrated: (message: string) => AlexaError; + notInOperation: (message: string) => AlexaError; + partnerApplicationRedirection: (message: string) => AlexaError; + powerLevelNotSupported: (message: string) => AlexaError; + rateLimitExceeded: (message: string) => AlexaError; + tooManyFailedAttempts: (message: string) => AlexaError; + unableToCharge: (message: string) => AlexaError; + failedToBootstrapCommissioningProcess: (message: string) => AlexaError; + maxCommissioningLimitReached: (message: string) => AlexaError; + childLock: (message: string) => AlexaError; + cookDurationTooLong: (message: string) => AlexaError; + doorClosedTooLong: (message: string) => AlexaError; + doorOpen: (message: string) => AlexaError; + preheatRequired: (message: string) => AlexaError; + probeRequired: (message: string) => AlexaError; + remoteStartNotSupported: (message: string) => AlexaError; + removeProbe: (message: string) => AlexaError; + remoteStartDisabled: (message: string) => AlexaError; + dataDeletionNotSupported: (message: string) => AlexaError; + dataRetrievalNotSupported: (message: string) => AlexaError; + obstacleDetected: (message: string) => AlexaError; + safetyBeamBreached: (message: string) => AlexaError; + authorizationRequired: (message: string) => AlexaError; + notReady: (message: string) => AlexaError; + unauthorized: (message: string) => AlexaError; + unclearedAlarm: (message: string) => AlexaError; + unclearedTrouble: (message: string) => AlexaError; + subscriptionRequired: (message: string) => AlexaError; + disabledByUser: (message: string) => AlexaError; + dualSetpointsUnsupported: (message: string) => AlexaError; + thermostatIsOff: (message: string) => AlexaError; + tripleSetpointsUnsupported: (message: string) => AlexaError; + unsupportedThermostatMode: (message: string) => AlexaError; + unwillingToSetSchedule: (message: string) => AlexaError; + unwillingToSetValue: (message: string) => AlexaError; + configurationUpdateNotAllowed: (message: string) => AlexaError; + coolingLockoutTemperatureValueOutOfRange: (message: string) => AlexaError; + coolingStagesExceedsLimit: (message: string) => AlexaError; + heatingLockoutTemperatureValueOutOfRange: (message: string) => AlexaError; + heatingStagesExceedsLimit: (message: string) => AlexaError; + invalidAuxiliaryHeatingSystemType: (message: string) => AlexaError; + invalidSystemType: (message: string) => AlexaError; + invalidTargetState: (message: string) => AlexaError; + invalidTemperatureScale: (message: string) => AlexaError; + invalidTerminalConnection: (message: string) => AlexaError; + missingSetupInformation: (message: string) => AlexaError; + insufficientSpace: (message: string) => AlexaError; +}; +export {}; diff --git a/dist/esm/messages/errors.js b/dist/esm/messages/errors.js index 10ee50f..749c1ae 100644 --- a/dist/esm/messages/errors.js +++ b/dist/esm/messages/errors.js @@ -1,5 +1,5 @@ -// 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"; /** * The namespace an ErrorResponse of this type goes under: the one the table of error types lists it with @@ -20,46 +20,80 @@ export class AlexaError extends Error { this.namespace = namespace; } } +// alexa-errorresponse.html, "Properties and objects": the values a field takes +const CURRENT_DEVICE_MODES = ["ASLEEP", "COLOR", "DEMAND_RESPONSE", "ECO", "NOT_PROVISIONED", "VACATION", "OTHER"]; +const REASONS = ["DEEP_SLEEP_MODE", "OUT_OF_NETWORK_CONNECTIVITY", "NO_CONNECTIVITY_PACKAGE_ENABLED", "UNKNOWN"]; +const CHARGE_STATES = ["ALREADY_CHARGED_TO_REQUIRED_LEVEL", "CURRENTLY_CHARGING", "FULLY_CHARGED", "NOT_CONNECTED_TO_POWER"]; +const RESOURCE_TYPES = ["WATER"]; +const MAINTENANCE_ACTIONS = ["EMPTY_BIN"]; // A field that was not given is left out of the payload function given(fields) { return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)); } -export const AlexaErrors = { - /** An error of any type. */ - of(type, message, extra = {}) { - return new AlexaError(type, message, extra); - }, - /** The value is outside what the endpoint takes. For a temperature: temperatureOutOfRange(). */ +// 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(helper, field, value, values) { + if (values.includes(value)) + return value; + throw new TypeError(`AlexaErrors.${helper}() got ${JSON.stringify(value)} as ${field}: pass one of ${values.join(", ")}`); +} +function range(helper, validRange) { + 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, validRange) { - return new AlexaError("VALUE_OUT_OF_RANGE", message, { validRange }); + return new AlexaError("VALUE_OUT_OF_RANGE", message, given({ validRange: range("valueOutOfRange", validRange) })); }, - temperatureOutOfRange(message, validRange) { - return new AlexaError("TEMPERATURE_VALUE_OUT_OF_RANGE", message, { validRange }); + temperatureValueOutOfRange(message, validRange) { + 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, currentDeviceMode) { - 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. */ endpointLowPower(message, percentageState) { return new AlexaError("ENDPOINT_LOW_POWER", message, given({ percentageState })); }, endpointControlUnavailable(message, reason) { - 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, currentChargeState, currentChargeLevelInPercentage) { - 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, resourceType) { - return new AlexaError("INSUFFICIENT_RESOURCE", message, { resourceType }); + return new AlexaError("INSUFFICIENT_RESOURCE", message, { + resourceType: required("insufficientResource", "resourceType", resourceType, RESOURCE_TYPES), + }); }, /** What the user has to do first. */ maintenanceRequired(message, maintenanceAction) { - return new AlexaError("MAINTENANCE_REQUIRED", message, given({ maintenanceAction })); + 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, minimumTemperatureDelta) { + requestedSetpointsTooClose(message, minimumTemperatureDelta) { 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. */ @@ -67,3 +101,26 @@ export const AlexaErrors = { return new AlexaError("BYPASS_NEEDED", message, given({ endpointsNeedingBypass })); }, }; +const camelCase = (type) => type.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); +const plain = Object.fromEntries(Object.keys(ERROR_TYPES) + .filter((type) => !Object.prototype.hasOwnProperty.call(withFields, camelCase(type))) + .map((type) => [camelCase(type), (message) => new AlexaError(type, message)])); +/** + * 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, message, extra = {}) { + return new AlexaError(type, message, extra); + }, + /** temperatureValueOutOfRange() under its shorter name. */ + temperatureOutOfRange: withFields.temperatureValueOutOfRange, + /** requestedSetpointsTooClose() under its shorter name. */ + setpointsTooClose: withFields.requestedSetpointsTooClose, +}; diff --git a/dist/esm/messages/index.d.ts b/dist/esm/messages/index.d.ts index 1fe2b45..b6feae2 100644 --- a/dist/esm/messages/index.d.ts +++ b/dist/esm/messages/index.d.ts @@ -1,7 +1,7 @@ export { changeReport, deferredResponse, doorbellPress, errorResponse, proactiveEvent, response, sceneEvent, simpleEvent, stateReport, MessageError, } from "./build.js"; export type { ChangeReportFields, DoorbellPressFields, ErrorResponseFields, ProactiveEventFields, ResponseFields, SceneEventFields, 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"; diff --git a/dist/esm/registry/catalog.d.ts b/dist/esm/registry/catalog.d.ts index 74d2e41..9a061ef 100644 --- a/dist/esm/registry/catalog.d.ts +++ b/dist/esm/registry/catalog.d.ts @@ -42,5 +42,21 @@ export declare const LIMITS: { readonly additionalAttributeLength: 256; readonly cookieBytes: 5000; }; +declare const ERROR_TYPES_BY_NAMESPACE: { + readonly Alexa: readonly ["ALREADY_IN_OPERATION", "BRIDGE_UNREACHABLE", "CLOUD_CONTROL_DISABLED", "DEVICE_STUCK", "DO_NOT_DISTURB_MODE", "ENDPOINT_BUSY", "ENDPOINT_CONTROL_UNAVAILABLE", "ENDPOINT_LOW_POWER", "ENDPOINT_UNREACHABLE", "EXPIRED_AUTHORIZATION_CREDENTIAL", "FIRMWARE_OUT_OF_DATE", "HARDWARE_MALFUNCTION", "INSUFFICIENT_PERMISSIONS", "INSUFFICIENT_RESOURCE", "INTERNAL_ERROR", "INVALID_AUTHORIZATION_CREDENTIAL", "INVALID_DIRECTIVE", "INVALID_VALUE", "MAINTENANCE_REQUIRED", "NO_SUCH_ENDPOINT", "NOT_CALIBRATED", "NOT_IN_OPERATION", "NOT_SUPPORTED_IN_CURRENT_MODE", "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE", "PARTNER_APPLICATION_REDIRECTION", "POWER_LEVEL_NOT_SUPPORTED", "RATE_LIMIT_EXCEEDED", "TEMPERATURE_VALUE_OUT_OF_RANGE", "TOO_MANY_FAILED_ATTEMPTS", "UNABLE_TO_CHARGE", "VALUE_OUT_OF_RANGE"]; + readonly "Alexa.Commissionable.ReportCommissioningInformation": readonly ["FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS", "MAX_COMMISSIONING_LIMIT_REACHED"]; + readonly "Alexa.Cooking": readonly ["CHILD_LOCK", "COOK_DURATION_TOO_LONG", "DOOR_CLOSED_TOO_LONG", "DOOR_OPEN", "PREHEAT_REQUIRED", "PROBE_REQUIRED", "REMOTE_START_NOT_SUPPORTED", "REMOVE_PROBE", "REMOTE_START_DISABLED"]; + readonly "Alexa.DataController": readonly ["DATA_DELETION_NOT_SUPPORTED", "DATA_RETRIEVAL_NOT_SUPPORTED"]; + readonly "Alexa.Safety": readonly ["OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED"]; + readonly "Alexa.SecurityPanelController": readonly ["AUTHORIZATION_REQUIRED", "BYPASS_NEEDED", "NOT_READY", "UNAUTHORIZED", "UNCLEARED_ALARM", "UNCLEARED_TROUBLE"]; + readonly "Alexa.SmartVision.ObjectDetectionSensor": readonly ["SUBSCRIPTION_REQUIRED"]; + readonly "Alexa.SmartVision.SnapshotProvider": readonly ["DISABLED_BY_USER"]; + readonly "Alexa.ThermostatController": readonly ["DUAL_SETPOINTS_UNSUPPORTED", "REQUESTED_SETPOINTS_TOO_CLOSE", "THERMOSTAT_IS_OFF", "TRIPLE_SETPOINTS_UNSUPPORTED", "UNSUPPORTED_THERMOSTAT_MODE", "UNWILLING_TO_SET_SCHEDULE", "UNWILLING_TO_SET_VALUE"]; + readonly "Alexa.ThermostatController.Configuration": readonly ["CONFIGURATION_UPDATE_NOT_ALLOWED", "COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", "COOLING_STAGES_EXCEEDS_LIMIT", "HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", "HEATING_STAGES_EXCEEDS_LIMIT", "INVALID_AUXILIARY_HEATING_SYSTEM_TYPE", "INVALID_SYSTEM_TYPE", "INVALID_TARGET_STATE", "INVALID_TEMPERATURE_SCALE", "INVALID_TERMINAL_CONNECTION", "MISSING_SETUP_INFORMATION"]; + readonly "Alexa.ThermostatController.Schedule": readonly ["INSUFFICIENT_SPACE"]; +}; +/** "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 declare const ERROR_TYPES: Readonly>; +export {}; diff --git a/dist/esm/registry/interfaces/EndpointHealth.d.ts b/dist/esm/registry/interfaces/EndpointHealth.d.ts index a153326..0690d10 100644 --- a/dist/esm/registry/interfaces/EndpointHealth.d.ts +++ b/dist/esm/registry/interfaces/EndpointHealth.d.ts @@ -7,7 +7,7 @@ export declare const EndpointHealth: import("../types.js").InterfaceDescriptor<{ connectivity: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"UNREACHABLE" | "OK">; reason: import("../schema.js").OptionalSchema<"UNKNOWN" | "WIFI_BAD_PASSWORD" | "WIFI_AP_NOT_FOUND" | "WIFI_ROUTER_UNREACHABLE" | "WIFI_AP_CHANNEL_QUALITY_LOW" | "INTERNET_UNREACHABLE" | "CAPTIVE_PORTAL_CHECK_FAILED">; }>>; note: string; diff --git a/dist/esm/registry/interfaces/PowerController.d.ts b/dist/esm/registry/interfaces/PowerController.d.ts index 8d9efe1..ced59b5 100644 --- a/dist/esm/registry/interfaces/PowerController.d.ts +++ b/dist/esm/registry/interfaces/PowerController.d.ts @@ -1,7 +1,7 @@ export declare const PowerController: import("../types.js").InterfaceDescriptor<{ powerState: { name: string; - value: import("../schema.js").EnumSchema<"ON" | "OFF">; + value: import("../schema.js").EnumSchema<"OFF" | "ON">; }; }, { TurnOn: { diff --git a/dist/esm/registry/interfaces/SecurityPanelController.d.ts b/dist/esm/registry/interfaces/SecurityPanelController.d.ts index a54a898..7106f75 100644 --- a/dist/esm/registry/interfaces/SecurityPanelController.d.ts +++ b/dist/esm/registry/interfaces/SecurityPanelController.d.ts @@ -17,25 +17,25 @@ export declare const SecurityPanelController: import("../types.js").InterfaceDes burglaryAlarm: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"ALARM" | "OK">; }>>; }; fireAlarm: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"ALARM" | "OK">; }>>; }; carbonMonoxideAlarm: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"ALARM" | "OK">; }>>; }; waterAlarm: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"ALARM" | "OK">; }>>; }; }, { diff --git a/dist/esm/registry/interfaces/ThermostatController.d.ts b/dist/esm/registry/interfaces/ThermostatController.d.ts index 556bee3..3460baf 100644 --- a/dist/esm/registry/interfaces/ThermostatController.d.ts +++ b/dist/esm/registry/interfaces/ThermostatController.d.ts @@ -67,5 +67,5 @@ export declare const ThermostatController: import("../types.js").InterfaceDescri /** The user can set a temperature for a time: SetTargetTemperature comes with holdUntil. Default: false. */ supportsScheduling: import("../schema.js").OptionalSchema; /** The properties the thermostat has. Default: the three setpoints and thermostatMode. */ - properties: import("../schema.js").OptionalSchema<("targetSetpoint" | "lowerSetpoint" | "upperSetpoint" | "thermostatMode" | "adaptiveRecoveryStatus")[]>; + properties: import("../schema.js").OptionalSchema<("thermostatMode" | "targetSetpoint" | "lowerSetpoint" | "upperSetpoint" | "adaptiveRecoveryStatus")[]>; }>, false>; diff --git a/dist/esm/registry/interfaces/ToggleController.d.ts b/dist/esm/registry/interfaces/ToggleController.d.ts index 7d610d4..0a74d54 100644 --- a/dist/esm/registry/interfaces/ToggleController.d.ts +++ b/dist/esm/registry/interfaces/ToggleController.d.ts @@ -5,7 +5,7 @@ export declare const ToggleController: import("../types.js").InterfaceDescriptor<{ toggleState: { name: string; - value: import("../schema.js").EnumSchema<"ON" | "OFF">; + value: import("../schema.js").EnumSchema<"OFF" | "ON">; }; }, { TurnOn: { diff --git a/dist/types/compat/enums.d.ts b/dist/types/compat/enums.d.ts index 62b8701..64caa79 100644 --- a/dist/types/compat/enums.d.ts +++ b/dist/types/compat/enums.d.ts @@ -159,7 +159,7 @@ declare const Connectivity: { export declare const PowerController: import("../index.js").InterfaceDescriptor<{ powerState: { name: string; - value: import("../registry/schema.js").EnumSchema<"ON" | "OFF">; + value: import("../registry/schema.js").EnumSchema<"OFF" | "ON">; }; }, { TurnOn: { @@ -182,7 +182,7 @@ export declare const EndpointHealth: import("../index.js").InterfaceDescriptor<{ connectivity: { name: string; value: import("../index.js").Schema; + value: import("../registry/schema.js").EnumSchema<"UNREACHABLE" | "OK">; reason: import("../registry/schema.js").OptionalSchema<"UNKNOWN" | "WIFI_BAD_PASSWORD" | "WIFI_AP_NOT_FOUND" | "WIFI_ROUTER_UNREACHABLE" | "WIFI_AP_CHANNEL_QUALITY_LOW" | "INTERNET_UNREACHABLE" | "CAPTIVE_PORTAL_CHECK_FAILED">; }>>; note: string; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 75ec406..e143350 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -9,10 +9,10 @@ export { Alexa, BrightnessController, ChannelController, ColorController, ColorT export { PowerController, EndpointHealth } from "./compat/enums.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, CAUSES as Causes, DISPLAY_CATEGORIES as DisplayCategories, INPUTS as Inputs, THERMOSTAT_MODES as ThermostatModes, } 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, Cause, InputName, InventoryLevel, Mode, ThermostatModeName, 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, Cause, ErrorType, InputName, InventoryLevel, Mode, ThermostatModeName, Infer, Schema, Temperature, TimeInterval, } from "./registry/index.js"; 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, } from "./messages/index.js"; +export type { ChangeCause, ChangeReportMessage, ChargeState, ControlUnavailableReason, CurrentDeviceMode, DeferredResponseMessage, EndpointNeedingBypass, ErrorResponseMessage, Header, MaintenanceAction, ProactiveEventFields, ProactiveEventMessage, Property, PropertyOptions, ResourceType, ResponseMessage, SceneEventMessage, ValidRange, } from "./messages/index.js"; export * as topics from "./topics.js"; export { MemoryPublisher } from "./transport.js"; export type { Publisher, PublishResult } from "./transport.js"; diff --git a/dist/types/messages/errors.d.ts b/dist/types/messages/errors.d.ts index 1fad696..2412a15 100644 --- a/dist/types/messages/errors.d.ts +++ b/dist/types/messages/errors.d.ts @@ -1,3 +1,4 @@ +import type { ErrorType } from "../registry/catalog.js"; import type { Temperature } from "../registry/schema.js"; /** * The namespace an ErrorResponse of this type goes under: the one the table of error types lists it with @@ -17,36 +18,145 @@ export declare class AlexaError extends Error { readonly namespace: string; constructor(type: string, message: string, extra?: Record, namespace?: string); } -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"; -export declare const AlexaErrors: { - /** An error of any type. */ - of(type: string, message: string, extra?: Record): AlexaError; - /** The value is outside what the endpoint takes. For a temperature: temperatureOutOfRange(). */ - valueOutOfRange(message: string, validRange: { - minimumValue: number; - maximumValue: number; - }): AlexaError; - temperatureOutOfRange(message: string, validRange: { - minimumValue: Temperature; - maximumValue: Temperature; - }): AlexaError; +declare const CURRENT_DEVICE_MODES: readonly ["ASLEEP", "COLOR", "DEMAND_RESPONSE", "ECO", "NOT_PROVISIONED", "VACATION", "OTHER"]; +declare const REASONS: readonly ["DEEP_SLEEP_MODE", "OUT_OF_NETWORK_CONNECTIVITY", "NO_CONNECTIVITY_PACKAGE_ENABLED", "UNKNOWN"]; +declare const CHARGE_STATES: readonly ["ALREADY_CHARGED_TO_REQUIRED_LEVEL", "CURRENTLY_CHARGING", "FULLY_CHARGED", "NOT_CONNECTED_TO_POWER"]; +declare const RESOURCE_TYPES: readonly ["WATER"]; +declare const MAINTENANCE_ACTIONS: readonly ["EMPTY_BIN"]; +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 { + 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; +} +declare const withFields: { + /** The value is outside what the endpoint takes. For a temperature: temperatureValueOutOfRange(). */ + valueOutOfRange(message: string, validRange?: ValidRange): AlexaError; + temperatureValueOutOfRange(message: string, validRange?: ValidRange): AlexaError; /** A light showing a color asked for a color temperature: "COLOR". */ notSupportedInCurrentMode(message: string, currentDeviceMode: CurrentDeviceMode): AlexaError; /** percentageState: what is left of the battery, 0 to 100. */ endpointLowPower(message: string, percentageState?: number): AlexaError; endpointControlUnavailable(message: string, reason: ControlUnavailableReason): AlexaError; + /** currentChargeLevelInPercentage: the battery level, 0 to 100. */ notSupportedWithCurrentBatteryChargeState(message: string, currentChargeState: ChargeState, currentChargeLevelInPercentage?: number): AlexaError; /** What has to be refilled. */ - insufficientResource(message: string, resourceType: "WATER"): AlexaError; + insufficientResource(message: string, resourceType: ResourceType): AlexaError; /** What the user has to do first. */ - maintenanceRequired(message: string, maintenanceAction?: "EMPTY_BIN"): AlexaError; + maintenanceRequired(message: string, maintenanceAction?: MaintenanceAction): AlexaError; /** 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; /** 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; }; +type CamelCase = S extends `${infer First}_${infer Rest}` ? `${Lowercase}${Capitalize>}` : Lowercase; +/** The helpers of the types whose payload is the type and the message: AlexaErrors.endpointUnreachable(message). */ +export type PlainErrorHelpers = { + [T in ErrorType as CamelCase extends keyof typeof withFields ? never : CamelCase]: (message: string) => AlexaError; +}; +/** + * 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 declare const AlexaErrors: { + /** An error of any type, one the table does not have too. */ + of(type: string, message: string, extra?: Record): AlexaError; + /** temperatureValueOutOfRange() under its shorter name. */ + temperatureOutOfRange: (message: string, validRange?: ValidRange) => AlexaError; + /** requestedSetpointsTooClose() under its shorter name. */ + setpointsTooClose: (message: string, minimumTemperatureDelta?: Temperature) => AlexaError; + /** The value is outside what the endpoint takes. For a temperature: temperatureValueOutOfRange(). */ + valueOutOfRange(message: string, validRange?: ValidRange): AlexaError; + temperatureValueOutOfRange(message: string, validRange?: ValidRange): AlexaError; + /** A light showing a color asked for a color temperature: "COLOR". */ + notSupportedInCurrentMode(message: string, currentDeviceMode: CurrentDeviceMode): AlexaError; + /** percentageState: what is left of the battery, 0 to 100. */ + endpointLowPower(message: string, percentageState?: number): AlexaError; + endpointControlUnavailable(message: string, reason: ControlUnavailableReason): AlexaError; + /** currentChargeLevelInPercentage: the battery level, 0 to 100. */ + notSupportedWithCurrentBatteryChargeState(message: string, currentChargeState: ChargeState, currentChargeLevelInPercentage?: number): AlexaError; + /** What has to be refilled. */ + insufficientResource(message: string, resourceType: ResourceType): AlexaError; + /** What the user has to do first. */ + maintenanceRequired(message: string, maintenanceAction?: MaintenanceAction): AlexaError; + /** Goes under Alexa.ThermostatController. minimumTemperatureDelta: how far apart the setpoints have to be. */ + requestedSetpointsTooClose(message: string, minimumTemperatureDelta?: Temperature): AlexaError; + /** Goes under Alexa.SecurityPanelController. With the endpoints listed, the user can bypass them by voice. */ + bypassNeeded(message: string, endpointsNeedingBypass?: EndpointNeedingBypass[]): AlexaError; + alreadyInOperation: (message: string) => AlexaError; + bridgeUnreachable: (message: string) => AlexaError; + cloudControlDisabled: (message: string) => AlexaError; + deviceStuck: (message: string) => AlexaError; + doNotDisturbMode: (message: string) => AlexaError; + endpointBusy: (message: string) => AlexaError; + endpointUnreachable: (message: string) => AlexaError; + expiredAuthorizationCredential: (message: string) => AlexaError; + firmwareOutOfDate: (message: string) => AlexaError; + hardwareMalfunction: (message: string) => AlexaError; + insufficientPermissions: (message: string) => AlexaError; + internalError: (message: string) => AlexaError; + invalidAuthorizationCredential: (message: string) => AlexaError; + invalidDirective: (message: string) => AlexaError; + invalidValue: (message: string) => AlexaError; + noSuchEndpoint: (message: string) => AlexaError; + notCalibrated: (message: string) => AlexaError; + notInOperation: (message: string) => AlexaError; + partnerApplicationRedirection: (message: string) => AlexaError; + powerLevelNotSupported: (message: string) => AlexaError; + rateLimitExceeded: (message: string) => AlexaError; + tooManyFailedAttempts: (message: string) => AlexaError; + unableToCharge: (message: string) => AlexaError; + failedToBootstrapCommissioningProcess: (message: string) => AlexaError; + maxCommissioningLimitReached: (message: string) => AlexaError; + childLock: (message: string) => AlexaError; + cookDurationTooLong: (message: string) => AlexaError; + doorClosedTooLong: (message: string) => AlexaError; + doorOpen: (message: string) => AlexaError; + preheatRequired: (message: string) => AlexaError; + probeRequired: (message: string) => AlexaError; + remoteStartNotSupported: (message: string) => AlexaError; + removeProbe: (message: string) => AlexaError; + remoteStartDisabled: (message: string) => AlexaError; + dataDeletionNotSupported: (message: string) => AlexaError; + dataRetrievalNotSupported: (message: string) => AlexaError; + obstacleDetected: (message: string) => AlexaError; + safetyBeamBreached: (message: string) => AlexaError; + authorizationRequired: (message: string) => AlexaError; + notReady: (message: string) => AlexaError; + unauthorized: (message: string) => AlexaError; + unclearedAlarm: (message: string) => AlexaError; + unclearedTrouble: (message: string) => AlexaError; + subscriptionRequired: (message: string) => AlexaError; + disabledByUser: (message: string) => AlexaError; + dualSetpointsUnsupported: (message: string) => AlexaError; + thermostatIsOff: (message: string) => AlexaError; + tripleSetpointsUnsupported: (message: string) => AlexaError; + unsupportedThermostatMode: (message: string) => AlexaError; + unwillingToSetSchedule: (message: string) => AlexaError; + unwillingToSetValue: (message: string) => AlexaError; + configurationUpdateNotAllowed: (message: string) => AlexaError; + coolingLockoutTemperatureValueOutOfRange: (message: string) => AlexaError; + coolingStagesExceedsLimit: (message: string) => AlexaError; + heatingLockoutTemperatureValueOutOfRange: (message: string) => AlexaError; + heatingStagesExceedsLimit: (message: string) => AlexaError; + invalidAuxiliaryHeatingSystemType: (message: string) => AlexaError; + invalidSystemType: (message: string) => AlexaError; + invalidTargetState: (message: string) => AlexaError; + invalidTemperatureScale: (message: string) => AlexaError; + invalidTerminalConnection: (message: string) => AlexaError; + missingSetupInformation: (message: string) => AlexaError; + insufficientSpace: (message: string) => AlexaError; +}; +export {}; diff --git a/dist/types/messages/index.d.ts b/dist/types/messages/index.d.ts index 1fe2b45..b6feae2 100644 --- a/dist/types/messages/index.d.ts +++ b/dist/types/messages/index.d.ts @@ -1,7 +1,7 @@ export { changeReport, deferredResponse, doorbellPress, errorResponse, proactiveEvent, response, sceneEvent, simpleEvent, stateReport, MessageError, } from "./build.js"; export type { ChangeReportFields, DoorbellPressFields, ErrorResponseFields, ProactiveEventFields, ResponseFields, SceneEventFields, 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"; diff --git a/dist/types/registry/catalog.d.ts b/dist/types/registry/catalog.d.ts index 74d2e41..9a061ef 100644 --- a/dist/types/registry/catalog.d.ts +++ b/dist/types/registry/catalog.d.ts @@ -42,5 +42,21 @@ export declare const LIMITS: { readonly additionalAttributeLength: 256; readonly cookieBytes: 5000; }; +declare const ERROR_TYPES_BY_NAMESPACE: { + readonly Alexa: readonly ["ALREADY_IN_OPERATION", "BRIDGE_UNREACHABLE", "CLOUD_CONTROL_DISABLED", "DEVICE_STUCK", "DO_NOT_DISTURB_MODE", "ENDPOINT_BUSY", "ENDPOINT_CONTROL_UNAVAILABLE", "ENDPOINT_LOW_POWER", "ENDPOINT_UNREACHABLE", "EXPIRED_AUTHORIZATION_CREDENTIAL", "FIRMWARE_OUT_OF_DATE", "HARDWARE_MALFUNCTION", "INSUFFICIENT_PERMISSIONS", "INSUFFICIENT_RESOURCE", "INTERNAL_ERROR", "INVALID_AUTHORIZATION_CREDENTIAL", "INVALID_DIRECTIVE", "INVALID_VALUE", "MAINTENANCE_REQUIRED", "NO_SUCH_ENDPOINT", "NOT_CALIBRATED", "NOT_IN_OPERATION", "NOT_SUPPORTED_IN_CURRENT_MODE", "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE", "PARTNER_APPLICATION_REDIRECTION", "POWER_LEVEL_NOT_SUPPORTED", "RATE_LIMIT_EXCEEDED", "TEMPERATURE_VALUE_OUT_OF_RANGE", "TOO_MANY_FAILED_ATTEMPTS", "UNABLE_TO_CHARGE", "VALUE_OUT_OF_RANGE"]; + readonly "Alexa.Commissionable.ReportCommissioningInformation": readonly ["FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS", "MAX_COMMISSIONING_LIMIT_REACHED"]; + readonly "Alexa.Cooking": readonly ["CHILD_LOCK", "COOK_DURATION_TOO_LONG", "DOOR_CLOSED_TOO_LONG", "DOOR_OPEN", "PREHEAT_REQUIRED", "PROBE_REQUIRED", "REMOTE_START_NOT_SUPPORTED", "REMOVE_PROBE", "REMOTE_START_DISABLED"]; + readonly "Alexa.DataController": readonly ["DATA_DELETION_NOT_SUPPORTED", "DATA_RETRIEVAL_NOT_SUPPORTED"]; + readonly "Alexa.Safety": readonly ["OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED"]; + readonly "Alexa.SecurityPanelController": readonly ["AUTHORIZATION_REQUIRED", "BYPASS_NEEDED", "NOT_READY", "UNAUTHORIZED", "UNCLEARED_ALARM", "UNCLEARED_TROUBLE"]; + readonly "Alexa.SmartVision.ObjectDetectionSensor": readonly ["SUBSCRIPTION_REQUIRED"]; + readonly "Alexa.SmartVision.SnapshotProvider": readonly ["DISABLED_BY_USER"]; + readonly "Alexa.ThermostatController": readonly ["DUAL_SETPOINTS_UNSUPPORTED", "REQUESTED_SETPOINTS_TOO_CLOSE", "THERMOSTAT_IS_OFF", "TRIPLE_SETPOINTS_UNSUPPORTED", "UNSUPPORTED_THERMOSTAT_MODE", "UNWILLING_TO_SET_SCHEDULE", "UNWILLING_TO_SET_VALUE"]; + readonly "Alexa.ThermostatController.Configuration": readonly ["CONFIGURATION_UPDATE_NOT_ALLOWED", "COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", "COOLING_STAGES_EXCEEDS_LIMIT", "HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", "HEATING_STAGES_EXCEEDS_LIMIT", "INVALID_AUXILIARY_HEATING_SYSTEM_TYPE", "INVALID_SYSTEM_TYPE", "INVALID_TARGET_STATE", "INVALID_TEMPERATURE_SCALE", "INVALID_TERMINAL_CONNECTION", "MISSING_SETUP_INFORMATION"]; + readonly "Alexa.ThermostatController.Schedule": readonly ["INSUFFICIENT_SPACE"]; +}; +/** "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 declare const ERROR_TYPES: Readonly>; +export {}; diff --git a/dist/types/registry/interfaces/EndpointHealth.d.ts b/dist/types/registry/interfaces/EndpointHealth.d.ts index a153326..0690d10 100644 --- a/dist/types/registry/interfaces/EndpointHealth.d.ts +++ b/dist/types/registry/interfaces/EndpointHealth.d.ts @@ -7,7 +7,7 @@ export declare const EndpointHealth: import("../types.js").InterfaceDescriptor<{ connectivity: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"UNREACHABLE" | "OK">; reason: import("../schema.js").OptionalSchema<"UNKNOWN" | "WIFI_BAD_PASSWORD" | "WIFI_AP_NOT_FOUND" | "WIFI_ROUTER_UNREACHABLE" | "WIFI_AP_CHANNEL_QUALITY_LOW" | "INTERNET_UNREACHABLE" | "CAPTIVE_PORTAL_CHECK_FAILED">; }>>; note: string; diff --git a/dist/types/registry/interfaces/PowerController.d.ts b/dist/types/registry/interfaces/PowerController.d.ts index 8d9efe1..ced59b5 100644 --- a/dist/types/registry/interfaces/PowerController.d.ts +++ b/dist/types/registry/interfaces/PowerController.d.ts @@ -1,7 +1,7 @@ export declare const PowerController: import("../types.js").InterfaceDescriptor<{ powerState: { name: string; - value: import("../schema.js").EnumSchema<"ON" | "OFF">; + value: import("../schema.js").EnumSchema<"OFF" | "ON">; }; }, { TurnOn: { diff --git a/dist/types/registry/interfaces/SecurityPanelController.d.ts b/dist/types/registry/interfaces/SecurityPanelController.d.ts index a54a898..7106f75 100644 --- a/dist/types/registry/interfaces/SecurityPanelController.d.ts +++ b/dist/types/registry/interfaces/SecurityPanelController.d.ts @@ -17,25 +17,25 @@ export declare const SecurityPanelController: import("../types.js").InterfaceDes burglaryAlarm: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"ALARM" | "OK">; }>>; }; fireAlarm: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"ALARM" | "OK">; }>>; }; carbonMonoxideAlarm: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"ALARM" | "OK">; }>>; }; waterAlarm: { name: string; value: import("../schema.js").Schema; + value: import("../schema.js").EnumSchema<"ALARM" | "OK">; }>>; }; }, { diff --git a/dist/types/registry/interfaces/ThermostatController.d.ts b/dist/types/registry/interfaces/ThermostatController.d.ts index 556bee3..3460baf 100644 --- a/dist/types/registry/interfaces/ThermostatController.d.ts +++ b/dist/types/registry/interfaces/ThermostatController.d.ts @@ -67,5 +67,5 @@ export declare const ThermostatController: import("../types.js").InterfaceDescri /** The user can set a temperature for a time: SetTargetTemperature comes with holdUntil. Default: false. */ supportsScheduling: import("../schema.js").OptionalSchema; /** The properties the thermostat has. Default: the three setpoints and thermostatMode. */ - properties: import("../schema.js").OptionalSchema<("targetSetpoint" | "lowerSetpoint" | "upperSetpoint" | "thermostatMode" | "adaptiveRecoveryStatus")[]>; + properties: import("../schema.js").OptionalSchema<("thermostatMode" | "targetSetpoint" | "lowerSetpoint" | "upperSetpoint" | "adaptiveRecoveryStatus")[]>; }>, false>; diff --git a/dist/types/registry/interfaces/ToggleController.d.ts b/dist/types/registry/interfaces/ToggleController.d.ts index 7d610d4..0a74d54 100644 --- a/dist/types/registry/interfaces/ToggleController.d.ts +++ b/dist/types/registry/interfaces/ToggleController.d.ts @@ -5,7 +5,7 @@ export declare const ToggleController: import("../types.js").InterfaceDescriptor<{ toggleState: { name: string; - value: import("../schema.js").EnumSchema<"ON" | "OFF">; + value: import("../schema.js").EnumSchema<"OFF" | "ON">; }; }, { TurnOn: { diff --git a/src/index.ts b/src/index.ts index 24185ca..5524fd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 diff --git a/src/messages/errors.ts b/src/messages/errors.ts index bc5d901..9a23749 100644 --- a/src/messages/errors.ts +++ b/src/messages/errors.ts @@ -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 { + 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): Record { 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 = {}): 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(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(helper: string, validRange: ValidRange | undefined): ValidRange | 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): 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): 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 `${infer First}_${infer Rest}` + ? `${Lowercase}${Capitalize>}` + : Lowercase; +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 extends keyof typeof withFields ? never : CamelCase]: (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 = {}): AlexaError { + return new AlexaError(type, message, extra); + }, + + /** temperatureValueOutOfRange() under its shorter name. */ + temperatureOutOfRange: withFields.temperatureValueOutOfRange, + + /** requestedSetpointsTooClose() under its shorter name. */ + setpointsTooClose: withFields.requestedSetpointsTooClose, +}; diff --git a/src/messages/index.ts b/src/messages/index.ts index d23fdef..8cb1d51 100644 --- a/src/messages/index.ts +++ b/src/messages/index.ts @@ -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"; diff --git a/src/registry/catalog.ts b/src/registry/catalog.ts index fe1a03d..c1e91fb 100644 --- a/src/registry/catalog.ts +++ b/src/registry/catalog.ts @@ -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> = Object.fromEntries( Object.entries(ERROR_TYPES_BY_NAMESPACE).flatMap(([namespace, types]) => types.map((type) => [type, namespace])) diff --git a/test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_CONTROL_UNAVAILABLE.error.json b/test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_CONTROL_UNAVAILABLE.error.json new file mode 100644 index 0000000..bbc69cf --- /dev/null +++ b/test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_CONTROL_UNAVAILABLE.error.json @@ -0,0 +1,19 @@ +{ + "event": { + "header": { + "namespace": "Alexa", + "name": "ErrorResponse", + "messageId": "Unique identifier, preferably a version 4 UUID", + "correlationToken": "Opaque correlation token that matches the request", + "payloadVersion": "3" + }, + "endpoint": { + "endpointId": "Endpoint ID" + }, + "payload": { + "type": "ENDPOINT_CONTROL_UNAVAILABLE", + "message": "Device has no connectivity package enabled.", + "reason": "NO_CONNECTIVITY_PACKAGE_ENABLED" + } + } +} diff --git a/test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_LOW_POWER.error.json b/test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_LOW_POWER.error.json new file mode 100644 index 0000000..4ec46d3 --- /dev/null +++ b/test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_LOW_POWER.error.json @@ -0,0 +1,19 @@ +{ + "event": { + "header": { + "namespace": "Alexa", + "name": "ErrorResponse", + "messageId": "Unique identifier, preferably a version 4 UUID", + "correlationToken": "Opaque correlation token that matches the request", + "payloadVersion": "3" + }, + "endpoint": { + "endpointId": "Endpoint ID" + }, + "payload": { + "type": "ENDPOINT_LOW_POWER", + "message": "The lock battery is low", + "percentageState": 5 + } + } +} diff --git a/test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_IN_CURRENT_MODE.error.json b/test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_IN_CURRENT_MODE.error.json new file mode 100644 index 0000000..dd13596 --- /dev/null +++ b/test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_IN_CURRENT_MODE.error.json @@ -0,0 +1,19 @@ +{ + "event": { + "header": { + "namespace": "Alexa", + "name": "ErrorResponse", + "messageId": "Unique identifier, preferably a version 4 UUID", + "correlationToken": "Opaque correlation token that matches the request", + "payloadVersion": "3" + }, + "endpoint": { + "endpointId": "Endpoint ID" + }, + "payload": { + "type": "NOT_SUPPORTED_IN_CURRENT_MODE", + "message": "The device is in demand response mode.", + "currentDeviceMode": "DEMAND_RESPONSE" + } + } +} diff --git a/test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE.error.json b/test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE.error.json new file mode 100644 index 0000000..c6ddc50 --- /dev/null +++ b/test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE.error.json @@ -0,0 +1,20 @@ +{ + "event": { + "header": { + "namespace": "Alexa", + "name": "ErrorResponse", + "messageId": "Unique identifier, preferably a version 4 UUID", + "correlationToken": "Opaque correlation token that matches the request", + "payloadVersion": "3" + }, + "endpoint": { + "endpointId": "Endpoint ID" + }, + "payload": { + "type": "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE ", + "message": "The device has been charging", + "currentChargeState": "ALREADY_CHARGED_TO_REQUIRED_LEVEL", + "currentChargeLevelInPercentage": 75 + } + } +} diff --git a/test/fixtures/alexa-docs/alexa-errorresponse/TEMPERATURE_VALUE_OUT_OF_RANGE.error.json b/test/fixtures/alexa-docs/alexa-errorresponse/TEMPERATURE_VALUE_OUT_OF_RANGE.error.json new file mode 100644 index 0000000..fa65a39 --- /dev/null +++ b/test/fixtures/alexa-docs/alexa-errorresponse/TEMPERATURE_VALUE_OUT_OF_RANGE.error.json @@ -0,0 +1,28 @@ +{ + "event": { + "header": { + "namespace": "Alexa", + "name": "ErrorResponse", + "messageId": "Unique identifier, preferably a version 4 UUID", + "correlationToken": "Opaque correlation token that matches the request", + "payloadVersion": "3" + }, + "endpoint": { + "endpointId": "Endpoint ID" + }, + "payload": { + "type": "TEMPERATURE_VALUE_OUT_OF_RANGE", + "message": "The requested temperature of -15 is out of range.", + "validRange": { + "minimumValue": { + "value": 15, + "scale": "CELSIUS" + }, + "maximumValue": { + "value": 30, + "scale": "CELSIUS" + } + } + } + } +} diff --git a/test/fixtures/alexa-docs/alexa-errorresponse/VALUE_OUT_OF_RANGE.error.json b/test/fixtures/alexa-docs/alexa-errorresponse/VALUE_OUT_OF_RANGE.error.json new file mode 100644 index 0000000..52ad136 --- /dev/null +++ b/test/fixtures/alexa-docs/alexa-errorresponse/VALUE_OUT_OF_RANGE.error.json @@ -0,0 +1,22 @@ +{ + "event": { + "header": { + "namespace": "Alexa", + "name": "ErrorResponse", + "messageId": "Unique identifier, preferably a version 4 UUID", + "correlationToken": "Opaque correlation token that matches the request", + "payloadVersion": "3" + }, + "endpoint": { + "endpointId": "Endpoint ID" + }, + "payload": { + "type": "VALUE_OUT_OF_RANGE", + "message": "The percent value cannot exceed 100.", + "validRange": { + "minimumValue": 0, + "maximumValue": 100 + } + } + } +} diff --git a/test/fixtures/alexa-docs/alexa-safety-errorresponse/OBSTACLE_DETECTED.error.json b/test/fixtures/alexa-docs/alexa-safety-errorresponse/OBSTACLE_DETECTED.error.json new file mode 100644 index 0000000..8a06424 --- /dev/null +++ b/test/fixtures/alexa-docs/alexa-safety-errorresponse/OBSTACLE_DETECTED.error.json @@ -0,0 +1,18 @@ +{ + "event": { + "header": { + "namespace": "Alexa.Safety", + "name": "ErrorResponse", + "messageId": "Unique identifier, preferably a version 4 UUID", + "correlationToken": "Opaque correlation token that matches the request", + "payloadVersion": "3" + }, + "endpoint": { + "endpointId": "Endpoint ID" + }, + "payload": { + "type": "OBSTACLE_DETECTED", + "message": "There is an obstacle in the way." + } + } +} diff --git a/test/fixtures/alexa-docs/alexa-safety-errorresponse/SAFETY_BEAM_BREACHED.error.json b/test/fixtures/alexa-docs/alexa-safety-errorresponse/SAFETY_BEAM_BREACHED.error.json new file mode 100644 index 0000000..16a0f5d --- /dev/null +++ b/test/fixtures/alexa-docs/alexa-safety-errorresponse/SAFETY_BEAM_BREACHED.error.json @@ -0,0 +1,18 @@ +{ + "event": { + "header": { + "namespace": "Alexa.Safety", + "name": "ErrorResponse", + "messageId": "Unique identifier, preferably a version 4 UUID", + "correlationToken": "Opaque correlation token that matches the request", + "payloadVersion": "3" + }, + "endpoint": { + "endpointId": "Endpoint ID" + }, + "payload": { + "type": "SAFETY_BEAM_BREACHED", + "message": "The safety beam was breached." + } + } +} diff --git a/test/fixtures/types.ts b/test/fixtures/types.ts index f026ed5..65e3ddd 100644 --- a/test/fixtures/types.ts +++ b/test/fixtures/types.ts @@ -156,6 +156,24 @@ const changedNames: string[] = report.event.payload.change.properties.map((prope 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 diff --git a/test/messages/errors.test.js b/test/messages/errors.test.js index e0e1985..3cb1c30 100644 --- a/test/messages/errors.test.js +++ b/test/messages/errors.test.js @@ -3,6 +3,11 @@ const { test } = require("node:test"); const assert = require("node:assert/strict"); const { AlexaError, AlexaErrors, AlexaErrorType, messages } = require("alex2node"); +const { ERROR_TYPES } = require("../../dist/cjs/registry/catalog.js"); +const { doc, examples } = require("../helpers/fixtures"); + +const celsius = (value) => ({ value, scale: "CELSIUS" }); +const camelCase = (type) => type.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); test("the namespace follows the type: the interface that documents it, Alexa for the rest", () => { const namespaces = Object.fromEntries( @@ -37,7 +42,6 @@ test("AlexaError: an Error with the type, the message for Alexa, the fields of t }); test("the helpers: the type, the namespace and the payload fields as the pages spell them", () => { - const celsius = (value) => ({ value, scale: "CELSIUS" }); const cases = [ [AlexaErrors.valueOutOfRange("m", { minimumValue: 1, maximumValue: 10 }), "Alexa", "VALUE_OUT_OF_RANGE", { validRange: { minimumValue: 1, maximumValue: 10 } }], @@ -60,6 +64,10 @@ test("the helpers: the type, the namespace and the payload fields as the pages s [AlexaErrors.bypassNeeded("m", [{ friendlyName: "Front door" }]), "Alexa.SecurityPanelController", "BYPASS_NEEDED", { endpointsNeedingBypass: [{ friendlyName: "Front door" }] }], [AlexaErrors.of("OBSTACLE_DETECTED", "m"), "Alexa.Safety", "OBSTACLE_DETECTED", {}], + [AlexaErrors.valueOutOfRange("m"), "Alexa", "VALUE_OUT_OF_RANGE", {}], + [AlexaErrors.temperatureValueOutOfRange("m"), "Alexa", "TEMPERATURE_VALUE_OUT_OF_RANGE", {}], + [AlexaErrors.requestedSetpointsTooClose("m"), "Alexa.ThermostatController", "REQUESTED_SETPOINTS_TOO_CLOSE", {}], + [AlexaErrors.bypassNeeded("m"), "Alexa.SecurityPanelController", "BYPASS_NEEDED", {}], ]; for (const [error, namespace, type, extra] of cases) { assert.deepEqual( @@ -77,3 +85,92 @@ test("errorResponse: the namespace of the type, or the one the caller names", () }); assert.equal(messages.errorResponse({ ...fields, namespace: "Alexa" }).event.header.namespace, "Alexa"); }); + +test("every type of the table has a helper under its name in camel case, which takes the message", () => { + const types = Object.keys(ERROR_TYPES); + assert.equal(types.length, 73); + // The fields these types cannot do without + const fields = { + NOT_SUPPORTED_IN_CURRENT_MODE: ["ECO"], + ENDPOINT_CONTROL_UNAVAILABLE: ["UNKNOWN"], + NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE: ["CURRENTLY_CHARGING"], + INSUFFICIENT_RESOURCE: ["WATER"], + }; + for (const type of types) { + const helper = AlexaErrors[camelCase(type)]; + assert.equal(typeof helper, "function", `AlexaErrors.${camelCase(type)}`); + const error = helper("what went wrong", ...(fields[type] || [])); + assert.ok(error instanceof AlexaError, type); + assert.deepEqual( + { type: error.type, alexaMessage: error.alexaMessage, namespace: error.namespace }, + { type, alexaMessage: "what went wrong", namespace: ERROR_TYPES[type] } + ); + if (!fields[type]) assert.deepEqual(error.extra, {}, type); + } + // of() and the two shorter names are the only helpers that are not named after a type + assert.deepEqual( + Object.keys(AlexaErrors).filter((name) => !types.some((type) => camelCase(type) === name)).sort(), + ["of", "setpointsTooClose", "temperatureOutOfRange"] + ); +}); + +test("the helpers build the examples of the error pages", () => { + const open = [ + { friendlyName: "side window sensor", endpointId: "Endpoint ID" }, + { friendlyName: "front door sensor", endpointId: "Endpoint ID" }, + { friendlyName: "water sensor" }, + ]; + // What the helper of each example is called with after the message + const fields = { + ENDPOINT_CONTROL_UNAVAILABLE: ["NO_CONNECTIVITY_PACKAGE_ENABLED"], + ENDPOINT_LOW_POWER: [5], + NOT_SUPPORTED_IN_CURRENT_MODE: ["DEMAND_RESPONSE"], + NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE: ["ALREADY_CHARGED_TO_REQUIRED_LEVEL", 75], + TEMPERATURE_VALUE_OUT_OF_RANGE: [{ minimumValue: celsius(15), maximumValue: celsius(30) }], + VALUE_OUT_OF_RANGE: [{ minimumValue: 0, maximumValue: 100 }], + REQUESTED_SETPOINTS_TOO_CLOSE: [celsius(2)], + BYPASS_NEEDED: [open], + }; + const pages = [ + "alexa-errorresponse", "alexa-safety-errorresponse", "alexa-securitypanelcontroller-errorresponse", + "alexa-thermostatcontroller-errorresponse", + ]; + let compared = 0; + for (const page of pages) { + for (const example of examples(page, ".error")) { + const { header, endpoint, payload } = doc(page, example).event; + // The page prints the type of NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE with a space after it + const type = payload.type.trim(); + const error = AlexaErrors[camelCase(type)](payload.message, ...(fields[type] || [])); + const { event } = messages.errorResponse({ + endpointId: endpoint.endpointId, + messageId: header.messageId, + correlationToken: header.correlationToken, + type: error.type, + message: error.alexaMessage, + extra: error.extra, + namespace: error.namespace, + }); + assert.deepEqual(event.header, header, `${page} ${example}`); + assert.deepEqual(event.payload, { ...payload, type }, `${page} ${example}`); + compared += 1; + } + } + assert.equal(compared, 15); +}); + +test("a helper called without a field it needs, or with a value the page does not have, says what to pass", () => { + assert.throws( + () => AlexaErrors.endpointControlUnavailable("no connection"), + { name: "TypeError", message: /^AlexaErrors\.endpointControlUnavailable\(\) got undefined as reason: pass one of DEEP_SLEEP_MODE, / } + ); + assert.throws(() => AlexaErrors.notSupportedInCurrentMode("m", "PARTY"), /got "PARTY" as currentDeviceMode: pass one of ASLEEP, COLOR, /); + assert.throws(() => AlexaErrors.notSupportedWithCurrentBatteryChargeState("m"), /got undefined as currentChargeState/); + assert.throws(() => AlexaErrors.insufficientResource("m", "SOAP"), /got "SOAP" as resourceType: pass one of WATER/); + assert.throws(() => AlexaErrors.maintenanceRequired("m", "CLEAN"), /got "CLEAN" as maintenanceAction: pass one of EMPTY_BIN/); + assert.throws( + () => AlexaErrors.valueOutOfRange("m", { minimumValue: 0 }), + /^TypeError: AlexaErrors\.valueOutOfRange\(\) got a validRange without maximumValue: pass both ends, or no range$/ + ); + assert.throws(() => AlexaErrors.temperatureOutOfRange("m", { maximumValue: celsius(30) }), /temperatureValueOutOfRange\(\) got a validRange without minimumValue/); +});