UNPKG

matterbridge

Version:
940 lines • 102 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.0.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. * */ // AnsiLogger module import { AnsiLogger, BLUE, CYAN, YELLOW, db, debugStringify, er, hk, or, zb } from './logger/export.js'; // Matterbridge import { bridgedNode } from './matterbridgeDeviceTypes.js'; import { isValidNumber, isValidObject } from './utils/export.js'; import { MatterbridgeBehavior, MatterbridgeBehaviorDevice, MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeSwitchServer, } from './matterbridgeBehaviors.js'; import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, capitalizeFirstLetter, createUniqueId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, getDefaultOccupancySensingClusterServer, lowercaseFirstLetter, updateAttribute, getClusterId, getAttributeId, setAttribute, getAttribute, checkNotLatinCharacters, generateUniqueId, subscribeAttribute, } from './matterbridgeEndpointHelpers.js'; // @matter import { Endpoint, Lifecycle, MutableEndpoint, NamedHandler, SupportedBehaviors, VendorId } from '@matter/main'; import { ClusterType, 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'; // @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'; export class MatterbridgeEndpoint extends Endpoint { static bridgeMode = ''; static logLevel = "info" /* LogLevel.INFO */; log; plugin = undefined; 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 first device type of the endpoint name = undefined; deviceType; uniqueStorageKey = undefined; tagList = undefined; // Maps matter deviceTypes deviceTypes = new Map(); // Command handler 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 MatterbridgeBehavior with MatterbridgeBehaviorDevice this.behaviors.require(MatterbridgeBehavior, { deviceCommand: new MatterbridgeBehaviorDevice(this.log, this.commandHandler, undefined) }); } /** * 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) => void} listener - A callback function that will be called when the attribute value changes. * @param {AnsiLogger} [log] - Optional logger for logging errors and information. * @returns {Promise<boolean>} - A boolean indicating whether the subscription was successful. */ // 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} clusterId - 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(clusterId, event, payload, log) { const clusterName = lowercaseFirstLetter(getClusterNameById(clusterId)); if (this.construction.status !== Lifecycle.Status.Active) { this.log.error(`triggerEvent ${hk}${clusterName}.${event}${er} error: Endpoint ${or}${this.maybeId}${er}:${or}${this.maybeNumber}${er} is in the ${BLUE}${this.construction.status}${er} state`); return false; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const events = this.events; if (!(clusterName in events) || !(event in events[clusterName])) { this.log.error(`triggerEvent ${hk}${event}${er} error: Cluster ${'0x' + clusterId.toString(16).padStart(4, '0')}:${clusterName} not found on endpoint ${or}${this.id}${er}:${or}${this.number}${er}`); return false; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await this.act((agent) => agent[clusterName].events[event].emit(payload, agent.context)); log?.info(`${db}Trigger event ${hk}${capitalizeFirstLetter(clusterName)}${db}.${hk}${event}${db} with ${debugStringify(payload)}${db} on endpoint ${or}${this.id}${db}:${or}${this.number}${db} `); return true; } /** * 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 {HandlerFunction} handler - The handler function to execute when the command is received. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ addCommandHandler(command, handler) { this.commandHandler.addHandler(command, handler); return this; } /** * Execute the command handler for the specified command. Mainly used 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. * @returns {Promise<void>} A promise that resolves when the command handler has been executed */ async executeCommandHandler(command, request) { await this.commandHandler.executeHandler(command, { request }); } /** * 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)) { for (const [attributeName, attributeValue] of Object.entries(clusterAttributes)) { const clusterId = getClusterId(this, clusterName); if (clusterId === undefined) { // this.log.error(`forEachAttribute error: cluster ${clusterName} not found`); continue; } const attributeId = getAttributeId(this, clusterName, attributeName); if (attributeId === undefined) { // this.log.error(`forEachAttribute error: 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; } /** * Creates a default Basic Information Cluster Server 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. */ 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, // 4874 vendorName: vendorName.slice(0, 32), productName: productName.slice(0, 32), productUrl: this.productUrl, productLabel: deviceName.slice(0, 64), nodeLabel: deviceName.slice(0, 32), serialNumber: serialNumber.slice(0, 32), uniqueId: this.uniqueId, softwareVersion, softwareVersionString: softwareVersionString.slice(0, 64), hardwareVersion, hardwareVersionString: hardwareVersionString.slice(0, 64), 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. */ createDefaultScenesClusterServer() { this.behaviors.require(ScenesManagementServer); return this; } /** * Creates a default OnOff cluster server for light devices. * * @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.for(ClusterType(OnOff.Base)), { onOff, }); return this; } /** * Creates a DeadFront OnOff cluster server. * * @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 a default level control cluster server for light devices. * * @param {number} [currentLevel=254] - The current level (default: 254). * @param {number} [minLevel=1] - The minimum level (default: 1). * @param {number} [maxLevel=254] - The maximum level (default: 254). * @param {number | null} [onLevel=null] - The on level (default: null). * @param {number | null} [startUpCurrentLevel=null] - The startUp on level (default: null). * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultLevelControlClusterServer(currentLevel = 254, minLevel = 1, maxLevel = 254, onLevel = null, startUpCurrentLevel = null) { this.behaviors.require(MatterbridgeLevelControlServer.with(LevelControl.Feature.OnOff, LevelControl.Feature.Lighting), { currentLevel, minLevel, maxLevel, onLevel, remainingTime: 0, startUpCurrentLevel, options: { executeIfOff: false, coupleColorTempToLevel: false, }, }); return this; } /** * Creates a default color control cluster server with Xy, HueSaturation and ColorTemperature. * * @param currentX - The current X value. * @param currentY - The current Y value. * @param currentHue - The current hue value. * @param currentSaturation - The current saturation value. * @param colorTemperatureMireds - The color temperature in mireds. * @param colorTempPhysicalMinMireds - The physical minimum color temperature in mireds. * @param colorTempPhysicalMaxMireds - The physical maximum color temperature in mireds. * @returns {this} The current MatterbridgeEndpoint instance for chaining. */ createDefaultColorControlClusterServer(currentX = 0, currentY = 0, currentHue = 0, currentSaturation = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500) { this.behaviors.require(MatterbridgeColorControlServer.with(ColorControl.Feature.Xy, ColorControl.Feature.HueSaturation, ColorControl.Feature.ColorTemperature), { colorMode: ColorControl.ColorMode.CurrentHueAndCurrentSaturation, enhancedColorMode: ColorControl.EnhancedColorMode.CurrentHueAndCurrentSaturation, colorCapabilities: { xy: true, hueSaturation: true, colorLoop: false, enhancedHue: false, colorTemperature: true }, options: { executeIfOff: false, }, numberOfPrimaries: null, currentX, currentY, currentHue, currentSaturation, colorTemperatureMireds, colorTempPhysicalMinMireds, colorTempPhysicalMaxMireds, coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds, remainingTime: 0, startUpColorTemperatureMireds: null, }); return this; } /** * Creates a Xy color control cluster server with Xy and ColorTemperature. * * @param currentX - The current X value. * @param currentY - The current Y value. * @param colorTemperatureMireds - The color temperature in mireds. * @param colorTempPhysicalMinMireds - The physical minimum color temperature in mireds. * @param colorTempPhysicalMaxMireds - The physical maximum color temperature in mireds. * @returns {this} The current MatterbridgeEndp