matterbridge
Version:
Matterbridge plugin manager for Matter
940 lines • 102 kB
JavaScript
/**
* 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