/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import type { ElementEvent, Events } from "#behavior/Events.js"; import type { ClientNode } from "#node/ClientNode.js"; import { ChangeNotificationService } from "#node/integration/ChangeNotificationService.js"; import { Diagnostic, isObject, Logger, Timestamp } from "@matter/general"; import { EventModel, MatterModel } from "@matter/model"; import type { ReadResult, Val } from "@matter/protocol"; import type { ClusterId, EventId } from "@matter/types"; import type { ClientStructure } from "./ClientStructure.js"; const logger = Logger.get("ClientEventEmitter"); /** * Event handler for Matter events transmitted by a peer. * * TODO - set priority on context when split for server vs. client */ export interface ClientEventEmitter { (event: ReadResult.EventValue): Promise; } /** * Cache of MatterModel + cluster + event ID -> event name. */ const nameCache = new WeakMap< MatterModel, Record<`${ClusterId}-${EventId}`, undefined | { cluster: string; event: string }> >(); /** * We warn for each cluster or cluster+event that we don't support. */ const warnedForUnknown = new Set(); export function ClientEventEmitter(node: ClientNode, structure: ClientStructure) { const changes = node.env.get(ChangeNotificationService); return emitClientEvent; async function emitClientEvent(occurrence: ReadResult.EventValue) { const names = getNames(node.matter, occurrence); if (!names) { return; } const target = getTarget(node, occurrence, names.cluster, names.event); if (!target) { return; } await node.act(agent => { // Current ActionContext is not writable, could skip act() but meh, see TODO above //agent.context.priority = occurrence.priority; return target.event.emit(occurrence.value, agent.context); }); const behavior = target.endpoint.behaviors.supported[names.cluster]; if (behavior) { const { number, timestamp, priority, value } = occurrence; const timestampKind = timestampKindOf(occurrence); changes.broadcastEvent(target.endpoint, behavior, target.event.schema as EventModel, { number, timestamp: timestamp as Timestamp, timestampKind, priority, payload: value as Val.Struct | undefined, }); } } function getTarget(node: ClientNode, occurrence: ReadResult.EventValue, clusterName: string, eventName: string) { const { value, path: { endpointId }, } = occurrence; const endpoint = structure.endpointFor(endpointId); if (endpoint === undefined) { logger.warn(`Received event for unknown endpoint #${endpointId} on ${node}`); return; } const events = (endpoint.events as Events.Generic)[clusterName]; if (events === undefined) { logger.warn(`Received event ${eventName} for unsupported cluster ${clusterName} on ${endpoint}`); return; } const event = events[eventName]; if (event) { logger.debug( "Received event", Diagnostic.strong(`${clusterName}.${eventName}`), " on ", Diagnostic.strong(endpoint.toString()), Diagnostic.weak(isObject(value) ? Diagnostic.dict(value) : value), ); return { endpoint, event }; } logger.warn(`Received unknown event ${clusterName}.${eventName} on ${endpoint}`); } } /** * Identify which of the four timestamp variants Matter Core ยง10.7 defines the peer sent. Absent all of them the * occurrence carries {@link ReadResult.EventValue.timestamp} 0, which is a Posix epoch of no useful precision. */ function timestampKindOf(occurrence: ReadResult.EventValue): ChangeNotificationService.TimestampKind { const { systemTimestamp, deltaEpochTimestamp, deltaSystemTimestamp } = occurrence; if (systemTimestamp !== undefined) { return "system"; } if (deltaEpochTimestamp !== undefined) { return "epoch-delta"; } if (deltaSystemTimestamp !== undefined) { return "system-delta"; } return "epoch"; } function getNames(matter: MatterModel, { path: { clusterId, eventId } }: ReadResult.EventValue) { let matterCache = nameCache.get(matter); if (matterCache === undefined) { matterCache = {}; nameCache.set(matter, matterCache); } const key = `${clusterId}-${eventId}` as const; if (key in matterCache) { return matterCache[key]; } const cluster = matter.clusters(clusterId); if (cluster === undefined) { if (!warnedForUnknown.has(clusterId)) { logger.notice(`Ignoring events for unknown cluster #${clusterId}`); warnedForUnknown.add(clusterId); matterCache[key] = undefined; } return; } const event = cluster.events(eventId); if (event === undefined) { if (!warnedForUnknown.has(key)) { logger.notice(`Ignoring unknown event #${eventId} for ${cluster.name} cluster`); warnedForUnknown.add(key); matterCache[key] = undefined; } return; } return (matterCache[key] = { cluster: cluster.propertyName, event: event.propertyName }); }