registry: SecurityPanelController with Arm.Response and its errors

Alexa.SecurityPanelController is described from its page: version 3 (the stub of 1.5.2 said 1), armState and the
four alarms, Arm and Disarm. ctx.respond() answers Arm with Arm.Response under the namespace of the interface,
its payload checked (exitDelayInSeconds 0 to 255, bypassedEndpoints), and Disarm with a Response. A Disarm
carries the PIN the user said when the panel is declared with supportedAuthorizationTypes; the handler checks
it and answers UNAUTHORIZED, which goes under Alexa.SecurityPanelController like the five other error types.

A panel declared without options lists armState, where 1.5.2 listed no property; alarms are declared.
17 examples of the two pages are saved as fixtures. 215 tests pass, 207 before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 20:50:57 +00:00
parent f555a5df70
commit c59cf3ff10
39 changed files with 1248 additions and 23 deletions

View file

@ -12,6 +12,7 @@ import { PercentageController } from "./interfaces/PercentageController.js";
import { PowerController } from "./interfaces/PowerController.js";
import { PowerLevelController } from "./interfaces/PowerLevelController.js";
import { RangeController } from "./interfaces/RangeController.js";
import { SecurityPanelController } from "./interfaces/SecurityPanelController.js";
import { TemperatureSensor } from "./interfaces/TemperatureSensor.js";
import { ThermostatController } from "./interfaces/ThermostatController.js";
import { ThermostatControllerSchedule } from "./interfaces/ThermostatControllerSchedule.js";
@ -25,7 +26,7 @@ export declare const registry: {
/** Every descriptor, ordered by namespace. */
list(): AnyDescriptor[];
};
export { Alexa, BrightnessController, ColorController, ColorTemperatureController, ContactSensor, EndpointHealth, HumiditySensor, LockController, ModeController, MotionSensor, PercentageController, PowerController, PowerLevelController, RangeController, TemperatureSensor, ThermostatController, ThermostatControllerSchedule, ToggleController, };
export { Alexa, BrightnessController, ColorController, ColorTemperatureController, ContactSensor, EndpointHealth, HumiditySensor, LockController, ModeController, MotionSensor, PercentageController, PowerController, PowerLevelController, RangeController, SecurityPanelController, TemperatureSensor, ThermostatController, ThermostatControllerSchedule, ToggleController, };
export type { Mode } from "./interfaces/ModeController.js";
export { THERMOSTAT_MODES } from "./interfaces/ThermostatController.js";
export type { ThermostatModeName } from "./interfaces/ThermostatController.js";

View file

@ -13,6 +13,7 @@ import { PercentageController } from "./interfaces/PercentageController.js";
import { PowerController } from "./interfaces/PowerController.js";
import { PowerLevelController } from "./interfaces/PowerLevelController.js";
import { RangeController } from "./interfaces/RangeController.js";
import { SecurityPanelController } from "./interfaces/SecurityPanelController.js";
import { TemperatureSensor } from "./interfaces/TemperatureSensor.js";
import { ThermostatController } from "./interfaces/ThermostatController.js";
import { ThermostatControllerSchedule } from "./interfaces/ThermostatControllerSchedule.js";
@ -34,6 +35,7 @@ const described = [
PowerController,
PowerLevelController,
RangeController,
SecurityPanelController,
TemperatureSensor,
ThermostatController,
ThermostatControllerSchedule,
@ -63,7 +65,7 @@ export const registry = {
return [...descriptors.values()].sort((a, b) => (a.namespace < b.namespace ? -1 : 1));
},
};
export { Alexa, BrightnessController, ColorController, ColorTemperatureController, ContactSensor, EndpointHealth, HumiditySensor, LockController, ModeController, MotionSensor, PercentageController, PowerController, PowerLevelController, RangeController, TemperatureSensor, ThermostatController, ThermostatControllerSchedule, ToggleController, };
export { Alexa, BrightnessController, ColorController, ColorTemperatureController, ContactSensor, EndpointHealth, HumiditySensor, LockController, ModeController, MotionSensor, PercentageController, PowerController, PowerLevelController, RangeController, SecurityPanelController, TemperatureSensor, ThermostatController, ThermostatControllerSchedule, ToggleController, };
export { THERMOSTAT_MODES } from "./interfaces/ThermostatController.js";
export { asset, text } from "./resources.js";
export { semantics, SemanticsBuilder } from "./semantics.js";

