messages: pure builders for every event the bridge sends
src/messages builds Response, StateReport, DeferredResponse, ErrorResponse, ChangeReport and the scene, doorbell and simple events from plain values; messageId and times are parameters, so a test compares whole objects. StateBuilder collects properties checked by the descriptors, AlexaError and AlexaErrors carry the payload fields of an error type. AlexaStatusMessage and AlexaErrorResponse move to src/compat and are written on the builders. On the wire, against 1.5.2: a DeferredResponse has no context key, a ChangeReport has no correlationToken, the namespace of an ErrorResponse follows its type, and a ChangeReport without a changed property is not published: send() resolves "" and the bridge reports an error that names the endpoint. 143 tests pass (108 before). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
2558e21787
commit
46fa06728c
68 changed files with 3410 additions and 1208 deletions
105
src/messages/errors.ts
Normal file
105
src/messages/errors.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// 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.
|
||||
import { ERROR_TYPES } 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
|
||||
* (THERMOSTAT_IS_OFF: Alexa.ThermostatController, OBSTACLE_DETECTED: Alexa.Safety), Alexa for a type the table
|
||||
* does not have.
|
||||
*/
|
||||
export function errorNamespace(type: string): string {
|
||||
return Object.prototype.hasOwnProperty.call(ERROR_TYPES, type) ? ERROR_TYPES[type] : "Alexa";
|
||||
}
|
||||
|
||||
/** Thrown by a directive handler, or built to be sent: the directive is answered with this ErrorResponse. */
|
||||
export class AlexaError extends Error {
|
||||
/** "ENDPOINT_UNREACHABLE" */
|
||||
readonly type: string;
|
||||
/** payload.message: for the log of the skill, Alexa does not read it to the user. */
|
||||
readonly alexaMessage: string;
|
||||
/** The fields of the payload next to type and message. */
|
||||
readonly extra: Record<string, unknown>;
|
||||
/** The namespace of the header. */
|
||||
readonly namespace: string;
|
||||
|
||||
constructor(type: string, message: string, extra: Record<string, unknown> = {}, namespace: string = errorNamespace(type)) {
|
||||
super(`${type}: ${message}`);
|
||||
this.name = "AlexaError";
|
||||
this.type = type;
|
||||
this.alexaMessage = message;
|
||||
this.extra = extra;
|
||||
this.namespace = namespace;
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
// A field that was not given is left out of the payload
|
||||
function given(fields: Record<string, unknown>): Record<string, unknown> {
|
||||
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
||||
}
|
||||
|
||||
export const AlexaErrors = {
|
||||
/** An error of any type. */
|
||||
of(type: string, message: string, extra: Record<string, unknown> = {}): AlexaError {
|
||||
return new AlexaError(type, message, extra);
|
||||
},
|
||||
|
||||
/** 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 });
|
||||
},
|
||||
|
||||
/** 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 });
|
||||
},
|
||||
|
||||
/** percentageState: what is left of the battery, 0 to 100. */
|
||||
endpointLowPower(message: string, percentageState?: number): AlexaError {
|
||||
return new AlexaError("ENDPOINT_LOW_POWER", message, given({ percentageState }));
|
||||
},
|
||||
|
||||
endpointControlUnavailable(message: string, reason: ControlUnavailableReason): AlexaError {
|
||||
return new AlexaError("ENDPOINT_CONTROL_UNAVAILABLE", message, { reason });
|
||||
},
|
||||
|
||||
notSupportedWithCurrentBatteryChargeState(
|
||||
message: string,
|
||||
currentChargeState: ChargeState,
|
||||
currentChargeLevelInPercentage?: number
|
||||
): AlexaError {
|
||||
return new AlexaError(
|
||||
"NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE",
|
||||
message,
|
||||
given({ currentChargeState, currentChargeLevelInPercentage })
|
||||
);
|
||||
},
|
||||
|
||||
/** What has to be refilled. */
|
||||
insufficientResource(message: string, resourceType: "WATER"): AlexaError {
|
||||
return new AlexaError("INSUFFICIENT_RESOURCE", message, { resourceType });
|
||||
},
|
||||
|
||||
/** What the user has to do first. */
|
||||
maintenanceRequired(message: string, maintenanceAction?: "EMPTY_BIN"): AlexaError {
|
||||
return new AlexaError("MAINTENANCE_REQUIRED", message, given({ maintenanceAction }));
|
||||
},
|
||||
|
||||
/** Goes under Alexa.ThermostatController. minimumTemperatureDelta: how far apart the setpoints have to be. */
|
||||
setpointsTooClose(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 {
|
||||
return new AlexaError("BYPASS_NEEDED", message, given({ endpointsNeedingBypass }));
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue