/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Immutable, ServerAddress } from "@matter/general"; import { CommissionableDevice, OperationalDevice, PeerAddress, SessionIntervals, SupportedTransportsSchema, } from "@matter/protocol"; import { DeviceTypeId, VendorId } from "@matter/types"; import type { CommissioningClient } from "./CommissioningClient.js"; /** * Device descriptor used by lower-level components. */ export type RemoteDescriptor = Partial; export namespace RemoteDescriptor { /** * The "long form" descriptor used by higher-level components. */ export type Long = CommissioningClient.State; /** * The subset of device identifiers that matches canonically for identity purposes. */ export interface Identifier { readonly peerAddress?: Readonly; readonly deviceIdentifier?: string; } export function is(subject: Identifier, object: Identifier) { if (object.peerAddress !== undefined && subject.peerAddress !== undefined) { return PeerAddress.is(subject.peerAddress, object.peerAddress); } // An empty deviceIdentifier means the scanner could not determine the device's identity // (e.g. macOS CoreBluetooth hides real BLE addresses). Treat it the same as absent — // an empty string must not be used to match unrelated devices. if (object.deviceIdentifier !== undefined && object.deviceIdentifier !== "") { return subject.deviceIdentifier === object.deviceIdentifier; } return false; } export function fromLongForm(long: Immutable): RemoteDescriptor { const result: RemoteDescriptor = {}; const { addresses, discoveredAt, ttl, deviceIdentifier, discriminator, commissioningMode, vendorId, productId, deviceType, deviceName, rotatingIdentifier, pairingHint, pairingInstructions, sessionParameters, tcpSupport, longIdleTimeOperatingMode, } = long; if (discoveredAt !== undefined) { result.discoveredAt = discoveredAt; } if (ttl !== undefined) { result.ttl = ttl; } if (deviceIdentifier !== undefined) { result.deviceIdentifier = deviceIdentifier; } if (vendorId !== undefined) { if (productId !== undefined) { result.VP = `${vendorId}+${productId}`; } else { result.VP = `${vendorId}`; } } if (deviceType !== undefined) { result.DT = deviceType; } if (deviceName !== undefined) { result.DN = deviceName; } if (rotatingIdentifier !== undefined) { result.RI = rotatingIdentifier; } if (pairingHint !== undefined) { result.PH = pairingHint; if (pairingInstructions !== undefined) { result.PI = pairingInstructions; } } if (sessionParameters !== undefined) { const { idleInterval, activeInterval, activeThreshold } = sessionParameters; if (idleInterval !== undefined) { result.SII = idleInterval; } if (activeInterval !== undefined) { result.SAI = activeInterval; } if (activeThreshold !== undefined) { result.SAT = activeThreshold; } } if (tcpSupport !== undefined) { // Long form persists the wire-format number; short form (DiscoveryData.T) holds the decoded bitmap. result.T = SupportedTransportsSchema.decode(tcpSupport); } if (longIdleTimeOperatingMode !== undefined) { result.ICD = longIdleTimeOperatingMode ? 1 : 0; } const isOperational = long.peerAddress !== undefined; if (isOperational) { if (addresses !== undefined) { result.addresses = addresses?.filter(address => ServerAddress.isIp(address)).map(ServerAddress); } } else { if (addresses !== undefined) { result.addresses = addresses.map(address => ({ ...address })).map(ServerAddress); } if (discriminator !== undefined) { (result as CommissionableDevice).D = discriminator; } if (commissioningMode !== undefined) { (result as CommissionableDevice).CM = commissioningMode; } } return result; } export function toLongForm(descriptor: RemoteDescriptor | undefined, long: Long = {}) { if (!descriptor) { return long; } const { addresses, discoveredAt, ttl, deviceIdentifier, VP, DT, DN, RI, PH, PI, SII, SAI, SAT, T, ICD } = descriptor; if (discoveredAt !== undefined) { long.discoveredAt = discoveredAt; } if (ttl !== undefined) { long.ttl = ttl; } if (addresses?.length) { long.addresses = addresses; } if (deviceIdentifier !== undefined) { long.deviceIdentifier = deviceIdentifier; } if (VP !== undefined) { const [vendor, product] = VP.split("+").map(part => Number.parseInt(part, 10)); long.vendorId = Number.isFinite(vendor) ? VendorId(vendor, false) : undefined; long.productId = Number.isFinite(product) ? product : undefined; } // DNS-SD caps SII/SAI at 1 hour, so a value at the cap may be a clamped advertisement of a larger // CASE-negotiated interval. Don't let it lower a higher session-derived value already on record. if (SII !== undefined && !isCappedBelow(SII, long.sessionParameters?.idleInterval)) { (long.sessionParameters ??= {}).idleInterval = SII; } if (SAI !== undefined && !isCappedBelow(SAI, long.sessionParameters?.activeInterval)) { (long.sessionParameters ??= {}).activeInterval = SAI; } if (SAT !== undefined) { (long.sessionParameters ??= {}).activeThreshold = SAT; } long.deviceType = DT === undefined ? undefined : DeviceTypeId(DT, false); long.deviceName = DN; long.rotatingIdentifier = RI; long.pairingHint = PH; long.pairingInstructions = PI; long.tcpSupport = T === undefined ? undefined : SupportedTransportsSchema.encode(T); long.longIdleTimeOperatingMode = ICD === undefined ? undefined : ICD === 1; if ("D" in descriptor) { long.discriminator = descriptor.D; } if ("CM" in descriptor) { long.commissioningMode = descriptor.CM; } return long; } } /** True when `advertised` is at the DNS-SD 1-hour cap and `current` already exceeds it. */ function isCappedBelow(advertised: number, current: number | undefined) { return current !== undefined && advertised === SessionIntervals.maxAdvertisedInterval && current > advertised; }