matterbridge
Version:
Matterbridge plugin manager for Matter
923 lines • 131 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.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