// 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 }, }, }; } 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: {}, }; } /** Somebody rang (alexa-doorbelleventsource.html). */ export function doorbellPress(fields) { return { event: { header: header("Alexa.DoorbellEventSource", "DoorbellPress", { messageId: fields.messageId }), endpoint: { endpointId: fields.endpointId }, payload: { cause: { type: fields.cause ?? "PHYSICAL_INTERACTION" }, timestamp: isoTime(fields.timestamp) }, }, }; } /** An event of a button or a sensor that routines start on (alexa-simpleeventsource.html). */ export function simpleEvent(fields) { return { event: { header: header("Alexa.SimpleEventSource", "Event", { instance: fields.instance, messageId: fields.messageId, payloadVersion: "1.0", }), endpoint: { endpointId: fields.endpointId }, payload: { id: fields.id, timestamp: isoTime(fields.timestamp) }, }, }; }