View file

@ -0,0 +1,67 @@
/**
* A security panel. Arm is answered with Arm.Response, Disarm with a Response, an error of errorTypes under the
* namespace of the interface (alexa-securitypanelcontroller-errorresponse.html).
*
* Alexa sends Disarm only when the user switched on disarming by voice in the Alexa app. A panel declared with
* supportedAuthorizationTypes gets the PIN the user said in payload.authorization and has to check it: a wrong PIN
* is answered with UNAUTHORIZED. Without authorization in the payload Alexa checked a voice code of its own.
*
* A panel that is ARMED_AWAY is disarmed before it is armed another way: Arm to ARMED_STAY or ARMED_NIGHT is
* then answered with AUTHORIZATION_REQUIRED. Arm to the state the panel is in is answered with Arm.Response.
*/
export declare const SecurityPanelController: import("../types.js").InterfaceDescriptor<{
armState: {
name: string;
value: import("../schema.js").EnumSchema<"ARMED_AWAY" | "ARMED_STAY" | "ARMED_NIGHT" | "DISARMED">;
};
burglaryAlarm: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").InferShape<{
value: import("../schema.js").EnumSchema<"OK" | "ALARM">;
}>>;
};
fireAlarm: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").InferShape<{
value: import("../schema.js").EnumSchema<"OK" | "ALARM">;
}>>;
};
carbonMonoxideAlarm: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").InferShape<{
value: import("../schema.js").EnumSchema<"OK" | "ALARM">;
}>>;
};
waterAlarm: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").InferShape<{
value: import("../schema.js").EnumSchema<"OK" | "ALARM">;
}>>;
};
}, {
Arm: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
armState: import("../schema.js").EnumSchema<"ARMED_AWAY" | "ARMED_STAY" | "ARMED_NIGHT" | "DISARMED">;
bypassType: import("../schema.js").OptionalSchema<"BYPASS_ALL">;
}>>;
note: string;
};
Disarm: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
authorization: import("../schema.js").OptionalSchema<import("../schema.js").InferShape<{
type: import("../schema.js").EnumSchema<"FOUR_DIGIT_PIN">;
value: import("../schema.js").Schema<string>;
}>>;
}>>;
note: string;
};
}, import("../schema.js").InferShape<{
/** Default: Alexa takes the panel to have all four. */
supportedArmStates: import("../schema.js").OptionalSchema<("ARMED_AWAY" | "ARMED_STAY" | "ARMED_NIGHT" | "DISARMED")[]>;
/** For a panel that has four digit PINs and checks the PIN of a Disarm itself. */
supportedAuthorizationTypes: import("../schema.js").OptionalSchema<"FOUR_DIGIT_PIN"[]>;
/** The alarms the panel reports. Default: none. */
alarms: import("../schema.js").OptionalSchema<("burglaryAlarm" | "fireAlarm" | "carbonMonoxideAlarm" | "waterAlarm")[]>;
}>, false>;

View file

