/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { IndexBehavior } from "#behavior/system/index/IndexBehavior.js"; import { Endpoint } from "#endpoint/Endpoint.js"; import { EndpointLifecycle } from "#endpoint/properties/EndpointLifecycle.js"; import { ImplementationError, isDeepEqual, Logger } from "@matter/general"; import { EndpointComposition, Matter } from "@matter/model"; import { ClusterId, DeviceTypeId, EndpointNumber, Semtag } from "@matter/types"; import { Descriptor } from "@matter/types/clusters/descriptor"; import { DescriptorBehavior } from "./DescriptorBehavior.js"; const logger = Logger.get("DescriptorServer"); /** * This is the default server implementation of DescriptorBehavior. */ export class DescriptorServer extends DescriptorBehavior { static override dependencies = [IndexBehavior]; override async initialize() { // We update PartsList differently if there's an index if (this.endpoint.behaviors.has(IndexBehavior)) { // Note - do not use lock here because this reactor triggers frequently so it pollutes the logs. Instead // lock manually as necessary this.reactTo(this.agent.get(IndexBehavior).events.change, this.#updatePartsList); this.reactTo(this.events.deviceTypeList$Changed, this.#updatePartsList, { offline: true }); } else if (this.endpoint.hasParts) { for (const endpoint of this.endpoint.parts) { this.#monitorDestruction(endpoint); } } await this.#updatePartsList(); // Handle lifecycle changes this.reactTo(this.endpoint.lifecycle.changed, this.#updateDescriptor); this.state.serverList = this.#serverList; this.state.clientList = this.#clientList; const enabledClientClusters = Object.keys(this.endpoint.type.clientClusters ?? {}); if (enabledClientClusters.length) { logger.debug( `Enabled client cluster(s) [${enabledClientClusters.join(", ")}] for endpoint ${this.endpoint.id} (device type ${this.endpoint.type.name})`, ); } // Initialize DeviceTypeList this.#initializeDeviceTypeList(); } /** Initialize device type list when it is not already initialized. */ #initializeDeviceTypeList() { if (!this.state.deviceTypeList.length) { const partType = this.endpoint.type; this.state.deviceTypeList = [ { deviceType: partType.deviceType, revision: partType.deviceRevision, }, ]; // For complete semantics it would be better to include all inherited device types. However there is // typical spec-level confusion that makes this of questionable practical utility so omitting for now // for ( // let base = Matter.get(DeviceTypeModel, partType.deviceType)?.base; // base; // { base } = base // ) { // if (!(base instanceof DeviceTypeModel) || base.id === undefined) { // continue; // } // this.state.deviceTypeList.push({ // deviceType: DeviceTypeId(base.id), // revision: base.revision, // }); // } } } /** * Extend device type metadata. This is a shortcut for deduped insert into the deviceTypeList cluster attribute. A * device type that is already listed keeps its listed revision. * * @param deviceTypes an array of objects or named device types as defined in {@link Matter} */ addDeviceTypes(...deviceTypes: (DescriptorServer.DeviceType | string)[]) { this.#initializeDeviceTypeList(); // Initialize if not already done const list = this.state.deviceTypeList; nextInput: for (let newDeviceType of deviceTypes) { if (typeof newDeviceType === "string") { const dt = Matter.deviceTypes(newDeviceType); if (dt === undefined) { throw new ImplementationError(`Device type ${newDeviceType} not found`); } newDeviceType = { deviceType: DeviceTypeId(dt.id), revision: dt.revision }; } for (const existingDeviceType of list) { if (existingDeviceType.deviceType === newDeviceType.deviceType) { continue nextInput; } } list.push(newDeviceType); } } /** * Add semantic tags. This is a shortcut for deduped insert into the tagList cluster attribute. A tag with the * same manufacturer code, namespace and tag as a listed one is not added again; a non-null label given with it * replaces the listed label, a `null` or missing label keeps it. * * You must enable the "TagList" feature to use this method. * * @see {@link MatterSpecification.v161.Core} § 9.5.6.5 */ addTags(...tags: Semtag[]) { // TODO - should automatically enable the feature if it's not enabled if (!this.features.tagList) { throw new ImplementationError('You must enable the descriptor "TagList" feature to set tags'); } const list = (this.state as unknown as { tagList: Semtag[] }).tagList; nextInput: for (const newTag of tags) { for (const existingTag of list) { if ( existingTag.mfgCode === newTag.mfgCode && existingTag.namespaceId === newTag.namespaceId && existingTag.tag === newTag.tag ) { if (newTag.label !== null && newTag.label !== undefined && existingTag.label !== newTag.label) { existingTag.label = newTag.label; } continue nextInput; } } list.push(newTag); } } /** * Check for presence of a device type. */ hasDeviceType(type: DeviceTypeId) { return this.state.deviceTypeList.findIndex(dt => dt.deviceType === type) !== -1; } /** * Process a structure change event and trigger state updates if necessary. */ async #updateDescriptor(type: EndpointLifecycle.Change, endpoint: Endpoint) { switch (type) { case EndpointLifecycle.Change.Ready: if (!this.endpoint.parts.has(endpoint)) { return; } await this.#updatePartsList(); if (!this.endpoint.behaviors.has(IndexBehavior)) { this.#monitorDestruction(endpoint); } break; case EndpointLifecycle.Change.ServersChanged: if (endpoint !== this.endpoint) { return; } await this.context.transaction.addResources(this); await this.context.transaction.begin(); this.state.serverList = this.#serverList; break; /* * No producer fires ClientsChanged today; endpoint.type.clientClusters is static post-construction. case EndpointLifecycle.Change.ClientsChanged: if (endpoint !== this.endpoint) { return; } await this.context.transaction.addResources(this); await this.context.transaction.begin(); this.state.clientList = this.#clientList; break; */ case EndpointLifecycle.Change.Destroying: if (endpoint !== this.endpoint) { return; } await this.stopReacting({ reactor: this.#updatePartsList }); break; } } /** * Monitor endpoint for removal. */ #monitorDestruction(endpoint: Endpoint) { this.reactTo(endpoint.lifecycle.destroyed, this.#updatePartsList, { once: true }); } /** * Update the parts list. */ async #updatePartsList() { // Skip the lock when nothing changed; beginning the transaction during synchronous initialization would // suspend and collide with a sibling behavior's write on the shared transaction if (isDeepEqual(this.state.partsList, this.#currentPartsListNumbers())) { return; } await this.context.transaction.addResources(this); await this.context.transaction.begin(); // Recompute under the lock so the write reflects membership at write time, not at reactor start this.state.partsList = this.#currentPartsListNumbers() as EndpointNumber[]; } #currentPartsListNumbers(): number[] { const endpoint = this.endpoint; let numbers: number[]; if (this.agent.has(IndexBehavior) && this.#composesFullFamily) { const index = this.agent.get(IndexBehavior); numbers = Object.keys(index.partsByNumber).map(n => Number.parseInt(n)); // My endpoint should not appear in its own PartsList const pos = numbers.indexOf(this.endpoint.number); if (pos !== -1) { numbers.splice(pos, 1); } } else if (endpoint.hasParts) { // Direct descendants only, limited to those the node's index admitted numbers = [...endpoint.parts] .filter(part => IndexBehavior.isIndexed(part)) .map(endpoint => endpoint.maybeNumber) .filter((n): n is EndpointNumber => n !== undefined); } else { // No sub-parts numbers = []; } numbers.sort((a, b) => a - b); return numbers; } /** * Whether this endpoint's device type composes its `PartsList` of every descendant rather than of * its own children (Matter Core § 9.2.7). * * {@link IndexBehavior} is how the flat list is obtained, not what decides it is wanted. */ get #composesFullFamily() { const deviceTypes = this.state.deviceTypeList.length ? this.state.deviceTypeList.map(entry => entry.deviceType) : [this.endpoint.type.deviceType]; return deviceTypes.some( deviceType => Matter.deviceTypes(deviceType)?.effectiveComposition === EndpointComposition.FullFamily, ); } /** * Computed current server list. */ get #serverList() { const list = new Array(); for (const type of Object.values(this.endpoint.behaviors.supported)) { const clusterId = (type as { cluster?: { id?: ClusterId } }).cluster?.id; if (clusterId) { list.push(clusterId); } } return list; } /** * Computed current client list from registered client cluster declarations. */ get #clientList() { const list = new Array(); for (const type of Object.values(this.endpoint.type.clientClusters ?? {})) { const clusterId = (type as { cluster?: { id?: ClusterId } }).cluster?.id; if (clusterId) { list.push(clusterId); } } return list; } } export namespace DescriptorServer { export type DeviceType = Descriptor.DeviceType; }