diff --git a/dist/cjs/device/Device.js b/dist/cjs/device/Device.js index aec0758..486c70c 100644 --- a/dist/cjs/device/Device.js +++ b/dist/cjs/device/Device.js @@ -181,6 +181,7 @@ class Device extends events_1.EventEmitter { instance, payloadVersion: event.payloadVersion ?? capability.descriptor.version, payload: checked, + emptyContext: event.emptyContext, }); const bytes = Buffer.byteLength(JSON.stringify(message)); if (bytes > topics.EVENT_BYTES) diff --git a/dist/cjs/messages/build.js b/dist/cjs/messages/build.js index 161e227..6689aee 100644 --- a/dist/cjs/messages/build.js +++ b/dist/cjs/messages/build.js @@ -113,7 +113,11 @@ function sceneEvent(fields) { context: {}, }; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * 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. + */ function proactiveEvent(fields) { const { namespace, name, instance, messageId, payloadVersion, payload } = fields; return { @@ -122,13 +126,16 @@ function proactiveEvent(fields) { endpoint: { endpointId: fields.endpointId }, payload, }, + ...(fields.emptyContext ? { context: {} } : {}), }; } /** Somebody rang (alexa-doorbelleventsource.html). */ function doorbellPress(fields) { const { endpointId, messageId } = fields; const payload = { cause: { type: fields.cause ?? "PHYSICAL_INTERACTION" }, timestamp: (0, property_js_1.isoTime)(fields.timestamp) }; - return proactiveEvent({ endpointId, messageId, namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", payload }); + 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). */ function simpleEvent(fields) { diff --git a/dist/cjs/registry/interfaces/DoorbellEventSource.js b/dist/cjs/registry/interfaces/DoorbellEventSource.js index 661f6e9..d339bc8 100644 --- a/dist/cjs/registry/interfaces/DoorbellEventSource.js +++ b/dist/cjs/registry/interfaces/DoorbellEventSource.js @@ -18,7 +18,7 @@ exports.DoorbellEventSource = (0, types_js_1.defineInterface)({ properties: {}, directives: {}, events: { - DoorbellPress: { name: "DoorbellPress", payload: (0, events_js_1.happened)("PHYSICAL_INTERACTION"), topic: "proactive" }, + DoorbellPress: { name: "DoorbellPress", payload: (0, events_js_1.happened)("PHYSICAL_INTERACTION"), topic: "proactive", emptyContext: true }, }, // The capability is the announcement that the endpoint raises the event: proactivelyReported whatever was declared discovery: () => ({ properties: false, topLevel: { proactivelyReported: true } }), diff --git a/dist/esm/device/Device.js b/dist/esm/device/Device.js index 14f11e3..1a49bbd 100644 --- a/dist/esm/device/Device.js +++ b/dist/esm/device/Device.js @@ -146,6 +146,7 @@ class Device extends EventEmitter { instance, payloadVersion: event.payloadVersion ?? capability.descriptor.version, payload: checked, + emptyContext: event.emptyContext, }); const bytes = Buffer.byteLength(JSON.stringify(message)); if (bytes > topics.EVENT_BYTES) diff --git a/dist/esm/messages/build.d.ts b/dist/esm/messages/build.d.ts index bf44eff..5f327e5 100644 --- a/dist/esm/messages/build.d.ts +++ b/dist/esm/messages/build.d.ts @@ -88,8 +88,14 @@ export interface ProactiveEventFields extends Envelope { /** Default: "3". */ payloadVersion?: string; payload: Record; + /** true: the message has "context": {}, as the page of the event prints it. Default: no context. */ + emptyContext?: boolean; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * 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 declare function proactiveEvent(fields: ProactiveEventFields): ProactiveEventMessage; export interface DoorbellPressFields extends Envelope { /** Default: "PHYSICAL_INTERACTION", somebody pressed the button. */ diff --git a/dist/esm/messages/build.js b/dist/esm/messages/build.js index 9159b1d..65527b8 100644 --- a/dist/esm/messages/build.js +++ b/dist/esm/messages/build.js @@ -99,7 +99,11 @@ export function sceneEvent(fields) { context: {}, }; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * 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 { @@ -108,13 +112,16 @@ export function proactiveEvent(fields) { 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 }); + 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) { diff --git a/dist/esm/messages/types.d.ts b/dist/esm/messages/types.d.ts index aa644f3..dc4bfb0 100644 --- a/dist/esm/messages/types.d.ts +++ b/dist/esm/messages/types.d.ts @@ -73,4 +73,6 @@ export interface SceneEventMessage { /** An event a device raises by itself: DoorbellPress, the Event of Alexa.SimpleEventSource. */ export interface ProactiveEventMessage { event: Event; + /** Only with an event whose page prints it: DoorbellPress has an empty one, the Event of a button has none. */ + context?: {}; } diff --git a/dist/esm/registry/interfaces/DoorbellEventSource.js b/dist/esm/registry/interfaces/DoorbellEventSource.js index 20739d8..cb531ad 100644 --- a/dist/esm/registry/interfaces/DoorbellEventSource.js +++ b/dist/esm/registry/interfaces/DoorbellEventSource.js @@ -15,7 +15,7 @@ export const DoorbellEventSource = defineInterface({ properties: {}, directives: {}, events: { - DoorbellPress: { name: "DoorbellPress", payload: happened("PHYSICAL_INTERACTION"), topic: "proactive" }, + DoorbellPress: { name: "DoorbellPress", payload: happened("PHYSICAL_INTERACTION"), topic: "proactive", emptyContext: true }, }, // The capability is the announcement that the endpoint raises the event: proactivelyReported whatever was declared discovery: () => ({ properties: false, topLevel: { proactivelyReported: true } }), diff --git a/dist/esm/registry/types.d.ts b/dist/esm/registry/types.d.ts index 2daf117..82aaea2 100644 --- a/dist/esm/registry/types.d.ts +++ b/dist/esm/registry/types.d.ts @@ -66,6 +66,8 @@ export interface EventDescriptor { payload: Schema; /** response: the answer to a directive. proactive: sent without one. */ topic: "response" | "proactive"; + /** A proactive event that the page prints with "context": {}, DoorbellPress. Default: sent without a context. */ + emptyContext?: boolean; } /** What an interface adds to the capability object in discovery, next to the fields every capability has. */ export interface CapabilityExtras { diff --git a/dist/types/messages/build.d.ts b/dist/types/messages/build.d.ts index bf44eff..5f327e5 100644 --- a/dist/types/messages/build.d.ts +++ b/dist/types/messages/build.d.ts @@ -88,8 +88,14 @@ export interface ProactiveEventFields extends Envelope { /** Default: "3". */ payloadVersion?: string; payload: Record; + /** true: the message has "context": {}, as the page of the event prints it. Default: no context. */ + emptyContext?: boolean; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * 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 declare function proactiveEvent(fields: ProactiveEventFields): ProactiveEventMessage; export interface DoorbellPressFields extends Envelope { /** Default: "PHYSICAL_INTERACTION", somebody pressed the button. */ diff --git a/dist/types/messages/types.d.ts b/dist/types/messages/types.d.ts index aa644f3..dc4bfb0 100644 --- a/dist/types/messages/types.d.ts +++ b/dist/types/messages/types.d.ts @@ -73,4 +73,6 @@ export interface SceneEventMessage { /** An event a device raises by itself: DoorbellPress, the Event of Alexa.SimpleEventSource. */ export interface ProactiveEventMessage { event: Event; + /** Only with an event whose page prints it: DoorbellPress has an empty one, the Event of a button has none. */ + context?: {}; } diff --git a/dist/types/registry/types.d.ts b/dist/types/registry/types.d.ts index 2daf117..82aaea2 100644 --- a/dist/types/registry/types.d.ts +++ b/dist/types/registry/types.d.ts @@ -66,6 +66,8 @@ export interface EventDescriptor { payload: Schema; /** response: the answer to a directive. proactive: sent without one. */ topic: "response" | "proactive"; + /** A proactive event that the page prints with "context": {}, DoorbellPress. Default: sent without a context. */ + emptyContext?: boolean; } /** What an interface adds to the capability object in discovery, next to the fields every capability has. */ export interface CapabilityExtras { diff --git a/src/device/Device.ts b/src/device/Device.ts index b3169cf..24cb789 100644 --- a/src/device/Device.ts +++ b/src/device/Device.ts @@ -228,6 +228,7 @@ class Device extends EventEmitter { instance, payloadVersion: event.payloadVersion ?? capability.descriptor.version, payload: checked, + emptyContext: event.emptyContext, }); const bytes = Buffer.byteLength(JSON.stringify(message)); if (bytes > topics.EVENT_BYTES) return refuse(`it is ${bytes} bytes as JSON, Alex2MQTT takes ${topics.EVENT_BYTES}`); diff --git a/src/messages/build.ts b/src/messages/build.ts index 1744dc9..2d962e5 100644 --- a/src/messages/build.ts +++ b/src/messages/build.ts @@ -182,9 +182,15 @@ export interface ProactiveEventFields extends Envelope { /** Default: "3". */ payloadVersion?: string; payload: Record; + /** true: the message has "context": {}, as the page of the event prints it. Default: no context. */ + emptyContext?: boolean; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * 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: ProactiveEventFields): ProactiveEventMessage { const { namespace, name, instance, messageId, payloadVersion, payload } = fields; return { @@ -193,6 +199,7 @@ export function proactiveEvent(fields: ProactiveEventFields): ProactiveEventMess endpoint: { endpointId: fields.endpointId }, payload, }, + ...(fields.emptyContext ? { context: {} } : {}), }; } @@ -207,7 +214,9 @@ export interface DoorbellPressFields extends Envelope { export function doorbellPress(fields: DoorbellPressFields): ProactiveEventMessage { 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 }); + return proactiveEvent({ + endpointId, messageId, namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", payload, emptyContext: true, + }); } export interface SimpleEventFields extends Envelope { diff --git a/src/messages/types.ts b/src/messages/types.ts index 173064d..da420f9 100644 --- a/src/messages/types.ts +++ b/src/messages/types.ts @@ -70,4 +70,6 @@ export interface SceneEventMessage { /** An event a device raises by itself: DoorbellPress, the Event of Alexa.SimpleEventSource. */ export interface ProactiveEventMessage { event: Event; + /** Only with an event whose page prints it: DoorbellPress has an empty one, the Event of a button has none. */ + context?: {}; } diff --git a/src/registry/interfaces/DoorbellEventSource.ts b/src/registry/interfaces/DoorbellEventSource.ts index 461ad96..bace421 100644 --- a/src/registry/interfaces/DoorbellEventSource.ts +++ b/src/registry/interfaces/DoorbellEventSource.ts @@ -16,7 +16,7 @@ export const DoorbellEventSource = defineInterface({ properties: {}, directives: {}, events: { - DoorbellPress: { name: "DoorbellPress", payload: happened("PHYSICAL_INTERACTION"), topic: "proactive" }, + DoorbellPress: { name: "DoorbellPress", payload: happened("PHYSICAL_INTERACTION"), topic: "proactive", emptyContext: true }, }, // The capability is the announcement that the endpoint raises the event: proactivelyReported whatever was declared discovery: () => ({ properties: false, topLevel: { proactivelyReported: true } }), diff --git a/src/registry/types.ts b/src/registry/types.ts index 4aa144d..3841f42 100644 --- a/src/registry/types.ts +++ b/src/registry/types.ts @@ -61,6 +61,8 @@ export interface EventDescriptor { payload: Schema; /** response: the answer to a directive. proactive: sent without one. */ topic: "response" | "proactive"; + /** A proactive event that the page prints with "context": {}, DoorbellPress. Default: sent without a context. */ + emptyContext?: boolean; } /** What an interface adds to the capability object in discovery, next to the fields every capability has. */ diff --git a/test/device/raise.test.js b/test/device/raise.test.js index 239c918..5f17130 100644 --- a/test/device/raise.test.js +++ b/test/device/raise.test.js @@ -30,7 +30,8 @@ const refused = (raise, problem) => assert.throws(raise, (err) => err instanceof test("raise() publishes the event on /event, with the endpoint, a new messageId, the time and the usual cause", async () => { const { device, sent } = doorbell(); - const printed = doc("alexa-doorbelleventsource", "DoorbellPress.event").event; + const example = doc("alexa-doorbelleventsource", "DoorbellPress.event"); + const printed = example.event; const before = Date.now(); assert.deepEqual(await device.raise(DoorbellEventSource, "DoorbellPress"), { ok: true, topic: "root/event" }); @@ -39,6 +40,10 @@ test("raise() publishes the event on /event, with the endpoint, a new mess assert.equal(topics.event("root"), "root/event"); assert.deepEqual(sent.published.map(({ topic }) => topic), ["root/event", "root/event", "root/event"]); + for (const { message } of sent.published) { + assert.deepEqual(Object.keys(message).sort(), Object.keys(example).sort()); + assert.deepEqual(message.context, example.context); + } const [first, second, third] = sent.published.map(({ message }) => message.event); for (const event of [first, second, third]) { assert.deepEqual(event.endpoint, { endpointId: "door-1" }); @@ -62,7 +67,8 @@ test("raise() for an instance: the Event of a button has the instance in its hea remote.publisher = sent; const top = remote.add(SimpleEventSource, button("TopButton", ["SinglePush", "DoublePush"])); remote.add(SimpleEventSource, button("BottomButton", ["SinglePush"])); - const printed = doc("alexa-simpleeventsource", "Event.event").event; + const example = doc("alexa-simpleeventsource", "Event.event"); + const printed = example.event; await remote.raise(SimpleEventSource, "Event", printed.payload, { instance: top.instance, messageId: "push-1" }); assert.deepEqual(sent.published, [{ @@ -76,6 +82,7 @@ test("raise() for an instance: the Event of a button has the instance in its hea }, }]); assert.equal(printed.header.payloadVersion, "1.0"); + assert.deepEqual(Object.keys(sent.published[0].message), Object.keys(example)); const raised = "remote-1: Alexa.SimpleEventSource.Event was not raised: "; assert.throws(() => remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }), { @@ -117,7 +124,7 @@ test("raise() refuses an event the device has no capability for, and one that wo ); refused( () => device.raise(DoorbellEventSource, "DoorbellPress", { picture: "x".repeat(topics.EVENT_BYTES) }), - `${was("Alexa.DoorbellEventSource.DoorbellPress")}it is 16296 bytes as JSON, Alex2MQTT takes 16000` + `${was("Alexa.DoorbellEventSource.DoorbellPress")}it is 16309 bytes as JSON, Alex2MQTT takes 16000` ); assert.deepEqual(sent.published, []); }); diff --git a/test/messages/build.test.js b/test/messages/build.test.js index 6207ef0..249b188 100644 --- a/test/messages/build.test.js +++ b/test/messages/build.test.js @@ -4,6 +4,7 @@ const { test } = require("node:test"); const assert = require("node:assert/strict"); const { messages, MessageError } = require("alex2node"); +const { doc } = require("../helpers/fixtures.js"); const { UUID_V4 } = require("../helpers/harness.js"); const AT = "2026-09-28T10:00:00.000Z"; @@ -134,17 +135,21 @@ test("sceneEvent: ActivationStarted and DeactivationStarted with the cause and t assert.deepEqual(stopped.event.payload, { cause: { type: "APP_INTERACTION" }, timestamp: AT }); }); -test("doorbellPress: an event without a correlationToken and without a context", () => { - assert.deepEqual(messages.doorbellPress({ endpointId: "door", messageId, timestamp: AT }), { +test("doorbellPress: an event without a correlationToken, with the empty context of the page", () => { + const built = messages.doorbellPress({ endpointId: "door", messageId, timestamp: AT }); + assert.deepEqual(built, { event: { header: { namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", messageId, payloadVersion: "3" }, endpoint: { endpointId: "door" }, payload: { cause: { type: "PHYSICAL_INTERACTION" }, timestamp: AT }, }, + context: {}, }); + assert.deepEqual(built.context, doc("alexa-doorbelleventsource", "DoorbellPress.event").context); }); -test("simpleEvent: the instance in the header, payloadVersion 1.0", () => { +test("simpleEvent: the instance in the header, payloadVersion 1.0, no context as on the page", () => { + assert.equal("context" in doc("alexa-simpleeventsource", "Event.event"), false); assert.deepEqual(messages.simpleEvent({ endpointId: "button", instance: "Button.Front", id: "Button.SinglePush.1", messageId, timestamp: AT }), { event: { header: { namespace: "Alexa.SimpleEventSource", name: "Event", instance: "Button.Front", messageId, payloadVersion: "1.0" }, diff --git a/test/registry/events.test.js b/test/registry/events.test.js index deac817..bbb6181 100644 --- a/test/registry/events.test.js +++ b/test/registry/events.test.js @@ -30,9 +30,15 @@ test("the events of the pages fit the payload of their descriptor, and come back ["Alexa.WakeOnLANController", "alexa-wakeonlancontroller", "WakeUp", "response"], ]; for (const [namespace, page, name, topic] of printed) { - const { header, payload } = doc(page, `${name}.event`).event; + const example = doc(page, `${name}.event`); + const { header, payload } = example.event; const descriptor = registry.get(namespace); const event = descriptor.events[name]; + // raise() sends a context where the page prints one, and the pages print an empty one or none + if (topic === "proactive") { + assert.equal(event.emptyContext === true, "context" in example, name); + assert.deepEqual(example.context ?? {}, {}, name); + } assert.deepEqual([header.namespace, header.name, header.payloadVersion], [namespace, event.name, descriptor.version]); assert.equal(event.topic, topic, name); assert.equal("correlationToken" in header, topic === "response", name);