/** * @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, hostname, ttl, deviceIdentifier, discriminator, commissioningMode, vendorId, productId, deviceType, deviceName, rotatingIdentifier, pairingHint, pairingInstructions, advertisedIntervals, tcpSupport, longIdleTimeOperatingMode, } = long; if (discoveredAt !== undefined) { result.discoveredAt = discoveredAt; } if (ttl !== undefined) { result.ttl = ttl; } if (deviceIdentifier !== undefined) { result.deviceIdentifier = deviceIdentifier; } if (hostname !== undefined) { result.hostname = hostname; } 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; } } const intervals = advertisedIntervals && SessionIntervals.advertisable(advertisedIntervals); if (intervals !== undefined) { const { idleInterval, activeInterval, activeThreshold } = intervals; 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, hostname, 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; } // Present-but-undefined is a device whose SRV expired, and must clear the host on record; a // descriptor without the key at all is a caller that knows nothing about the host — half of them // pass a bare `DiscoveryData` — and must leave what discovery found alone if ("hostname" in descriptor) { long.hostname = hostname; } if (VP !== undefined) { const [vendor, product] = VP.split("+").map(part => Number.parseInt(part, 10)); // § 2.5.2 / § 2.5.3 write "unspecified" as 0, which is an identity the device declines to // state rather than one a caller can match against long.vendorId = Number.isFinite(vendor) && vendor !== 0 ? VendorId(vendor, false) : undefined; long.productId = Number.isFinite(product) && product !== 0 ? product : undefined; } long.advertisedIntervals = SessionIntervals.advertisable({ idleInterval: SII, activeInterval: SAI, 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; } }