proactiveEvent() built every proactive event without a context, so doorbellPress() and
device.raise(DoorbellEventSource, "DoorbellPress") left out the "context": {} that the example of
alexa-doorbelleventsource.html has. The Event of Alexa.SimpleEventSource is printed without one, so
the event descriptor says which it is (emptyContext) and raise() passes it on to the builder.
A raised DoorbellPress is now { event, context: {} }, 13 bytes longer; the Event of a button is
unchanged. The tests compare the keys and the context of the built message with the fixture of the
page, for the builder, the descriptor and raise(). 267 tests pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
131 lines
6.1 KiB
JavaScript
131 lines
6.1 KiB
JavaScript
// Every message the bridge publishes, built from plain values. Nothing here reads the clock or makes an id unless
|
|
// the caller leaves messageId or a time out, so a test that passes both compares whole objects.
|
|
import { randomUUID } from "crypto";
|
|
import { errorNamespace } from "./errors.js";
|
|
import { isoTime } from "./property.js";
|
|
/** A message that would be dropped on its way to Alexa, refused where it is built. */
|
|
export class MessageError extends Error {
|
|
constructor(endpointId, problem) {
|
|
super(`${endpointId}: ${problem}`);
|
|
this.endpointId = endpointId;
|
|
this.problem = problem;
|
|
this.name = "MessageError";
|
|
}
|
|
}
|
|
// The fields in the order of the examples
|
|
function header(namespace, name, fields) {
|
|
return {
|
|
namespace,
|
|
name,
|
|
...(fields.instance ? { instance: fields.instance } : {}),
|
|
messageId: fields.messageId ?? randomUUID(),
|
|
...(fields.correlationToken !== undefined ? { correlationToken: fields.correlationToken } : {}),
|
|
payloadVersion: fields.payloadVersion ?? "3",
|
|
};
|
|
}
|
|
/**
|
|
* The answer to a directive (alexa-response.html, "Synchronous response"). An endpoint with nothing to report
|
|
* answers with an empty list of properties, not without a context (state-reporting-for-smart-home-addons.html,
|
|
* "Directive response example for a property that isn't retrievable").
|
|
*/
|
|
export function response(fields) {
|
|
return {
|
|
event: {
|
|
header: header(fields.namespace ?? "Alexa", fields.name ?? "Response", fields),
|
|
endpoint: { endpointId: fields.endpointId },
|
|
payload: fields.payload ?? {},
|
|
},
|
|
context: { properties: [...(fields.context ?? [])] },
|
|
};
|
|
}
|
|
/** The answer to ReportState: every retrievable property of the endpoint. */
|
|
export function stateReport(fields) {
|
|
return response({ ...fields, name: "StateReport", namespace: "Alexa", payload: {} });
|
|
}
|
|
/**
|
|
* "The directive arrived, the answer follows": no context, the state is in the Response that follows
|
|
* (alexa-response.html, "Deferred response example").
|
|
*/
|
|
export function deferredResponse(fields) {
|
|
const { estimatedDeferralInSeconds } = fields;
|
|
return {
|
|
event: {
|
|
header: header("Alexa", "DeferredResponse", fields),
|
|
endpoint: { endpointId: fields.endpointId },
|
|
payload: estimatedDeferralInSeconds === undefined ? {} : { estimatedDeferralInSeconds },
|
|
},
|
|
};
|
|
}
|
|
/** The answer to a directive the endpoint could not follow (alexa-errorresponse.html). */
|
|
export function errorResponse(fields) {
|
|
return {
|
|
event: {
|
|
header: header(fields.namespace ?? errorNamespace(fields.type), "ErrorResponse", fields),
|
|
endpoint: { endpointId: fields.endpointId },
|
|
payload: { type: fields.type, message: fields.message, ...fields.extra },
|
|
},
|
|
};
|
|
}
|
|
/** The same property of the same instance, whatever its value. */
|
|
export const sameProperty = (a, b) => a.namespace === b.namespace && a.name === b.name && (a.instance ?? "") === (b.instance ?? "");
|
|
/**
|
|
* A change of state nobody asked for. The header has no correlationToken (message-guide.html, "Header object").
|
|
* A property that changed is left out of the context: it is reported in one of the two (same page, "Context
|
|
* object"). Throws MessageError when nothing changed: Alex2MQTT drops such a report without a word.
|
|
*/
|
|
export function changeReport(fields) {
|
|
const { endpointId, changed, context = [] } = fields;
|
|
if (changed.length === 0) {
|
|
throw new MessageError(endpointId, "a ChangeReport needs at least one property that changed, this one has none. "
|
|
+ "Add the changed property before unchanged(), or send no report when nothing changed");
|
|
}
|
|
return {
|
|
event: {
|
|
header: header("Alexa", "ChangeReport", { messageId: fields.messageId }),
|
|
endpoint: { endpointId },
|
|
payload: { change: { cause: { type: fields.cause ?? "PHYSICAL_INTERACTION" }, properties: [...changed] } },
|
|
},
|
|
context: { properties: context.filter((property) => !changed.some((other) => sameProperty(property, other))) },
|
|
};
|
|
}
|
|
/** The answer to Activate and Deactivate of a scene (alexa-scenecontroller.html). */
|
|
export function sceneEvent(fields) {
|
|
return {
|
|
event: {
|
|
header: header("Alexa.SceneController", fields.activated ? "ActivationStarted" : "DeactivationStarted", fields),
|
|
endpoint: { endpointId: fields.endpointId },
|
|
payload: { cause: { type: fields.cause ?? "VOICE_INTERACTION" }, timestamp: isoTime(fields.timestamp) },
|
|
},
|
|
context: {},
|
|
};
|
|
}
|
|
/**
|
|
* An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. The
|
|
* pages differ on the context, so the caller says which it is: DoorbellPress is printed with an empty one
|
|
* (alexa-doorbelleventsource.html), the Event of Alexa.SimpleEventSource without.
|
|
*/
|
|
export function proactiveEvent(fields) {
|
|
const { namespace, name, instance, messageId, payloadVersion, payload } = fields;
|
|
return {
|
|
event: {
|
|
header: header(namespace, name, { instance, messageId, payloadVersion }),
|
|
endpoint: { endpointId: fields.endpointId },
|
|
payload,
|
|
},
|
|
...(fields.emptyContext ? { context: {} } : {}),
|
|
};
|
|
}
|
|
/** Somebody rang (alexa-doorbelleventsource.html). */
|
|
export function doorbellPress(fields) {
|
|
const { endpointId, messageId } = fields;
|
|
const payload = { cause: { type: fields.cause ?? "PHYSICAL_INTERACTION" }, timestamp: isoTime(fields.timestamp) };
|
|
return proactiveEvent({
|
|
endpointId, messageId, namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", payload, emptyContext: true,
|
|
});
|
|
}
|
|
/** An event of a button or a sensor that routines start on (alexa-simpleeventsource.html). */
|
|
export function simpleEvent(fields) {
|
|
const { endpointId, messageId, instance } = fields;
|
|
const payload = { id: fields.id, timestamp: isoTime(fields.timestamp) };
|
|
return proactiveEvent({ endpointId, messageId, instance, namespace: "Alexa.SimpleEventSource", name: "Event", payloadVersion: "1.0", payload });
|
|
}
|