UNPKG

matterbridge

Version:
864 lines • 149 kB
/** * This file contains the class MatterbridgeEndpoint that extends the Endpoint class from the Matter.js library. * * @file matterbridgeEndpoint.ts * @author Luca Liguori * @created 2024-10-01 * @version 2.1.1 * @license Apache-2.0 * * Copyright 2024, 2025, 2026 Luca Liguori. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // @matter import { Endpoint, Lifecycle, MutableEndpoint, NamedHandler, SupportedBehaviors, UINT16_MAX, UINT32_MAX, VendorId } from '@matter/main'; import { getClusterNameById, MeasurementType } from '@matter/main/types'; // @matter clusters import { Descriptor } from '@matter/main/clusters/descriptor'; import { PowerSource } from '@matter/main/clusters/power-source'; import { BridgedDeviceBasicInformation } from '@matter/main/clusters/bridged-device-basic-information'; import { Identify } from '@matter/main/clusters/identify'; import { OnOff } from '@matter/main/clusters/on-off'; import { LevelControl } from '@matter/main/clusters/level-control'; import { ColorControl } from '@matter/main/clusters/color-control'; import { WindowCovering } from '@matter/main/clusters/window-covering'; import { Thermostat } from '@matter/main/clusters/thermostat'; import { FanControl } from '@matter/main/clusters/fan-control'; import { DoorLock } from '@matter/main/clusters/door-lock'; import { ValveConfigurationAndControl } from '@matter/main/clusters/valve-configuration-and-control'; import { PumpConfigurationAndControl } from '@matter/main/clusters/pump-configuration-and-control'; import { SmokeCoAlarm } from '@matter/main/clusters/smoke-co-alarm'; import { Switch } from '@matter/main/clusters/switch'; import { BooleanStateConfiguration } from '@matter/main/clusters/boolean-state-configuration'; import { PowerTopology } from '@matter/main/clusters/power-topology'; import { ElectricalPowerMeasurement } from '@matter/main/clusters/electrical-power-measurement'; import { ElectricalEnergyMeasurement } from '@matter/main/clusters/electrical-energy-measurement'; import { AirQuality } from '@matter/main/clusters/air-quality'; import { ConcentrationMeasurement } from '@matter/main/clusters/concentration-measurement'; import { OccupancySensing } from '@matter/main/clusters/occupancy-sensing'; import { ThermostatUserInterfaceConfiguration } from '@matter/main/clusters/thermostat-user-interface-configuration'; import { OperationalState } from '@matter/main/clusters/operational-state'; import { DeviceEnergyManagement } from '@matter/main/clusters/device-energy-management'; import { DeviceEnergyManagementMode } from '@matter/main/clusters/device-energy-management-mode'; // @matter behaviors import { DescriptorServer } from '@matter/main/behaviors/descriptor'; import { PowerSourceServer } from '@matter/main/behaviors/power-source'; import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors/bridged-device-basic-information'; import { GroupsServer } from '@matter/main/behaviors/groups'; import { ScenesManagementServer } from '@matter/main/behaviors/scenes-management'; import { PumpConfigurationAndControlServer } from '@matter/main/behaviors/pump-configuration-and-control'; import { SwitchServer } from '@matter/main/behaviors/switch'; import { BooleanStateServer } from '@matter/main/behaviors/boolean-state'; import { PowerTopologyServer } from '@matter/main/behaviors/power-topology'; import { ElectricalPowerMeasurementServer } from '@matter/main/behaviors/electrical-power-measurement'; import { ElectricalEnergyMeasurementServer } from '@matter/main/behaviors/electrical-energy-measurement'; import { TemperatureMeasurementServer } from '@matter/main/behaviors/temperature-measurement'; import { RelativeHumidityMeasurementServer } from '@matter/main/behaviors/relative-humidity-measurement'; import { PressureMeasurementServer } from '@matter/main/behaviors/pressure-measurement'; import { FlowMeasurementServer } from '@matter/main/behaviors/flow-measurement'; import { IlluminanceMeasurementServer } from '@matter/main/behaviors/illuminance-measurement'; import { OccupancySensingServer } from '@matter/main/behaviors/occupancy-sensing'; import { AirQualityServer } from '@matter/main/behaviors/air-quality'; import { CarbonMonoxideConcentrationMeasurementServer } from '@matter/main/behaviors/carbon-monoxide-concentration-measurement'; import { CarbonDioxideConcentrationMeasurementServer } from '@matter/main/behaviors/carbon-dioxide-concentration-measurement'; import { NitrogenDioxideConcentrationMeasurementServer } from '@matter/main/behaviors/nitrogen-dioxide-concentration-measurement'; import { OzoneConcentrationMeasurementServer } from '@matter/main/behaviors/ozone-concentration-measurement'; import { FormaldehydeConcentrationMeasurementServer } from '@matter/main/behaviors/formaldehyde-concentration-measurement'; import { Pm1ConcentrationMeasurementServer } from '@matter/main/behaviors/pm1-concentration-measurement'; import { Pm25ConcentrationMeasurementServer } from '@matter/main/behaviors/pm25-concentration-measurement'; import { Pm10ConcentrationMeasurementServer } from '@matter/main/behaviors/pm10-concentration-measurement'; import { RadonConcentrationMeasurementServer } from '@matter/main/behaviors/radon-concentration-measurement'; import { TotalVolatileOrganicCompoundsConcentrationMeasurementServer } from '@matter/main/behaviors/total-volatile-organic-compounds-concentration-measurement'; import { FanControlServer } from '@matter/main/behaviors/fan-control'; import { ResourceMonitoring } from '@matter/main/clusters/resource-monitoring'; import { ThermostatUserInterfaceConfigurationServer } from '@matter/main/behaviors/thermostat-user-interface-configuration'; // AnsiLogger module import { AnsiLogger, CYAN, YELLOW, db, debugStringify, hk, or, zb } from './logger/export.js'; // Matterbridge import { bridgedNode } from './matterbridgeDeviceTypes.js'; import { isValidNumber, isValidObject, isValidString } from './utils/export.js'; import { MatterbridgeServer, MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeLiftWindowCoveringServer, MatterbridgeLiftTiltWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeSwitchServer, MatterbridgeOperationalStateServer, MatterbridgeDeviceEnergyManagementModeServer, MatterbridgeDeviceEnergyManagementServer, MatterbridgeActivatedCarbonFilterMonitoringServer, MatterbridgeHepaFilterMonitoringServer, } from './matterbridgeBehaviors.js'; import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, createUniqueId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getDefaultOperationalStateClusterServer, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, getDefaultOccupancySensingClusterServer, lowercaseFirstLetter, updateAttribute, getClusterId, getAttributeId, setAttribute, getAttribute, checkNotLatinCharacters, generateUniqueId, subscribeAttribute, invokeBehaviorCommand, triggerEvent, featuresFor, } from './matterbridgeEndpointHelpers.js'; export class MatterbridgeEndpoint extends Endpoint { /** The bridge mode of Matterbridge */ static bridgeMode = ''; /** The default log level of the new MatterbridgeEndpoints */ static logLevel = "info" /* LogLevel.INFO */; /** * Activates a special mode for this endpoint. * - 'server': it creates the device server node and add the device as Matter device that needs to be paired individually. * In this case the bridge mode is not relevant. The device is autonomous. The main use case is a workaround for the Apple Home rvc issue. * * - 'matter': it adds the device directly to the bridge server node as Matter device. In this case the implementation must respect * the 9.2.3. Disambiguation rule (i.e. use taglist if needed cause the device doesn't have nodeLabel). * Furthermore the device will be a part of the bridge (i.e. will have the same name and will be in the same room). * See 9.12.2.2. Native Matter functionality in Bridge. * * @remarks * Always use createDefaultBasicInformationClusterServer() to create the BasicInformation cluster server. */ mode = undefined; /** The server node of the endpoint, if it is a single not bridged endpoint */ serverNode; /** The logger instance for the MatterbridgeEndpoint */ log; /** The plugin name this MatterbridgeEndpoint belongs to */ plugin = undefined; /** The configuration URL of the device, if available */ configUrl = undefined; deviceName = undefined; serialNumber = undefined; uniqueId = undefined; vendorId = undefined; vendorName = undefined; productId = undefined; productName = undefined; softwareVersion = undefined; softwareVersionString = undefined; hardwareVersion = undefined; hardwareVersionString = undefined; productUrl = 'https://www.npmjs.com/package/matterbridge'; /** The name of the first device type of the endpoint (old api compatibility) */ name = undefined; /** The code of the first device type of the endpoint (old api compatibility) */ deviceType = undefined; /** The original id (with spaces and .) of the endpoint (old api compatibility) */ uniqueStorageKey = undefined; tagList = undefined; /** Maps the DeviceTypeDefinitions with their code */ deviceTypes = new Map(); /** Command handler for the MatterbridgeEndpoint commands */ commandHandler = new NamedHandler(); /** * Represents a MatterbridgeEndpoint. * * @class MatterbridgeEndpoint * @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The DeviceTypeDefinition(s) of the endpoint. * @param {MatterbridgeEndpointOptions} [options] - The options for the device. * @param {boolean} [debug] - Debug flag. */ constructor(definition, options = {}, debug = false) { let deviceTypeList = []; // Get the first DeviceTypeDefinition let firstDefinition; if (Array.isArray(definition)) { firstDefinition = definition[0]; deviceTypeList = Array.from(definition.values()).map((dt) => ({ deviceType: dt.code, revision: dt.revision, })); } else { firstDefinition = definition; deviceTypeList = [{ deviceType: firstDefinition.code, revision: firstDefinition.revision }]; } // Convert the first DeviceTypeDefinition to an EndpointType.Options const deviceTypeDefinitionV8 = { name: firstDefinition.name.replace('-', '_'), deviceType: firstDefinition.code, deviceRevision: firstDefinition.revision, deviceClass: firstDefinition.deviceClass.toLowerCase(), requirements: { server: { mandatory: SupportedBehaviors(...getBehaviourTypesFromClusterServerIds(firstDefinition.requiredServerClusters)), optional: SupportedBehaviors(...getBehaviourTypesFromClusterServerIds(firstDefinition.optionalServerClusters)), }, client: { mandatory: SupportedBehaviors(...getBehaviourTypesFromClusterClientIds(firstDefinition.requiredClientClusters)), optional: SupportedBehaviors(...getBehaviourTypesFromClusterClientIds(firstDefinition.optionalClientClusters)), }, }, behaviors: options.tagList ? SupportedBehaviors(DescriptorServer.with(Descriptor.Feature.TagList)) : SupportedBehaviors(DescriptorServer), }; const endpointV8 = MutableEndpoint(deviceTypeDefinitionV8); // Check if the uniqueStorageKey is valid if (options.uniqueStorageKey && checkNotLatinCharacters(options.uniqueStorageKey)) { options.uniqueStorageKey = generateUniqueId(options.uniqueStorageKey); } if (options.id && checkNotLatinCharacters(options.id)) { options.id = generateUniqueId(options.id); } // Convert the options to an Endpoint.Options const optionsV8 = { id: options.uniqueStorageKey?.replace(/[ .]/g, ''), number: options.endpointId, descriptor: options.tagList ? { tagList: options.tagList, deviceTypeList } : { deviceTypeList }, }; // Override the deprecated uniqueStorageKey && endpointId with id and number if provided if (options.id !== undefined) { optionsV8.id = options.id.replace(/[ .]/g, ''); } if (options.number !== undefined) { optionsV8.number = options.number; } super(endpointV8, optionsV8); this.mode = options.mode; this.uniqueStorageKey = options.id ? options.id : options.uniqueStorageKey; this.name = firstDefinition.name; this.deviceType = firstDefinition.code; this.tagList = options.tagList; if (Array.isArray(definition)) { definition.forEach((deviceType) => { this.deviceTypes.set(deviceType.code, deviceType); }); } else this.deviceTypes.set(firstDefinition.code, firstDefinition); // console.log('MatterbridgeEndpoint.option', options); // console.log('MatterbridgeEndpoint.endpointV8', endpointV8); // console.log('MatterbridgeEndpoint.optionsV8', optionsV8); // Create the logger this.log = new AnsiLogger({ logName: options.uniqueStorageKey ?? 'MatterbridgeEndpoint', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: debug === true ? "debug" /* LogLevel.DEBUG */ : MatterbridgeEndpoint.logLevel }); this.log.debug(`${YELLOW}new${db} MatterbridgeEndpoint: ${zb}${'0x' + firstDefinition.code.toString(16).padStart(4, '0')}${db}-${zb}${firstDefinition.name}${db} mode: ${CYAN}${this.mode}${db} id: ${CYAN}${optionsV8.id}${db} number: ${CYAN}${optionsV8.number}${db} taglist: ${CYAN}${options.tagList ? debugStringify(options.tagList) : 'undefined'}${db}`); // Add MatterbridgeServer this.behaviors.require(MatterbridgeServer, { log: this.log, commandHandler: this.commandHandler }); } /** * Loads an instance of the MatterbridgeEndpoint class. * * @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The DeviceTypeDefinition(s) of the device. * @param {MatterbridgeEndpointOptions} [options] - The options for the device. * @param {boolean} [debug] - Debug flag. * @returns {Promise<MatterbridgeEndpoint>} MatterbridgeEndpoint instance. */ static async loadInstance(definition, options = {}, debug = false) { return new MatterbridgeEndpoint(definition, options, debug); } /** * Get all the device types of this endpoint. * * @returns {DeviceTypeDefinition[]} The device types of this endpoint. */ getDeviceTypes() { return Array.from(this.deviceTypes.values()); } /** * Checks if the provided cluster server is supported by this endpoint. * * @param {Behavior.Type | ClusterType | ClusterId | string} cluster - The cluster to check. * @returns {boolean} True if the cluster server is supported, false otherwise. */ hasClusterServer(cluster) { const behavior = getBehavior(this, cluster); if (behavior) return this.behaviors.supported[behavior.id] !== undefined; else return false; } /** * Checks if the provided attribute server is supported for a given cluster of this endpoint. * * @param {Behavior.Type | ClusterType | ClusterId | string} cluster - The cluster to check. * @param {string} attribute - The attribute name to check. * @returns {boolean} True if the attribute server is supported, false otherwise. */ hasAttributeServer(cluster, attribute) { const behavior = getBehavior(this, cluster); if (!behavior || !this.behaviors.supported[behavior.id]) return false; const options = this.behaviors.optionsFor(behavior); const defaults = this.behaviors.defaultsFor(behavior); return lowercaseFirstLetter(attribute) in options || lowercaseFirstLetter(attribute) in defaults; } /** * Retrieves the initial options for the provided cluster server. * * @param {Behavior.Type | ClusterType | ClusterId | string} cluster - The cluster to get options for. * @returns {Record<string, boolean | number | bigint | string | object | null> | undefined} The options for the provided cluster server, or undefined if the cluster is not supported. */ getClusterServerOptions(cluster) { const behavior = getBehavior(this, cluster); if (!behavior) return undefined; return this.behaviors.optionsFor(behavior); } /** * Retrieves the value of the provided attribute from the given cluster. * * @param {Behavior.Type | ClusterType | ClusterId | string} cluster - The cluster to retrieve the attribute from. * @param {string} attribute - The name of the attribute to retrieve. * @param {AnsiLogger} [log] - Optional logger for error and info messages. * @returns {any} The value of the attribute, or undefined if the attribute is not found. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any getAttribute(cluster, attribute, log) { return getAttribute(this, cluster, attribute, log); } /** * Sets the value of an attribute on a cluster server. * * @param {Behavior.Type | ClusterType | ClusterId | string} clusterId - The ID of the cluster. * @param {string} attribute - The name of the attribute. * @param {boolean | number | bigint | string | object | null} value - The value to set for the attribute. * @param {AnsiLogger} [log] - (Optional) The logger to use for logging errors and information. * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether the attribute was successfully set. */ async setAttribute(clusterId, attribute, value, log) { return await setAttribute(this, clusterId, attribute, value, log); } /** * Update the value of an attribute on a cluster server only if the value is different. * * @param {Behavior.Type | ClusterType | ClusterId | string} cluster - The cluster to set the attribute on. * @param {string} attribute - The name of the attribute. * @param {boolean | number | bigint | string | object | null} value - The value to set for the attribute. * @param {AnsiLogger} [log] - (Optional) The logger to use for logging the update. Errors are logged to the endpoint logger. * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether the attribute was successfully set. */ async updateAttribute(cluster, attribute, value, log) { return await updateAttribute(this, cluster, attribute, value, log); } /** * Subscribes to the provided attribute on a cluster. * * @param {Behavior.Type | ClusterType | ClusterId | string} cluster - The cluster to subscribe the attribute to. * @param {string} attribute - The name of the attribute to subscribe to. * @param {(newValue: any, oldValue: any, context: ActionContext) => void} listener - A callback function that will be called when the attribute value changes. When context.offline === true then the change is locally generated and not from the controller. * @param {AnsiLogger} [log] - Optional logger for logging errors and information. * @returns {Promise<boolean>} - A boolean indicating whether the subscription was successful. * * @remarks The listener function (cannot be async) will receive three parameters: * - `newValue`: The new value of the attribute. * - `oldValue`: The old value of the attribute. * - `context`: The action context, which includes information about the action that triggered the change. When context.offline === true then the change is locally generated and not from the controller. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async subscribeAttribute(cluster, attribute, listener, log) { return await subscribeAttribute(this, cluster, attribute, listener, log); } /** * Triggers an event on the specified cluster. * * @param {ClusterId} cluster - The ID of the cluster. * @param {string} event - The name of the event to trigger. * @param {Record<string, boolean | number | bigint | string | object | undefined | null>} payload - The payload to pass to the event. * @param {AnsiLogger} [log] - Optional logger for logging information. * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether the event was successfully triggered. */ async triggerEvent(cluster, event, payload, log) { return await triggerEvent(this, cluster, event, payload, log); } /** * Adds cluster servers from the provided server list. * * @param {ClusterId[]} serverList - The list of cluster IDs to add. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ addClusterServers(serverList) { addClusterServers(this, serverList); return this; } /** * Adds a fixed label to the FixedLabel cluster. If the cluster server is not present, it will be added. * * @param {string} label - The label to add. * @param {string} value - The value of the label. * @returns {Promise<this>} The current MatterbridgeEndpoint instance for chaining. */ async addFixedLabel(label, value) { await addFixedLabel(this, label, value); return this; } /** * Adds a user label to the UserLabel cluster. If the cluster server is not present, it will be added. * * @param {string} label - The label to add. * @param {string} value - The value of the label. * @returns {Promise<this>} The current MatterbridgeEndpoint instance for chaining. */ async addUserLabel(label, value) { await addUserLabel(this, label, value); return this; } /** * Adds a command handler for the specified command. * * @param {keyof MatterbridgeEndpointCommands} command - The command to add the handler for. * @param {CommandHandlerFunction} handler - The handler function to execute when the command is received. * @returns {this} The current MatterbridgeEndpoint instance for chaining. * * @remarks * The handler function will receive an object with the following properties: * - `request`: The request object sent with the command. * - `cluster`: The id of the cluster that received the command (i.e. "onOff"). * - `attributes`: The current attributes of the cluster that received the command (i.e. { onOff: true}). * - `endpoint`: The MatterbridgeEndpoint instance that received the command. */ addCommandHandler(command, handler) { this.commandHandler.addHandler(command, handler); return this; } /** * Execute the command handler for the specified command. Used ONLY in Jest tests. * * @param {keyof MatterbridgeEndpointCommands} command - The command to execute. * @param {Record<string, boolean | number | bigint | string | object | null>} [request] - The optional request to pass to the handler function. * @param {string} [cluster] - The optional cluster to pass to the handler function. * @param {Record<string, boolean | number | bigint | string | object | null>} [attributes] - The optional attributes to pass to the handler function. * @param {MatterbridgeEndpoint} [endpoint] - The optional MatterbridgeEndpoint instance to pass to the handler function * * @deprecated Used ONLY in Jest tests. */ async executeCommandHandler(command, request, cluster, attributes, endpoint) { await this.commandHandler.executeHandler(command, { request, cluster, attributes, endpoint }); } /** * Invokes a behavior command on the specified cluster. Used ONLY in Jest tests. * * @param {Behavior.Type | ClusterType | ClusterId | string} cluster - The cluster to invoke the command on. * @param {string} command - The command to invoke. * @param {Record<string, boolean | number | bigint | string | object | null>} [params] - The optional parameters to pass to the command. * * @deprecated Used ONLY in Jest tests. */ async invokeBehaviorCommand(cluster, command, params) { await invokeBehaviorCommand(this, cluster, command, params); } /** * Adds the required cluster servers (only if they are not present) for the device types of the specified endpoint. * * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ addRequiredClusterServers() { addRequiredClusterServers(this); return this; } /** * Adds the optional cluster servers (only if they are not present) for the device types of the specified endpoint. * * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ addOptionalClusterServers() { addOptionalClusterServers(this); return this; } /** * Retrieves all cluster servers. * * @returns {Behavior.Type[]} An array of all cluster servers. */ getAllClusterServers() { return Object.values(this.behaviors.supported); } /** * Retrieves the names of all cluster servers. * * @returns {string[]} An array of all cluster server names. */ getAllClusterServerNames() { return Object.keys(this.behaviors.supported); } /** * Iterates over each attribute of each cluster server of the device state and calls the provided callback function. * * @param {Function} callback - The callback function to call with the cluster name, cluster id, attribute name, attribute id and attribute value. */ forEachAttribute(callback) { if (!this.lifecycle.isReady || this.construction.status !== Lifecycle.Status.Active) return; for (const [clusterName, clusterAttributes] of Object.entries(this.state)) { // Skip if the key / cluster name is a number, cause they are double indexed. if (!isNaN(Number(clusterName))) continue; for (const [attributeName, attributeValue] of Object.entries(clusterAttributes)) { // Skip if the behavior has no associated cluster (i.e. matterbridge server) const clusterId = getClusterId(this, clusterName); if (clusterId === undefined) { // this.log.debug(`***forEachAttribute: cluster ${clusterName} not found`); continue; } // Skip if the attribute is not present in the ClusterBehavior.Type. Also skip if the attribute it is an internal state. const attributeId = getAttributeId(this, clusterName, attributeName); if (attributeId === undefined) { // this.log.debug(`***forEachAttribute: attribute ${clusterName}.${attributeName} not found`); continue; } callback(clusterName, clusterId, attributeName, attributeId, attributeValue); } } } /** * Adds a child endpoint with the specified device types and options. * If the child endpoint is not already present, it will be created and added. * If the child endpoint is already present, the existing child endpoint will be returned. * * @param {string} endpointName - The name of the new endpoint to add. * @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The device types to add. * @param {MatterbridgeEndpointOptions} [options] - The options for the endpoint. * @param {boolean} [debug] - Whether to enable debug logging. * @returns {MatterbridgeEndpoint} - The child endpoint that was found or added. * * @example * ```typescript * const endpoint = device.addChildDeviceType('Temperature', [temperatureSensor], { tagList: [{ mfgCode: null, namespaceId: LocationTag.Indoor.namespaceId, tag: LocationTag.Indoor.tag, label: null }] }, true); * ``` */ addChildDeviceType(endpointName, definition, options = {}, debug = false) { this.log.debug(`addChildDeviceType: ${CYAN}${endpointName}${db}`); let alreadyAdded = false; let child = this.getChildEndpointByName(endpointName); if (child) { this.log.debug(`****- endpoint ${CYAN}${endpointName}${db} already added!`); alreadyAdded = true; } else { if ('tagList' in options) { for (const tag of options.tagList) { this.log.debug(`- with tagList: mfgCode ${CYAN}${tag.mfgCode}${db} namespaceId ${CYAN}${tag.namespaceId}${db} tag ${CYAN}${tag.tag}${db} label ${CYAN}${tag.label}${db}`); } child = new MatterbridgeEndpoint(definition, { uniqueStorageKey: endpointName, endpointId: options.endpointId, tagList: options.tagList }, debug); } else { child = new MatterbridgeEndpoint(definition, { uniqueStorageKey: endpointName, endpointId: options.endpointId }, debug); } } if (Array.isArray(definition)) { definition.forEach((deviceType) => { this.log.debug(`- with deviceType: ${zb}${'0x' + deviceType.code.toString(16).padStart(4, '0')}${db}-${zb}${deviceType.name}${db}`); }); } else { this.log.debug(`- with deviceType: ${zb}${'0x' + definition.code.toString(16).padStart(4, '0')}${db}-${zb}${definition.name}${db}`); } if (alreadyAdded) return child; if (this.lifecycle.isInstalled) { this.log.debug(`- with lifecycle installed`); this.add(child); } else { this.log.debug(`- with lifecycle NOT installed`); this.parts.add(child); } return child; } /** * Adds a child endpoint with one or more device types with the required cluster servers and the specified cluster servers. * If the child endpoint is not already present in the childEndpoints, it will be added. * If the child endpoint is already present in the childEndpoints, the device types and cluster servers will be added to the existing child endpoint. * * @param {string} endpointName - The name of the new enpoint to add. * @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The device types to add. * @param {ClusterId[]} [serverList] - The list of cluster IDs to include. * @param {MatterbridgeEndpointOptions} [options] - The options for the device. * @param {boolean} [debug] - Whether to enable debug logging. * @returns {MatterbridgeEndpoint} - The child endpoint that was found or added. * * @example * ```typescript * const endpoint = device.addChildDeviceTypeWithClusterServer('Temperature', [temperatureSensor], [], { tagList: [{ mfgCode: null, namespaceId: LocationTag.Indoor.namespaceId, tag: LocationTag.Indoor.tag, label: null }] }, true); * ``` */ addChildDeviceTypeWithClusterServer(endpointName, definition, serverList = [], options = {}, debug = false) { this.log.debug(`addChildDeviceTypeWithClusterServer: ${CYAN}${endpointName}${db}`); let alreadyAdded = false; let child = this.getChildEndpointByName(endpointName); if (child) { this.log.debug(`****- endpoint ${CYAN}${endpointName}${db} already added!`); alreadyAdded = true; } else { if ('tagList' in options) { for (const tag of options.tagList) { this.log.debug(`- with tagList: mfgCode ${CYAN}${tag.mfgCode}${db} namespaceId ${CYAN}${tag.namespaceId}${db} tag ${CYAN}${tag.tag}${db} label ${CYAN}${tag.label}${db}`); } child = new MatterbridgeEndpoint(definition, { uniqueStorageKey: endpointName, endpointId: options.endpointId, tagList: options.tagList }, debug); } else { child = new MatterbridgeEndpoint(definition, { uniqueStorageKey: endpointName, endpointId: options.endpointId }, debug); } } if (Array.isArray(definition)) { definition.forEach((deviceType) => { this.log.debug(`- with deviceType: ${zb}${'0x' + deviceType.code.toString(16).padStart(4, '0')}${db}-${zb}${deviceType.name}${db}`); deviceType.requiredServerClusters.forEach((clusterId) => { if (!serverList.includes(clusterId)) serverList.push(clusterId); }); }); } else { this.log.debug(`- with deviceType: ${zb}${'0x' + definition.code.toString(16).padStart(4, '0')}${db}-${zb}${definition.name}${db}`); definition.requiredServerClusters.forEach((clusterId) => { if (!serverList.includes(clusterId)) serverList.push(clusterId); }); } serverList.forEach((clusterId) => { if (!child.hasClusterServer(clusterId)) { this.log.debug(`- with cluster: ${hk}${'0x' + clusterId.toString(16).padStart(4, '0')}${db}-${hk}${getClusterNameById(clusterId)}${db}`); } else { serverList.splice(serverList.indexOf(clusterId), 1); } }); if (alreadyAdded) { serverList.forEach((clusterId) => { if (child.hasClusterServer(clusterId)) serverList.splice(serverList.indexOf(clusterId), 1); }); } addClusterServers(child, serverList); if (alreadyAdded) return child; if (this.lifecycle.isInstalled) { this.log.debug(`- with lifecycle installed`); this.add(child); } else { this.log.debug(`- with lifecycle NOT installed`); this.parts.add(child); } return child; } /** * Retrieves a child endpoint by its name. * * @param {string} endpointName - The name of the endpoint to retrieve. * @returns {Endpoint | undefined} The child endpoint with the specified name, or undefined if not found. */ getChildEndpointByName(endpointName) { return this.parts.find((part) => part.id === endpointName); } /** * Retrieves a child endpoint by its EndpointNumber. * * @param {EndpointNumber} endpointNumber - The EndpointNumber of the endpoint to retrieve. * @returns {MatterbridgeEndpoint | undefined} The child endpoint with the specified EndpointNumber, or undefined if not found. */ getChildEndpoint(endpointNumber) { return this.parts.find((part) => part.number === endpointNumber); } /** * Get all the child endpoints of this endpoint. * * @returns {MatterbridgeEndpoint[]} The child endpoints. */ getChildEndpoints() { return Array.from(this.parts); } /** * Serializes the Matterbridge device into a serialized object. * * @param {MatterbridgeEndpoint} device - The Matterbridge device to serialize. * * @returns {SerializedMatterbridgeEndpoint | undefined} The serialized Matterbridge device object. */ static serialize(device) { if (!device.serialNumber || !device.deviceName || !device.uniqueId) return; const serialized = { pluginName: device.plugin ?? '', deviceName: device.deviceName, serialNumber: device.serialNumber, uniqueId: device.uniqueId, productId: device.productId, productName: device.productName, vendorId: device.vendorId, vendorName: device.vendorName, deviceTypes: Array.from(device.deviceTypes.values()), endpoint: device.maybeNumber, endpointName: device.maybeId ?? device.deviceName, clusterServersId: [], }; Object.keys(device.behaviors.supported).forEach((behaviorName) => { if (behaviorName === 'bridgedDeviceBasicInformation') serialized.clusterServersId.push(BridgedDeviceBasicInformation.Cluster.id); if (behaviorName === 'powerSource') serialized.clusterServersId.push(PowerSource.Cluster.id); // serialized.clusterServersId.push(this.behaviors.supported[behaviorName]cluster.id); }); return serialized; } /** * Deserializes the device into a serialized object. * * @param {SerializedMatterbridgeEndpoint} serializedDevice - The serialized Matterbridge device object. * @returns {MatterbridgeEndpoint | undefined} The deserialized Matterbridge device. */ static deserialize(serializedDevice) { const device = new MatterbridgeEndpoint(serializedDevice.deviceTypes, { uniqueStorageKey: serializedDevice.endpointName, endpointId: serializedDevice.endpoint }, false); device.plugin = serializedDevice.pluginName; device.deviceName = serializedDevice.deviceName; device.serialNumber = serializedDevice.serialNumber; device.uniqueId = serializedDevice.uniqueId; device.vendorId = serializedDevice.vendorId; device.vendorName = serializedDevice.vendorName; device.productId = serializedDevice.productId; device.productName = serializedDevice.productName; for (const clusterId of serializedDevice.clusterServersId) { if (clusterId === BridgedDeviceBasicInformation.Cluster.id) device.createDefaultBridgedDeviceBasicInformationClusterServer(serializedDevice.deviceName, serializedDevice.serialNumber, serializedDevice.vendorId ?? 0xfff1, serializedDevice.vendorName ?? 'Matterbridge', serializedDevice.productName ?? 'Matterbridge device'); else if (clusterId === PowerSource.Cluster.id) device.createDefaultPowerSourceWiredClusterServer(); // else addClusterServerFromList(device, [clusterId]); } return device; } /** * Creates a default power source wired cluster server. * * @param {PowerSource.WiredCurrentType} wiredCurrentType - The type of wired current (default: PowerSource.WiredCurrentType.Ac) * @returns {this} The current MatterbridgeEndpoint instance for chaining. * * @remarks * - order: The order of the power source is a persisted attribute that indicates the order in which the power sources are used. * - description: The description of the power source is a fixed attribute that describes the power source type. * - wiredCurrentType: The type of wired current is a fixed attribute that indicates the type of wired current used by the power source (AC or DC). */ createDefaultPowerSourceWiredClusterServer(wiredCurrentType = PowerSource.WiredCurrentType.Ac) { this.behaviors.require(PowerSourceServer.with(PowerSource.Feature.Wired), { // Base attributes status: PowerSource.PowerSourceStatus.Active, order: 0, description: wiredCurrentType === PowerSource.WiredCurrentType.Ac ? 'AC Power' : 'DC Power', endpointList: [], // Wired feature attributes wiredCurrentType, }); return this; } /** * Creates a default power source replaceable battery cluster server. * * @param {number} batPercentRemaining - The remaining battery percentage (default: 100). * @param {PowerSource.BatChargeLevel} batChargeLevel - The battery charge level (default: PowerSource.BatChargeLevel.Ok). * @param {number} batVoltage - The battery voltage (default: 1500). * @param {string} batReplacementDescription - The description of the battery replacement (default: 'Battery type'). * @param {number} batQuantity - The quantity of the battery (default: 1). * @param {PowerSource.BatReplaceability} batReplaceability - The replaceability of the battery (default: PowerSource.BatReplaceability.Unspecified). * @returns {this} The current MatterbridgeEndpoint instance for chaining. * * @remarks * - order: The order of the power source is a persisted attribute that indicates the order in which the power sources are used. * - description: The description of the power source is a fixed attribute that describes the power source type. * - batReplaceability: The replaceability of the battery is a fixed attribute that indicates whether the battery is user-replaceable or not. * - batReplacementDescription: The description of the battery replacement is a fixed attribute that describes the battery type. * - batQuantity: The quantity of the battery is a fixed attribute that indicates how many batteries are present in the device. */ createDefaultPowerSourceReplaceableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500, batReplacementDescription = 'Battery type', batQuantity = 1, batReplaceability = PowerSource.BatReplaceability.UserReplaceable) { this.behaviors.require(PowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Replaceable), { // Base attributes status: PowerSource.PowerSourceStatus.Active, order: 0, description: 'Primary battery', endpointList: [], // Battery feature attributes batVoltage, batPercentRemaining: Math.min(Math.max(batPercentRemaining * 2, 0), 200), batChargeLevel, batReplacementNeeded: false, batReplaceability, activeBatFaults: undefined, // Replaceable feature attributes batReplacementDescription, batQuantity, }); return this; } /** * Creates a default power source rechargeable battery cluster server. * * @param {number} [batPercentRemaining] - The remaining battery percentage (default: 100). * @param {PowerSource.BatChargeLevel} [batChargeLevel] - The battery charge level (default: PowerSource.BatChargeLevel.Ok). * @param {number} [batVoltage] - The battery voltage in mV (default: 1500). * @param {PowerSource.BatReplaceability} [batReplaceability] - The replaceability of the battery (default: PowerSource.BatReplaceability.Unspecified). * @returns {this} The current MatterbridgeEndpoint instance for chaining. * * @remarks * - order: The order of the power source is a persisted attribute that indicates the order in which the power sources are used. * - description: The description of the power source is a fixed attribute that describes the power source type. * - batReplaceability: The replaceability of the battery is a fixed attribute that indicates whether the battery is user-replaceable or not. */ createDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500, batReplaceability = PowerSource.BatReplaceability.Unspecified) { this.behaviors.require(PowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Rechargeable), { // Base attributes status: PowerSource.PowerSourceStatus.Active, order: 0, description: 'Primary battery', endpointList: [], // Battery feature attributes batVoltage, batPercentRemaining: Math.min(Math.max(batPercentRemaining * 2, 0), 200), batTimeRemaining: null, // Indicates the estimated time in seconds before the battery will no longer be able to provide power to the Node batChargeLevel, batReplacementNeeded: false, batReplaceability, batPresent: true, activeBatFaults: [], // Rechargeable feature attributes batChargeState: PowerSource.BatChargeState.IsNotCharging, batFunctionalWhileCharging: true, }); return this; } /** * Setup the default Basic Information Cluster Server attributes for the server node. * * This method sets the device name, serial number, unique ID, vendor ID, vendor name, product ID, product name, software version, software version string, hardware version and hardware version string. * * In bridge mode, it also adds the bridgedNode device type to the deviceTypes map and the bridgedNode device type to the deviceTypeList of the Descriptor cluster and creates a default BridgedDeviceBasicInformationClusterServer. * * The actual BasicInformationClusterServer is created by the MatterbridgeEndpoint class for device.mode = 'server' and for the unique device of an AccessoryPlatform. * * @param {string} deviceName - The name of the device. * @param {string} serialNumber - The serial number of the device. * @param {number} [vendorId] - The vendor ID of the device. Default is 0xfff1 (Matter Test VendorId). * @param {string} [vendorName] - The name of the vendor. Default is 'Matterbridge'. * @param {number} [productId] - The product ID of the device. Default is 0x8000 (Matter Test ProductId). * @param {string} [productName] - The name of the product. Default is 'Matterbridge device'. * @param {number} [softwareVersion] - The software version of the device. Default is 1. * @param {string} [softwareVersionString] - The software version string of the device. Default is '1.0.0'. * @param {number} [hardwareVersion] - The hardware version of the device. Default is 1. * @param {string} [hardwareVersionString] - The hardware version string of the device. Default is '1.0.0'. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultBasicInformationClusterServer(deviceName, serialNumber, vendorId = 0xfff1, vendorName = 'Matterbridge', productId = 0x8000, productName = 'Matterbridge device', softwareVersion = 1, softwareVersionString = '1.0.0', hardwareVersion = 1, hardwareVersionString = '1.0.0') { this.log.logName = deviceName; this.deviceName = deviceName; this.serialNumber = serialNumber; this.uniqueId = createUniqueId(deviceName, serialNumber, vendorName, productName); this.productId = productId; this.productName = productName; this.vendorId = vendorId; this.vendorName = vendorName; this.softwareVersion = softwareVersion; this.softwareVersionString = softwareVersionString; this.hardwareVersion = hardwareVersion; this.hardwareVersionString = hardwareVersionString; if (MatterbridgeEndpoint.bridgeMode === 'bridge' && this.mode === undefined) { this.deviceTypes.set(bridgedNode.code, bridgedNode); const options = this.getClusterServerOptions(Descriptor.Cluster.id); if (options) { const deviceTypeList = options.deviceTypeList; if (!deviceTypeList.find((dt) => dt.deviceType === bridgedNode.code)) { deviceTypeList.push({ deviceType: bridgedNode.code, revision: bridgedNode.revision }); } } this.createDefaultBridgedDeviceBasicInformationClusterServer(deviceName, serialNumber, vendorId, vendorName, productName, softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString); } return this; } /** * Creates a default BridgedDeviceBasicInformationClusterServer for the aggregator endpoints. * * @param {string} deviceName - The name of the device. * @param {string} serialNumber - The serial number of the device. * @param {number} [vendorId] - The vendor ID of the device. Default is 0xfff1 (Matter Test VendorId). * @param {string} [vendorName] - The name of the vendor. Default is 'Matterbridge'. * @param {string} [productName] - The name of the product. Default is 'Matterbridge device'. * @param {number} [softwareVersion] - The software version of the device. Default is 1. * @param {string} [softwareVersionString] - The software version string of the device. Default is '1.0.0'. * @param {number} [hardwareVersion] - The hardware version of the device. Default is 1. * @param {string