// 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. */ 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, }, }; } /** 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 }); } /** 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 }); }