@ -0,0 +1,88 @@
import { s } from "../schema.js";
import { DeclarationError, defineInterface } from "../types.js";
const ARM_STATES = ["ARMED_AWAY", "ARMED_STAY", "ARMED_NIGHT", "DISARMED"];
const ALARMS = ["burglaryAlarm", "fireAlarm", "carbonMonoxideAlarm", "waterAlarm"];
const alarm = s.object({ value: s.enum("OK", "ALARM") });
// A sensor that is left out when the panel arms. The endpointId is for a sensor that is an endpoint of its own.
const bypassed = s.object({ friendlyName: s.optional(s.string({ min: 1 })), endpointId: s.optional(s.string({ min: 1 })) });
const armResponse = s.object({
/** How long the occupants have to leave before the panel arms. 0: it arms at once. */
exitDelayInSeconds: s.optional(s.number({ min: 0, max: 255, integer: true })),
/** For an Arm that came with bypassType only. */
bypassedEndpoints: s.optional(s.array(bypassed)),
});
/**
* A security panel. Arm is answered with Arm.Response, Disarm with a Response, an error of errorTypes under the
* namespace of the interface (alexa-securitypanelcontroller-errorresponse.html).
*
* Alexa sends Disarm only when the user switched on disarming by voice in the Alexa app. A panel declared with
* supportedAuthorizationTypes gets the PIN the user said in payload.authorization and has to check it: a wrong PIN
* is answered with UNAUTHORIZED. Without authorization in the payload Alexa checked a voice code of its own.
*
* A panel that is ARMED_AWAY is disarmed before it is armed another way: Arm to ARMED_STAY or ARMED_NIGHT is
* then answered with AUTHORIZATION_REQUIRED. Arm to the state the panel is in is answered with Arm.Response.
*/
export const SecurityPanelController = defineInterface({
namespace: "Alexa.SecurityPanelController",
version: "3",
doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-securitypanelcontroller.html",
kind: "controller",
tier: 2,
instanced: false,
properties: {
armState: { name: "armState", value: s.enum(...ARM_STATES) },
burglaryAlarm: { name: "burglaryAlarm", value: alarm },
fireAlarm: { name: "fireAlarm", value: alarm },
carbonMonoxideAlarm: { name: "carbonMonoxideAlarm", value: alarm },
waterAlarm: { name: "waterAlarm", value: alarm },
},
directives: {
Arm: {
name: "Arm",
payload: s.object({ armState: s.enum(...ARM_STATES), bypassType: s.optional(s.enum("BYPASS_ALL")) }),
note: "bypassType: the user said to arm with the open sensors left out, which BYPASS_NEEDED listed",
},
Disarm: {
name: "Disarm",
payload: s.object({
authorization: s.optional(s.object({
type: s.enum("FOUR_DIGIT_PIN"),
value: s.string({ pattern: /^\d{4}$/, expects: "a PIN of four digits" }),
})),
}),
note: "authorization: the PIN the user said, for the panel to check",
},
},
options: s.object({
/** Default: Alexa takes the panel to have all four. */
supportedArmStates: s.optional(s.array(s.enum(...ARM_STATES), { min: 1 })),
/** For a panel that has four digit PINs and checks the PIN of a Disarm itself. */
supportedAuthorizationTypes: s.optional(s.array(s.enum("FOUR_DIGIT_PIN"), { min: 1 })),
/** The alarms the panel reports. Default: none. */
alarms: s.optional(s.array(s.enum(...ALARMS))),
}, { unknownKeys: "reject" }),
discovery({ options: { supportedArmStates, supportedAuthorizationTypes, alarms = [] } }) {
const configuration = {
...(supportedArmStates && { supportedArmStates: supportedArmStates.map((value) => ({ value })) }),
...(supportedAuthorizationTypes && { supportedAuthorizationTypes: supportedAuthorizationTypes.map((type) => ({ type })) }),
};
return {
supported: ["armState", ...alarms],
...(Object.keys(configuration).length > 0 && { configuration }),
};
},
validate(capability) {
const { supportedArmStates = [], supportedAuthorizationTypes = [], alarms = [] } = capability.options;
const lists = [supportedArmStates, supportedAuthorizationTypes, alarms];
for (const list of lists) {
const twice = list.find((entry, i) => list.indexOf(entry) !== i);
if (twice)
throw new DeclarationError(capability, `${twice} is listed twice`);
}
},
responseFor: (directive) => (directive === "Arm"
? { namespace: "Alexa.SecurityPanelController", name: "Arm.Response", payload: armResponse }
: undefined),
errorNamespace: "Alexa.SecurityPanelController",
errorTypes: ["AUTHORIZATION_REQUIRED", "BYPASS_NEEDED", "NOT_READY", "UNAUTHORIZED", "UNCLEARED_ALARM", "UNCLEARED_TROUBLE"],
});

View file

@ -48,7 +48,6 @@ const TABLE = [
["Alexa.RecordController", "3", [], "alexa-recordcontroller.html"],
["Alexa.RemoteVideoPlayer", "1", [], "alexa-remotevideoplayer.html"],
["Alexa.SceneController", "3", [], "alexa-scenecontroller.html"],
["Alexa.SecurityPanelController", "1", [], "alexa-securitypanelcontroller.html"],
["Alexa.SecurityPanelController.Alert", "1", [], "alexa-securitypanelcontroller-alert.html"],
["Alexa.SeekController", "3", [], "alexa-seekcontroller.html"],
["Alexa.SimpleEventSource", "1", [], "alexa-simpleeventsource.html"],