registry: SceneController events, DoorbellEventSource, SimpleEventSource, TimeHoldController, InventoryLevelSensor, WakeOnLANController
Six descriptors written from their pages replace the last stubs of tiers 1 and 2. A scene answers Activate and Deactivate through ctx.respond() with ActivationStarted and DeactivationStarted, the time and the cause filled in. device.raise(descriptor, name, payload) publishes DoorbellPress and the Event of a button on <root>/event with the endpoint and a new messageId; it throws a MessageError for an interface or instance the device did not declare, an event that answers a directive, a payload that does not fit and a message over 16000 bytes. TurnOn of a device with WakeOnLANController is deferred without the warning. On the wire: a doorbell has no properties object and proactivelyReported on the capability; SimpleEventSource is version 1.0, InventoryLevelSensor and WakeOnLANController version 3 (1.5.2: 1). A scene declared without options is announced as before. Alex2MQTT has no topic yet for the WakeUp event. 24 examples of the six pages are saved as fixtures. 267 tests pass, 241 before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
d7dfc5b122
commit
3a07c861b1
116 changed files with 3136 additions and 166 deletions
|
|
@ -4,18 +4,19 @@ import { AlexaInterface } from "../compat/AlexaInterface.js";
|
|||
import { AlexaStatusMessage } from "../compat/AlexaStatusMessage.js";
|
||||
import { DisplayCategory } from "../compat/enums.js";
|
||||
import type { AlexaInterfaceType } from "../compat/enums.js";
|
||||
import { sceneEvent } from "../messages/build.js";
|
||||
import { MessageError, proactiveEvent, sceneEvent } from "../messages/build.js";
|
||||
import type { ChangeCause } from "../messages/types.js";
|
||||
import type { DirectiveHandler, Fill } from "../dispatcher.js";
|
||||
import type { DisplayCategoryName } from "../registry/catalog.js";
|
||||
import { Alexa } from "../registry/interfaces/Alexa.js";
|
||||
import { EndpointHealth } from "../registry/interfaces/EndpointHealth.js";
|
||||
import { SceneController } from "../registry/interfaces/SceneController.js";
|
||||
import { SchemaError } from "../registry/schema.js";
|
||||
import { DeclarationError } from "../registry/types.js";
|
||||
import type { Directives, EndpointView, InterfaceDescriptor, Label, Properties } from "../registry/types.js";
|
||||
import * as topics from "../topics.js";
|
||||
import { send } from "../transport.js";
|
||||
import type { Publisher } from "../transport.js";
|
||||
import type { Publisher, PublishResult } from "../transport.js";
|
||||
import { Capability, commonOptions } from "./Capability.js";
|
||||
import type { AnyCapability, CapabilityJson, Declaration } from "./Capability.js";
|
||||
import { checkCapability, checkCapabilityCount, checkEndpoint } from "./validate.js";
|
||||
|
|
@ -176,6 +177,71 @@ class Device extends EventEmitter {
|
|||
const build = () => sceneEvent({ endpointId: this.endpointId, correlationToken, activated, cause });
|
||||
return send(this.publisher, answer(this.rootTopic, this.endpointId), build, this.onPublishError);
|
||||
}
|
||||
/**
|
||||
* Say that something happened on the device, which nobody asked for:
|
||||
*
|
||||
* door.raise(DoorbellEventSource, "DoorbellPress");
|
||||
* remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }, { instance: topButton.instance });
|
||||
*
|
||||
* The event goes to <root>/event, which Alex2MQTT posts to Alexa: up to 30 events a minute for a root. The
|
||||
* payload is checked by the descriptor, which also sets the time of the event to now and its cause to the usual
|
||||
* one when the payload has none. Resolves with what became of the publish and does not reject.
|
||||
*
|
||||
* Throws a MessageError for an event that would not arrive: of an interface or an instance the device did not
|
||||
* declare, not an event of the interface, with a payload that does not fit, or too long for Alex2MQTT.
|
||||
*/
|
||||
raise(
|
||||
descriptor: InterfaceDescriptor<any, any, any, boolean>,
|
||||
name: string,
|
||||
payload: Record<string, unknown> = {},
|
||||
options: { instance?: string; messageId?: string } = {}
|
||||
): Promise<PublishResult> {
|
||||
const { endpointId } = this;
|
||||
const { namespace } = descriptor;
|
||||
const { instance = "", messageId } = options;
|
||||
const refuse = (problem: string): never => {
|
||||
throw new MessageError(endpointId, `${namespace}.${name} was not raised: ${problem}`);
|
||||
};
|
||||
|
||||
const capability = this.capability(namespace, instance);
|
||||
if (!capability) {
|
||||
const declared = instance ? `the instance ${JSON.stringify(instance)} of ${namespace}` : namespace;
|
||||
return refuse(`the device did not declare ${declared}. Declare it with add() first${descriptor.instanced && !instance ? ", and pass the instance that raises the event" : ""}`);
|
||||
}
|
||||
const events = capability.descriptor.events ?? {};
|
||||
const event = events[name];
|
||||
if (!event) return refuse(`it is not an event of the interface, which has ${Object.keys(events).join(", ") || "none"}`);
|
||||
if (event.topic !== "proactive") return refuse("it answers a directive. Send it with respond() in the handler of the directive");
|
||||
|
||||
let checked: Record<string, unknown>;
|
||||
try {
|
||||
checked = event.payload.parse(payload, "payload");
|
||||
} catch (err) {
|
||||
if (err instanceof SchemaError) return refuse(err.message);
|
||||
throw err;
|
||||
}
|
||||
const message = proactiveEvent({
|
||||
endpointId,
|
||||
messageId,
|
||||
namespace: event.namespace ?? namespace,
|
||||
name,
|
||||
instance,
|
||||
payloadVersion: event.payloadVersion ?? capability.descriptor.version,
|
||||
payload: checked,
|
||||
});
|
||||
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);
|
||||
const unpublished = new Error(`nothing was published to ${topic}: the device is on no bridge, register it with addDevice() or registerDevice()`);
|
||||
const published: Promise<PublishResult> = this.publisher
|
||||
? this.publisher.publish(topic, message)
|
||||
: Promise.resolve({ ok: false, topic, error: unpublished });
|
||||
return published.then((result) => {
|
||||
if (!result.ok) this.onPublishError?.(result.error);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* How the device reports its state, every retrievable property of it:
|
||||
*
|
||||
|
|
@ -354,7 +420,7 @@ class Device extends EventEmitter {
|
|||
const has = (namespace: string): boolean => declared.some((capability) => capability.namespace === namespace);
|
||||
const added: AnyCapability[] = [];
|
||||
// alexa-scenecontroller.html, "Discovery": a scene is not a physical device and has no Alexa.EndpointHealth
|
||||
if (this.endpointHealth && !has(EndpointHealth.namespace) && !has("Alexa.SceneController")) {
|
||||
if (this.endpointHealth && !has(EndpointHealth.namespace) && !has(SceneController.namespace)) {
|
||||
added.push(new Capability(EndpointHealth, { options: {} }));
|
||||
}
|
||||
if (this.alexaInterface && !has(Alexa.namespace)) added.push(new Capability(Alexa, { options: {} }));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue