UNPKG

matterbridge

Version:
923 lines • 131 kB
/** * This file contains the class MatterbridgeEndpoint that extends the Endpoint class from the Matter.js library. * * @file matterbridgeEndpoint.ts * @author Luca Liguori * @date 2024-10-01 * @version 2.1.1 * * 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. * */ // 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, } 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, } from './matterbridgeEndpointHelpers.js'; // @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 { HepaFilterMonitoringServer } from '@matter/main/behaviors/hepa-filter-monitoring'; import { ActivatedCarbonFilterMonitoringServer } from '@matter/main/behaviors/activated-carbon-filter-monitoring'; import { ThermostatUserInterfaceConfigurationServer } from '@matter/main/behaviors/thermostat-user-interface-configuration'; import { DeviceEnergyManagementServer } from '@matter/main/behaviors/device-energy-management'; 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 */; /** 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 server node of the endpoint, if it is a single not bridged endpoint */ serverNode; /** 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; /** The id 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. * @constructor * @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The DeviceTypeDefinition(s) of the endpoint. * @param {MatterbridgeEndpointOptions} [options={}] - The options for the device. * @param {boolean} [debug=false] - 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)) : {}, }; const endpointV8 = MutableEndpoint(deviceTypeDefinitionV8); // Check if the uniqueStorageKey is valid if (options.uniqueStorageKey && checkNotLatinCharacters(options.uniqueStorageKey)) { options.uniqueStorageKey = generateUniqueId(options.uniqueStorageKey); } // 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 }, }; super(endpointV8, optionsV8); this.uniqueStorageKey = 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} id: ${CYAN}${options.uniqueStorageKey}${db} number: ${CYAN}${options.endpointId}${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=false] - 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. * * @deprecated Used ONLY in Jest tests. */ async executeCommandHandler(command, request) { await this.commandHandler.executeHandler(command, { request }); } /** * 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=false] - 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=false] - 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 pluginName - The name of the plugin. * @returns 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. * * @returns The deserialized MatterbridgeDevice. */ 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 wiredCurrentType - The type of wired current (default: PowerSource.WiredCurrentType.Ac) * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultPowerSourceWiredClusterServer(wiredCurrentType = PowerSource.WiredCurrentType.Ac) { this.behaviors.require(PowerSourceServer.with(PowerSource.Feature.Wired), { wiredCurrentType, description: wiredCurrentType === PowerSource.WiredCurrentType.Ac ? 'AC Power' : 'DC Power', status: PowerSource.PowerSourceStatus.Active, order: 0, endpointList: [], }); return this; } /** * Creates a default power source replaceable battery cluster server. * * @param batPercentRemaining - The remaining battery percentage (default: 100). * @param batChargeLevel - The battery charge level (default: PowerSource.BatChargeLevel.Ok). * @param batVoltage - The battery voltage (default: 1500). * @param batReplacementDescription - The battery replacement description (default: 'Battery type'). * @param batQuantity - The battery quantity (default: 1). * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultPowerSourceReplaceableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500, batReplacementDescription = 'Battery type', batQuantity = 1) { this.behaviors.require(PowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Replaceable), { status: PowerSource.PowerSourceStatus.Active, order: 0, description: 'Primary battery', batVoltage, batPercentRemaining: Math.min(Math.max(batPercentRemaining * 2, 0), 200), batChargeLevel, batReplacementNeeded: false, batReplaceability: PowerSource.BatReplaceability.UserReplaceable, activeBatFaults: undefined, batReplacementDescription, batQuantity, endpointList: [], }); return this; } /** * Creates a default power source rechargeable battery cluster server. * * @param batPercentRemaining - The remaining battery percentage (default: 100). * @param batChargeLevel - The battery charge level (default: PowerSource.BatChargeLevel.Ok). * @param batVoltage - The battery voltage (default: 1500). * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500) { this.behaviors.require(PowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Rechargeable), { status: PowerSource.PowerSourceStatus.Active, order: 0, description: 'Primary battery', batVoltage, batPercentRemaining: Math.min(Math.max(batPercentRemaining * 2, 0), 200), batTimeRemaining: 1, batChargeLevel, batReplacementNeeded: false, batReplaceability: PowerSource.BatReplaceability.Unspecified, activeBatFaults: undefined, batChargeState: PowerSource.BatChargeState.IsNotCharging, batFunctionalWhileCharging: true, endpointList: [], }); return this; } /** * Setup the default Basic Information Cluster Server attributes for the server node. * * @param deviceName - The name of the device. * @param serialNumber - The serial number of the device. * @param vendorId - The vendor ID of the device. * @param vendorName - The vendor name of the device. * @param productId - The product ID of the device. * @param productName - The product name of the device. * @param softwareVersion - The software version of the device. Default is 1. * @param softwareVersionString - The software version string of the device. Default is 'v.1.0.0'. * @param hardwareVersion - The hardware version of the device. Default is 1. * @param hardwareVersionString - The hardware version string of the device. Default is 'v.1.0.0'. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultBasicInformationClusterServer(deviceName, serialNumber, vendorId, vendorName, productId, productName, 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') { const options = this.getClusterServerOptions(Descriptor.Cluster.id); if (options) { const deviceTypeList = options.deviceTypeList; 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 deviceName - The name of the device. * @param serialNumber - The serial number of the device. * @param vendorId - The vendor ID of the device. * @param vendorName - The name of the vendor. * @param productName - The name of the product. * @param softwareVersion - The software version of the device. Default is 1. * @param softwareVersionString - The software version string of the device. Default is 'v.1.0.0'. * @param hardwareVersion - The hardware version of the device. Default is 1. * @param hardwareVersionString - The hardware version string of the device. Default is 'v.1.0.0'. * @returns {this} The current MatterbridgeEndpoint instance for chaining. * * @remarks The bridgedNode device type must be added to the deviceTypeList of the Descriptor cluster. */ createDefaultBridgedDeviceBasicInformationClusterServer(deviceName, serialNumber, vendorId, vendorName, productName, 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 = undefined; this.productName = productName; this.vendorId = vendorId; this.vendorName = vendorName; this.softwareVersion = softwareVersion; this.softwareVersionString = softwareVersionString; this.hardwareVersion = hardwareVersion; this.hardwareVersionString = hardwareVersionString; this.behaviors.require(BridgedDeviceBasicInformationServer.enable({ events: { leave: true, reachableChanged: true }, }), { vendorId: vendorId !== undefined ? VendorId(vendorId) : undefined, vendorName: vendorName.slice(0, 32), productName: productName.slice(0, 32), productUrl: this.productUrl.slice(0, 256), productLabel: deviceName.slice(0, 64), nodeLabel: deviceName.slice(0, 32), serialNumber: serialNumber.slice(0, 32), uniqueId: this.uniqueId.slice(0, 32), softwareVersion: isValidNumber(softwareVersion, 0, UINT32_MAX) ? softwareVersion : undefined, softwareVersionString: isValidString(softwareVersionString) ? softwareVersionString.slice(0, 64) : undefined, hardwareVersion: isValidNumber(hardwareVersion, 0, UINT16_MAX) ? hardwareVersion : undefined, hardwareVersionString: isValidString(hardwareVersionString) ? hardwareVersionString.slice(0, 64) : undefined, reachable: true, }); return this; } /** * Creates a default identify cluster server with the specified identify time and type. * * @param {number} [identifyTime=0] - The time to identify the server. Defaults to 0. * @param {Identify.IdentifyType} [identifyType=Identify.IdentifyType.None] - The type of identification. Defaults to Identify.IdentifyType.None. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultIdentifyClusterServer(identifyTime = 0, identifyType = Identify.IdentifyType.None) { this.behaviors.require(MatterbridgeIdentifyServer, { identifyTime, identifyType, }); return this; } /** * Creates a default groups cluster server. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultGroupsClusterServer() { this.behaviors.require(GroupsServer); return this; } /** * Creates a default scenes management cluster server. * @returns {this} The current MatterbridgeEndpoint instance for chaining. * * @remarks The scenes management cluster server is still provisional and so not yet implemented. */ createDefaultScenesClusterServer() { this.behaviors.require(ScenesManagementServer); return this; } /** * Creates a default OnOff cluster server for light devices with feature Lighting. * * @param {boolean} [onOff=false] - The initial state of the OnOff cluster. * @param {boolean} [globalSceneControl=false] - The global scene control state. * @param {number} [onTime=0] - The on time value. * @param {number} [offWaitTime=0] - The off wait time value. * @param {OnOff.StartUpOnOff | null} [startUpOnOff=null] - The start-up OnOff state. Null means previous state. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultOnOffClusterServer(onOff = false, globalSceneControl = false, onTime = 0, offWaitTime = 0, startUpOnOff = null) { this.behaviors.require(MatterbridgeOnOffServer.with(OnOff.Feature.Lighting), { onOff, globalSceneControl, onTime, offWaitTime, startUpOnOff, }); return this; } /** * Creates an OnOff cluster server without features. * * @param {boolean} [onOff=false] - The initial state of the OnOff cluster. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createOnOffClusterServer(onOff = false) { this.behaviors.require(MatterbridgeOnOffServer, { onOff, }); return this; } /** * Creates a DeadFront OnOff cluster server with feature DeadFrontBehavior. * * @param {boolean} [onOff=false] - The initial state of the OnOff cluster. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDeadFrontOnOffClusterServer(onOff = false) { this.behaviors.require(MatterbridgeOnOffServer.with(OnOff.Feature.DeadFrontBehavior), { onOff, }); return this; } /** * Creates an OffOnly OnOff cluster server with feature OffOnly. * * @param {boolean} [onOff=false] - The initial state of the OnOff cluster. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createOffOnlyOnOffClusterServer(onOff = false) { this.behaviors.require(MatterbridgeOnOffServer.with(OnOff.Feature.OffOnly), { onOff, }); return this; } /** * Creates a de