device: changeReport(cause, fill) sends a ChangeReport on the 2.0 API

The design has it (section 5) and 2.0 lacked it: a ChangeReport could only be
sent with getChangeReport() of 1.x, which has no helper for a contact or a
motion sensor. fill sets what changed, and after unchanged() what did not;
the rest of the context is the state of device.state(). The report goes to
<root>/changeReport. It throws MessageError without a changed property and
resolves with the failure when the device is on no bridge, as raise() does.
Four tests in test/device/raise.test.js.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 21:37:37 +00:00
parent f0ca4c1fa4
commit 2cbcde563e
6 changed files with 165 additions and 6 deletions

View file

@ -39,6 +39,7 @@ const AlexaInterface_js_1 = require("../compat/AlexaInterface.js");
const AlexaStatusMessage_js_1 = require("../compat/AlexaStatusMessage.js");
const enums_js_1 = require("../compat/enums.js");
const build_js_1 = require("../messages/build.js");
const StateBuilder_js_1 = require("../messages/StateBuilder.js");
const Alexa_js_1 = require("../registry/interfaces/Alexa.js");
const EndpointHealth_js_1 = require("../registry/interfaces/EndpointHealth.js");
const SceneController_js_1 = require("../registry/interfaces/SceneController.js");
@ -218,7 +219,31 @@ class Device extends events_1.EventEmitter {
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}`);
const topic = topics.event(this.rootTopic);
return this.publish(topics.event(this.rootTopic), message);
}
/**
* Say that the state of the device changed, without a directive that asked for it:
*
* door.changeReport("PHYSICAL_INTERACTION", (s) => s.set(contact, "detectionState", "DETECTED"));
*
* fill sets what changed; after unchanged() it sets what did not. The rest of the context is the state of
* device.state(). The report goes to <root>/changeReport, which Alex2MQTT posts to Alexa; Alexa takes it for a
* capability declared with proactivelyReported. Resolves with what became of the publish and does not reject.
*
* Throws a MessageError when fill sets no property that changed, and a SchemaError for a value Alexa would not take.
*/
changeReport(cause, fill) {
const { endpointId } = this;
const own = new StateBuilder_js_1.StateBuilder({ target: "change" });
fill(own);
const whole = new StateBuilder_js_1.StateBuilder();
this.stateProvider?.(whole);
const context = [...whole.context.filter((property) => !own.context.some((other) => (0, build_js_1.sameProperty)(property, other))), ...own.context];
const message = (0, build_js_1.changeReport)({ endpointId, cause, changed: own.change, context });
return this.publish(topics.changeReport(this.rootTopic), message);
}
// A message nobody asked for: a failed publish is reported to the bridge and is the result
publish(topic, message) {
const unpublished = new Error(`nothing was published to ${topic}: the device is on no bridge, register it with addDevice() or registerDevice()`);
const published = this.publisher
? this.publisher.publish(topic, message)

View file

@ -134,6 +134,19 @@ declare class Device extends EventEmitter {
instance?: string;
messageId?: string;
}): Promise<PublishResult>;
/**
* Say that the state of the device changed, without a directive that asked for it:
*
* door.changeReport("PHYSICAL_INTERACTION", (s) => s.set(contact, "detectionState", "DETECTED"));
*
* fill sets what changed; after unchanged() it sets what did not. The rest of the context is the state of
* device.state(). The report goes to <root>/changeReport, which Alex2MQTT posts to Alexa; Alexa takes it for a
* capability declared with proactivelyReported. Resolves with what became of the publish and does not reject.
*
* Throws a MessageError when fill sets no property that changed, and a SchemaError for a value Alexa would not take.
*/
changeReport(cause: ChangeCause, fill: Fill): Promise<PublishResult>;
private publish;
/**
* How the device reports its state, every retrievable property of it:
*

View file

@ -3,7 +3,8 @@ import { AlexaErrorResponse } from "../compat/AlexaErrorResponse.js";
import { AlexaInterface } from "../compat/AlexaInterface.js";
import { AlexaStatusMessage } from "../compat/AlexaStatusMessage.js";
import { DisplayCategory } from "../compat/enums.js";
import { MessageError, proactiveEvent, sceneEvent } from "../messages/build.js";
import { changeReport, MessageError, proactiveEvent, sameProperty, sceneEvent } from "../messages/build.js";
import { StateBuilder } from "../messages/StateBuilder.js";
import { Alexa } from "../registry/interfaces/Alexa.js";
import { EndpointHealth } from "../registry/interfaces/EndpointHealth.js";
import { SceneController } from "../registry/interfaces/SceneController.js";
@ -183,7 +184,31 @@ class Device extends EventEmitter {
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}`);
const topic = topics.event(this.rootTopic);
return this.publish(topics.event(this.rootTopic), message);
}
/**
* Say that the state of the device changed, without a directive that asked for it:
*
* door.changeReport("PHYSICAL_INTERACTION", (s) => s.set(contact, "detectionState", "DETECTED"));
*
* fill sets what changed; after unchanged() it sets what did not. The rest of the context is the state of
* device.state(). The report goes to <root>/changeReport, which Alex2MQTT posts to Alexa; Alexa takes it for a
* capability declared with proactivelyReported. Resolves with what became of the publish and does not reject.
*
* Throws a MessageError when fill sets no property that changed, and a SchemaError for a value Alexa would not take.
*/
changeReport(cause, fill) {
const { endpointId } = this;
const own = new StateBuilder({ target: "change" });
fill(own);
const whole = new StateBuilder();
this.stateProvider?.(whole);
const context = [...whole.context.filter((property) => !own.context.some((other) => sameProperty(property, other))), ...own.context];
const message = changeReport({ endpointId, cause, changed: own.change, context });
return this.publish(topics.changeReport(this.rootTopic), message);
}
// A message nobody asked for: a failed publish is reported to the bridge and is the result
publish(topic, message) {
const unpublished = new Error(`nothing was published to ${topic}: the device is on no bridge, register it with addDevice() or registerDevice()`);
const published = this.publisher
? this.publisher.publish(topic, message)

View file

@ -134,6 +134,19 @@ declare class Device extends EventEmitter {
instance?: string;
messageId?: string;
}): Promise<PublishResult>;
/**
* Say that the state of the device changed, without a directive that asked for it:
*
* door.changeReport("PHYSICAL_INTERACTION", (s) => s.set(contact, "detectionState", "DETECTED"));
*
* fill sets what changed; after unchanged() it sets what did not. The rest of the context is the state of
* device.state(). The report goes to <root>/changeReport, which Alex2MQTT posts to Alexa; Alexa takes it for a
* capability declared with proactivelyReported. Resolves with what became of the publish and does not reject.
*
* Throws a MessageError when fill sets no property that changed, and a SchemaError for a value Alexa would not take.
*/
changeReport(cause: ChangeCause, fill: Fill): Promise<PublishResult>;
private publish;
/**
* How the device reports its state, every retrievable property of it:
*