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/); +});