Alex2Node/dist/cjs/messages/errors.js
David 94b13f733a errors: typed helpers for every documented error type
AlexaErrors has a helper for each of the 73 types of the table on
alexa-errorresponse.html, named after the type in camel case: 63 take the
message only, 10 take the payload fields their page documents. A field the
page requires (reason, currentDeviceMode, currentChargeState, resourceType)
is a required argument, so leaving it out does not compile; from JavaScript
the helper throws a TypeError that lists the values. validRange is optional,
as the page says, and has both ends when given.
temperatureOutOfRange and setpointsTooClose stay as shorter names.
The helpers are compared with the 15 examples of the four error pages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 21:14:55 +00:00

131 lines
7.2 KiB
JavaScript

"use strict";
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 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
* (THERMOSTAT_IS_OFF: Alexa.ThermostatController, OBSTACLE_DETECTED: Alexa.Safety), Alexa for a type the table
* does not have.
*/
function errorNamespace(type) {
return Object.prototype.hasOwnProperty.call(catalog_js_1.ERROR_TYPES, type) ? catalog_js_1.ERROR_TYPES[type] : "Alexa";
}
/** Thrown by a directive handler, or built to be sent: the directive is answered with this ErrorResponse. */
class AlexaError extends Error {
constructor(type, message, extra = {}, namespace = errorNamespace(type)) {
super(`${type}: ${message}`);
this.name = "AlexaError";
this.type = type;
this.alexaMessage = message;
this.extra = extra;
this.namespace = namespace;
}
}
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));
}
// 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, given({ validRange: range("valueOutOfRange", 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) {
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: required("endpointControlUnavailable", "reason", reason, REASONS),
});
},
/** currentChargeLevelInPercentage: the battery level, 0 to 100. */
notSupportedWithCurrentBatteryChargeState(message, 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: required("insufficientResource", "resourceType", resourceType, RESOURCE_TYPES),
});
},
/** What the user has to do first. */
maintenanceRequired(message, 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. */
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. */
bypassNeeded(message, endpointsNeedingBypass) {
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,
};