/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Behavior } from "#behavior/Behavior.js"; import { ClusterBehavior } from "#behavior/cluster/ClusterBehavior.js"; import { ClusterBehaviorType } from "#behavior/cluster/ClusterBehaviorType.js"; import { camelize, InternalError } from "@matter/general"; import { AttributeModel, ClusterModel, ClusterRevision, CommandModel, Conformance, EncodedBitmap, EventModel, FeatureBitmap, FeatureSet, Matter, MatterModel, type ValueModel, } from "@matter/model"; import { AttributeId, ClusterId, CommandId } from "@matter/types"; import { ClientCommandMethod } from "./ClientCommandMethod.js"; // A bitwise shift operand is taken modulo 32, so a wider block would alias IDs onto each other const BIT_BLOCK_SIZE = 32; const discoveredCaches = new Map< ClusterBehaviorType.CommandFactory, WeakMap> >(); const knownCaches = new Map>(); const peerTypes = new WeakSet(); /** * Obtain a {@link ClusterBehavior.Type} for a remote cluster. */ export function PeerBehavior(shape: PeerBehavior.ClusterShape): ClusterBehavior.Type { let type: ClusterBehavior.Type; const { kind } = shape; switch (kind) { case "known": if (peerTypes.has(shape.behavior)) { return shape.behavior; } type = instrumentKnownShape(shape); break; case "discovered": type = instrumentDiscoveredShape(shape); break; default: throw new InternalError(`Unknown cluster shape kind ${kind}`); } peerTypes.add(type); return type; } export namespace PeerBehavior { export type ClusterShape = DiscoveredClusterShape | KnownClusterShape; /** * A cluster shape that we assemble using a combination of Matter standards and metadata discovered by reading from * a peer. */ export interface DiscoveredClusterShape { kind: "discovered"; id: ClusterId; /** * The cluster revision the peer reports. Undefined if the peer did not report {@link ClusterRevision}. */ revision?: number; features?: FeatureBitmap | number; attributes?: AttributeId[]; commands?: CommandId[]; generatedCommands?: CommandId[]; commandFactory?: ClusterBehaviorType.CommandFactory; /** * Model used to resolve standard element names/shapes. Defaults to the global {@link Matter} model; a client * node supplies its own so custom or extended clusters resolve to real names. */ matter?: MatterModel; } /** * A known cluster shape that we instrument as is. */ export interface KnownClusterShape { kind: "known"; behavior: ClusterBehavior.Type; commandFactory?: ClusterBehaviorType.CommandFactory; } } function instrumentDiscoveredShape(shape: PeerBehavior.DiscoveredClusterShape) { const matter = shape.matter ?? Matter; const analysis = DiscoveredShapeAnalysis(shape, matter); const factory = shape.commandFactory ?? ClientCommandMethod; let byModel = discoveredCaches.get(factory); if (!byModel) { discoveredCaches.set(factory, (byModel = new WeakMap())); } let cache = byModel.get(matter); if (!cache) { byModel.set(matter, (cache = {})); } const fingerprint = createFingerprint(analysis); let type = cache[fingerprint]; if (type) { return type; } // Find a base behavior for the standard cluster, if available let baseType: Behavior.Type | undefined; const standardSchema = matter.get(ClusterModel, shape.id); if (standardSchema) { // Create a base behavior from the standard schema baseType = ClusterBehaviorType({ base: ClusterBehavior, schema: standardSchema, }); } type = cache[fingerprint] = generateDiscoveredType(analysis, baseType, factory); return type; } function instrumentKnownShape(shape: PeerBehavior.KnownClusterShape) { const factory = shape.commandFactory ?? ClientCommandMethod; let cache = knownCaches.get(factory); if (!cache) { knownCaches.set(factory, (cache = new WeakMap())); } let type = cache.get(shape.behavior); if (type) { return type; } const base = shape.behavior; type = ClusterBehaviorType({ base, namespace: base.cluster, name: `${base.schema.name}Client`, forClient: true, commandFactory: factory, }); cache.set(shape.behavior, type); return type; } function generateDiscoveredType( analysis: DiscoveredShapeAnalysis, baseType?: Behavior.Type, commandFactory: ClusterBehaviorType.CommandFactory = ClientCommandMethod, ): ClusterBehavior.Type { let { schema } = analysis; let isExtended = !baseType; if (baseType) { // Ensure the input type is a ClusterBehavior if (!ClusterBehavior.is(baseType)) { throw new InternalError(`Base for cluster ${analysis.schema.name} is not a ClusterBehavior`); } } else { baseType = ClusterBehavior; } const { attrSupportOverrides, extraAttrs, commandSupportOverrides, extraCommands } = analysis; // Identify known features the device supports let supportedFeatures = analysis.shape.features; if (typeof supportedFeatures === "number") { // Decode numeric feature bitmap using the schema's featureMap const featureMap = schema.featureMap; const decoded: FeatureBitmap = {}; for (const child of featureMap.children) { const bitValue = child.constraint.value; if (typeof bitValue === "number") { const key = camelize(child.title ?? child.name); decoded[key] = !!(supportedFeatures & (1 << bitValue)); } } supportedFeatures = decoded; } if (supportedFeatures === undefined) { supportedFeatures = {}; } // If there are features supported, set them on the schema const featureNames = Object.entries(supportedFeatures) .filter(([, v]) => v) .map(([k]) => k); if (featureNames.length) { extendSchema(); // Map user-facing feature names to model feature codes for FeatureSet const featureSet = new FeatureSet(); for (const child of schema.featureMap.children) { const key = camelize(child.title ?? child.name); if (featureNames.includes(key)) { featureSet.add(child.name); } } schema.supportedFeatures = featureSet; } // If the schema does not match what the device actually returned, further augment the schema // with unknown attributes and/or commands const { revisionOverride } = analysis; if ( revisionOverride !== undefined || extraAttrs.size || extraCommands.size || attrSupportOverrides.size || commandSupportOverrides.size ) { extendSchema(); if (revisionOverride !== undefined) { const base = schema.attributes(ClusterRevision.id); if (base === undefined) { throw new InternalError(`Cluster ${schema.name} states no ClusterRevision to override`); } schema.children.push(base.extend({ default: revisionOverride })); } if (attrSupportOverrides.size) { for (const [attr, isSupported] of attrSupportOverrides.entries()) { schema.children.push(attr.extend(supportOverrideOf(attr, isSupported, schema))); } } for (const id of extraAttrs) { const name = createUnknownName("attr", id); schema.children.push(new AttributeModel({ id, name, type: "any" })); } if (commandSupportOverrides.size) { for (const [command, isSupported] of commandSupportOverrides.entries()) { schema.children.push(command.extend(supportOverrideOf(command, isSupported, schema))); } } for (const id of extraCommands) { const name = createUnknownName("command", id); schema.children.push(new CommandModel({ id, name, type: "any" })); } } // Specialize for the specific cluster and schema — namespace is constructed from schema by ClusterBehaviorType return ClusterBehaviorType({ base: baseType, schema, name: `${schema.name}Client`, forClient: true, commandFactory, }); function extendSchema() { if (isExtended) { return; } schema = schema.extend(); isExtended = true; } } /** * Create a compact string that uniquely identifies a shape for matching purposes. */ function createFingerprint(analysis: DiscoveredShapeAnalysis) { const fingerprint = [analysis.shape.id] as (number | string | bigint)[]; if (analysis.revisionOverride !== undefined) { fingerprint.push("r", analysis.revisionOverride); } if (analysis.featureBitmap) { fingerprint.push("f", analysis.featureBitmap); } if (analysis.attrSupportOverrides.size) { addSupportFingerprints("a", analysis.attrSupportOverrides); } if (analysis.extraAttrs.size) { fingerprint.push("a", createElementFingerprint(analysis.extraAttrs)); } if (analysis.commandSupportOverrides.size) { addSupportFingerprints("c", analysis.commandSupportOverrides); } if (analysis.extraCommands.size) { fingerprint.push("c", createElementFingerprint(analysis.extraCommands)); } return fingerprint.join(";"); /** * Create a fingerprint for a specific type of ACE element. * * For elements we create a series of bitmaps, one for each range of BIT_BLOCK_SIZE unique integers with an ID * present. Note that these bitmaps may not be consecutive due to gaps in IDs. We then serialize all bitmaps * present as ":" and concatenate to create a unique fingerprint. * * The goal is to efficiently create a compact unique identifier. */ function createElementFingerprint(ids: Iterable) { const blocks = {} as Record; for (const id of ids) { const block = Math.floor(id / BIT_BLOCK_SIZE); blocks[block] = (blocks[block] ?? 0) | (1 << (id % BIT_BLOCK_SIZE)); } return Object.entries(blocks) .sort(([a], [b]) => a.localeCompare(b)) .map(([block, map]) => (block ? `${block}:${map >>> 0}` : `${map >>> 0}`)) .join(","); } /** * Add fingerprints for overrides of element support. * * This adds an "x-" component for unsupported elements and "x+" for supported elements. */ function addSupportFingerprints(prefix: string, elements: Map) { let supported: Array | undefined; let unsupported: Array | undefined; for (const [{ id }, isSupported] of elements) { if (id === undefined) { continue; } if (isSupported) { if (supported) { supported.push(id); } else { supported = [id]; } } else { if (unsupported) { unsupported.push(id); } else { unsupported = [id]; } } } if (supported) { fingerprint.push(`${prefix}+`, createElementFingerprint(supported)); } if (unsupported) { fingerprint.push(`${prefix}-`, createElementFingerprint(unsupported)); } } } /** * The properties that record an element's support on the peer. * * An element the peer implements although its conformance deprecates it becomes optional, so the client exposes it as * it does any optional element the peer supports. */ function supportOverrideOf(element: AttributeModel | CommandModel, isSupported: boolean, cluster: ClusterModel) { const conformance = element.effectiveConformance; if ( isSupported && conformance.applicabilityFor(cluster) === Conformance.Applicability.None && conformance.applicabilityFor(cluster, { deprecatedIsOptional: true }) !== Conformance.Applicability.None ) { return { operationalIsSupported: true, conformance: Conformance.Flag.Optional }; } return { operationalIsSupported: isSupported }; } function createUnknownName(prefix: string, id: number) { return `${prefix}$${id.toString(16)}`; } interface DiscoveredShapeAnalysis { schema: ClusterModel & { id: ClusterId }; featureBitmap: number | bigint; shape: PeerBehavior.DiscoveredClusterShape; /** * The revision to record on the schema, present only where it differs from the revision the schema already states. */ revisionOverride?: number; attrSupportOverrides: Map; extraAttrs: Set; commandSupportOverrides: Map; extraCommands: Set; } /** * Analyze a discovered cluster shape to determine how we should override the behavior and schema. */ function DiscoveredShapeAnalysis( shape: PeerBehavior.DiscoveredClusterShape, matter: MatterModel = Matter, ): DiscoveredShapeAnalysis { const standardCluster = matter.clusters(shape.id); const schema = standardCluster ?? new ClusterModel({ id: shape.id, name: createUnknownName("Cluster", shape.id) }); // The specification constrains ClusterRevision to "min 1", so anything else is not a revision the peer holds const { revision } = shape; const revisionOverride = typeof revision === "number" && Number.isInteger(revision) && revision >= 1 && revision !== schema.revision ? revision : undefined; let featureBitmap: bigint | number; if (typeof shape.features === "number") { featureBitmap = shape.features; } else { featureBitmap = EncodedBitmap(schema.featureMap, shape.features ?? {}); } const attrSupportOverrides = new Map(); const extraAttrs = new Set(shape.attributes ?? []); for (const attr of schema.attributes) { maybeOverrideSupport(standardCluster, attr, extraAttrs, attrSupportOverrides); extraAttrs.delete(attr.id as AttributeId); } const commandSupportOverrides = new Map(); const extraCommands = new Set(shape.commands ?? []); for (const command of schema.commands) { maybeOverrideSupport(standardCluster, command, extraCommands, commandSupportOverrides); extraCommands.delete(command.id as CommandId); } return { schema: schema as ClusterModel & { id: ClusterId }, featureBitmap, shape, revisionOverride, attrSupportOverrides, extraAttrs, commandSupportOverrides, extraCommands, }; } /** * Determine whether we need to override {@link ClusterModel#operationalIsSupported}. * * We do this if: * * * The element may be present according to the standard but is not implemented on the peer, or * * * the element is optional according to the standard and is implemented on the peer */ function maybeOverrideSupport( standardCluster: ClusterModel | undefined, element: T, supported: Set | true, overrides: Map, ) { if (!standardCluster) { return; } // Global attributes are injected into cluster members independent of conformance (see Scope.injectGlobalAttributes), // so a support override would push a second, undeconflictable definition. Their support is never in question. if (AttributeModel.isGlobal(element)) { return; } const isSupported = supported === true || supported.has(element.id); const applicability = element.effectiveConformance.applicabilityFor(standardCluster); if (!isSupported) { if (applicability === Conformance.Applicability.Mandatory) { // We don't really pay attention to "unsupported mandatory attributes" but mark them anyway overrides.set(element, false); } } else { if (applicability !== Conformance.Applicability.Mandatory) { // Indicate support for optional feature overrides.set(element, true); } } }