matterbridge-hass
Version:
Matterbridge hass plugin
677 lines (676 loc) • 36.8 kB
JavaScript
import { createHash, randomBytes } from 'node:crypto';
import { bridgedNode, colorTemperatureLight, colorTemperatureSwitch, dimmableLight, dimmableOutlet, dimmableSwitch, extendedColorLight, MatterbridgeColorControlServer, MatterbridgeEndpoint, MatterbridgeFanControlServer, MatterbridgeModeSelectServer, MatterbridgeOnOffServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeThermostatServer, onOffLight, onOffOutlet, onOffSwitch, } from 'matterbridge';
import { MatterbridgeKeypadInputServer, MatterbridgeMediaPlaybackServer, MatterbridgeRvcCleanModeServer, MatterbridgeRvcOperationalStateServer, MatterbridgeRvcRunModeServer, } from 'matterbridge/devices';
import { AnsiLogger, CYAN, db, debugStringify, idn, ign, rs } from 'matterbridge/logger';
import { UINT16_MAX, UINT32_MAX } from 'matterbridge/matter';
import { BooleanStateServer, BridgedDeviceBasicInformationServer, PowerSourceServer } from 'matterbridge/matter/behaviors';
import { BooleanState, BridgedDeviceBasicInformation, ColorControl, FanControl, Groups, Identify, KeypadInput, MediaPlayback, ModeSelect, OnOff, PowerSource, RvcCleanMode, RvcOperationalState, RvcRunMode, SmokeCoAlarm, Thermostat, } from 'matterbridge/matter/clusters';
import { getClusterNameById, VendorId } from 'matterbridge/matter/types';
import { isValidNumber, isValidString } from 'matterbridge/utils';
export function getClusterServerObj(clusterId, type, options) {
return { id: clusterId, type, options };
}
export class MutableDevice {
log;
mutableDevices = new Map();
endpoints = new Map();
remappedEndpoints = new Set();
splitEndpoints = new Set();
matterbridge;
deviceName;
serialNumber;
vendorId;
vendorName;
productId;
productName;
softwareVersion;
softwareVersionString;
hardwareVersion;
hardwareVersionString;
composedType = undefined;
configUrl = undefined;
mode = undefined;
constructor(matterbridge, deviceName, serialNumber, vendorId = 0xfff1, vendorName = 'Matterbridge', productId = 0x8000, productName = 'Matterbridge Device', softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString) {
this.log = new AnsiLogger({ logName: 'MutableDevice', logTimestampFormat: 4 });
this.matterbridge = matterbridge;
this.deviceName = deviceName;
this.serialNumber = serialNumber ?? '0x' + randomBytes(8).toString('hex');
this.vendorId = VendorId(vendorId);
this.vendorName = vendorName;
this.productId = productId;
this.productName = productName;
this.softwareVersion = softwareVersion ?? parseInt(matterbridge.matterbridgeVersion.split('-')[0].replace(/\D/g, ''));
this.softwareVersionString = softwareVersionString ?? matterbridge.matterbridgeVersion;
this.hardwareVersion = hardwareVersion ?? parseInt(this.matterbridge.systemInformation.nodeVersion.replace(/\D/g, ''));
this.hardwareVersionString = hardwareVersionString ?? this.matterbridge.systemInformation.nodeVersion;
this.initializeEndpoint('');
}
setLogLevel(level) {
this.log.logLevel = level;
}
destroy() {
this.mutableDevices.clear();
this.endpoints.clear();
this.remappedEndpoints.clear();
this.splitEndpoints.clear();
}
size() {
return this.mutableDevices.size;
}
has(endpoint) {
return this.mutableDevices.has(endpoint);
}
name() {
return this.deviceName;
}
getEndpoints() {
return this.endpoints;
}
getRemappedEndpoints() {
return this.remappedEndpoints;
}
getSplitEndpoints() {
return this.splitEndpoints;
}
get(endpoint = '') {
if (this.mutableDevices.get(endpoint) === undefined)
throw new Error(`Device ${endpoint} is not defined`);
return this.mutableDevices.get(endpoint);
}
getEndpoint(endpoint = '') {
if (this.mutableDevices.get(endpoint)?.endpoint === undefined)
throw new Error(`Device ${endpoint} endpoint is not defined`);
return this.mutableDevices.get(endpoint)?.endpoint;
}
initializeEndpoint(endpoint) {
if (!this.mutableDevices.has(endpoint)) {
this.mutableDevices.set(endpoint, {
friendlyName: endpoint,
tagList: [],
deviceTypes: [],
clusterServersIds: [],
clusterServersObjs: [],
clusterClientsIds: [],
clusterClientsObjs: [],
commandHandlers: [],
subscribeHandlers: [],
});
}
return this.mutableDevices.get(endpoint);
}
setFriendlyName(endpoint, friendlyName) {
const device = this.initializeEndpoint(endpoint);
device.friendlyName = friendlyName;
return this;
}
setComposedType(composedType) {
this.composedType = composedType;
return this;
}
setConfigUrl(configUrl) {
this.configUrl = configUrl;
return this;
}
setMode(mode) {
this.mode = mode;
return this;
}
addTagLists(endpoint, ...tagList) {
const device = this.initializeEndpoint(endpoint);
device.tagList.push(...tagList);
return this;
}
addDeviceTypes(endpoint, ...deviceTypes) {
const device = this.initializeEndpoint(endpoint);
device.deviceTypes.push(...deviceTypes);
return this;
}
addClusterServerIds(endpoint, ...clusterServerIds) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersIds.push(...clusterServerIds);
return this;
}
addClusterServerObjs(endpoint, ...clusterServerObj) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(...clusterServerObj);
return this;
}
addCommandHandler(endpoint, command, handler) {
const device = this.initializeEndpoint(endpoint);
device.commandHandlers.push({ endpointName: endpoint, command, handler });
return this;
}
addSubscribeHandler(endpoint, clusterId, attribute, listener) {
const device = this.initializeEndpoint(endpoint);
device.subscribeHandlers.push({ endpointName: endpoint, clusterId, attribute, listener });
return this;
}
addClusterServerBatteryPowerSource(endpoint, batChargeLevel, batPercentRemaining) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(PowerSource.Cluster.id, PowerSourceServer.with(PowerSource.Feature.Battery), {
status: PowerSource.PowerSourceStatus.Active,
order: 0,
description: 'Primary battery',
batReplacementNeeded: false,
batReplaceability: PowerSource.BatReplaceability.Unspecified,
batVoltage: null,
batPercentRemaining,
batChargeLevel,
}));
return this;
}
addClusterServerBooleanState(endpoint, stateValue) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(BooleanState.Cluster.id, BooleanStateServer.enable({
events: { stateChange: true },
}), {
stateValue,
}));
return this;
}
addClusterServerSmokeAlarmSmokeCoAlarm(endpoint, smokeState) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(SmokeCoAlarm.Cluster.id, MatterbridgeSmokeCoAlarmServer.with(SmokeCoAlarm.Feature.SmokeAlarm).enable({
events: {
smokeAlarm: true,
interconnectSmokeAlarm: false,
lowBattery: true,
hardwareFault: true,
endOfService: true,
selfTestComplete: true,
alarmMuted: true,
muteEnded: true,
allClear: true,
},
}), {
smokeState,
expressedState: SmokeCoAlarm.ExpressedState.Normal,
batteryAlert: SmokeCoAlarm.AlarmState.Normal,
deviceMuted: SmokeCoAlarm.MuteState.NotMuted,
testInProgress: false,
hardwareFaultAlert: false,
endOfServiceAlert: SmokeCoAlarm.EndOfService.Normal,
}));
return this;
}
addClusterServerCoAlarmSmokeCoAlarm(endpoint, coState) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(SmokeCoAlarm.Cluster.id, MatterbridgeSmokeCoAlarmServer.with(SmokeCoAlarm.Feature.CoAlarm).enable({
events: {
coAlarm: true,
interconnectCoAlarm: false,
lowBattery: true,
hardwareFault: true,
endOfService: true,
selfTestComplete: true,
alarmMuted: true,
muteEnded: true,
allClear: true,
},
}), {
coState,
expressedState: SmokeCoAlarm.ExpressedState.Normal,
batteryAlert: SmokeCoAlarm.AlarmState.Normal,
deviceMuted: SmokeCoAlarm.MuteState.NotMuted,
testInProgress: false,
hardwareFaultAlert: false,
endOfServiceAlert: SmokeCoAlarm.EndOfService.Normal,
}));
return this;
}
addClusterServerColorTemperatureColorControl(endpoint, colorTempPhysicalMinMireds, colorTempPhysicalMaxMireds) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(ColorControl.Cluster.id, MatterbridgeColorControlServer.with(ColorControl.Feature.ColorTemperature), {
colorMode: ColorControl.ColorMode.ColorTemperatureMireds,
enhancedColorMode: ColorControl.EnhancedColorMode.ColorTemperatureMireds,
colorCapabilities: {
xy: false,
hueSaturation: false,
colorLoop: false,
enhancedHue: false,
colorTemperature: true,
},
options: {
executeIfOff: false,
},
numberOfPrimaries: null,
colorTemperatureMireds: 250,
colorTempPhysicalMinMireds,
colorTempPhysicalMaxMireds,
coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds,
remainingTime: 0,
startUpColorTemperatureMireds: null,
}));
return this;
}
addClusterServerColorControl(endpoint, colorTempPhysicalMinMireds, colorTempPhysicalMaxMireds) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(ColorControl.Cluster.id, MatterbridgeColorControlServer.with(ColorControl.Feature.ColorTemperature, ColorControl.Feature.HueSaturation, ColorControl.Feature.Xy), {
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: 0,
currentY: 0,
currentHue: 0,
currentSaturation: 0,
colorTemperatureMireds: 250,
colorTempPhysicalMinMireds,
colorTempPhysicalMaxMireds,
coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds,
remainingTime: 0,
startUpColorTemperatureMireds: null,
}));
return this;
}
addClusterServerAutoModeThermostat(endpoint, localTemperature, occupiedHeatingSetpoint, occupiedCoolingSetpoint, minSetpointLimit, maxSetpointLimit) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(Thermostat.Cluster.id, MatterbridgeThermostatServer.with(Thermostat.Feature.AutoMode, Thermostat.Feature.Heating, Thermostat.Feature.Cooling), {
localTemperature: isValidNumber(localTemperature) ? localTemperature * 100 : null,
externalMeasuredIndoorTemperature: isValidNumber(localTemperature) ? localTemperature * 100 : undefined,
systemMode: Thermostat.SystemMode.Auto,
controlSequenceOfOperation: Thermostat.ControlSequenceOfOperation.CoolingAndHeating,
occupiedHeatingSetpoint: occupiedHeatingSetpoint * 100,
minHeatSetpointLimit: minSetpointLimit * 100,
absMinHeatSetpointLimit: minSetpointLimit * 100,
maxHeatSetpointLimit: maxSetpointLimit * 100,
absMaxHeatSetpointLimit: maxSetpointLimit * 100,
occupiedCoolingSetpoint: occupiedCoolingSetpoint * 100,
minCoolSetpointLimit: minSetpointLimit * 100,
absMinCoolSetpointLimit: minSetpointLimit * 100,
maxCoolSetpointLimit: maxSetpointLimit * 100,
absMaxCoolSetpointLimit: maxSetpointLimit * 100,
minSetpointDeadBand: 0,
thermostatRunningMode: Thermostat.ThermostatRunningMode.Off,
}));
return this;
}
addClusterServerHeatingThermostat(endpoint, localTemperature, occupiedHeatingSetpoint, minSetpointLimit, maxSetpointLimit) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(Thermostat.Cluster.id, MatterbridgeThermostatServer.with(Thermostat.Feature.Heating), {
localTemperature: isValidNumber(localTemperature) ? localTemperature * 100 : null,
externalMeasuredIndoorTemperature: isValidNumber(localTemperature) ? localTemperature * 100 : undefined,
systemMode: Thermostat.SystemMode.Heat,
controlSequenceOfOperation: Thermostat.ControlSequenceOfOperation.HeatingOnly,
occupiedHeatingSetpoint: occupiedHeatingSetpoint * 100,
minHeatSetpointLimit: minSetpointLimit * 100,
absMinHeatSetpointLimit: minSetpointLimit * 100,
maxHeatSetpointLimit: maxSetpointLimit * 100,
absMaxHeatSetpointLimit: maxSetpointLimit * 100,
}));
return this;
}
addClusterServerCoolingThermostat(endpoint, localTemperature, occupiedCoolingSetpoint, minSetpointLimit, maxSetpointLimit) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(Thermostat.Cluster.id, MatterbridgeThermostatServer.with(Thermostat.Feature.Cooling), {
localTemperature: isValidNumber(localTemperature) ? localTemperature * 100 : null,
externalMeasuredIndoorTemperature: isValidNumber(localTemperature) ? localTemperature * 100 : undefined,
systemMode: Thermostat.SystemMode.Cool,
controlSequenceOfOperation: Thermostat.ControlSequenceOfOperation.CoolingOnly,
occupiedCoolingSetpoint: occupiedCoolingSetpoint * 100,
minCoolSetpointLimit: minSetpointLimit * 100,
absMinCoolSetpointLimit: minSetpointLimit * 100,
maxCoolSetpointLimit: maxSetpointLimit * 100,
absMaxCoolSetpointLimit: maxSetpointLimit * 100,
}));
return this;
}
addClusterServerHeatingCoolingThermostat(endpoint, localTemperature, occupiedHeatingSetpoint, occupiedCoolingSetpoint, minSetpointLimit, maxSetpointLimit) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(Thermostat.Cluster.id, MatterbridgeThermostatServer.with(Thermostat.Feature.Heating, Thermostat.Feature.Cooling), {
localTemperature: isValidNumber(localTemperature) ? localTemperature * 100 : null,
externalMeasuredIndoorTemperature: isValidNumber(localTemperature) ? localTemperature * 100 : undefined,
systemMode: Thermostat.SystemMode.Off,
controlSequenceOfOperation: Thermostat.ControlSequenceOfOperation.CoolingAndHeating,
occupiedHeatingSetpoint: occupiedHeatingSetpoint * 100,
minHeatSetpointLimit: minSetpointLimit * 100,
absMinHeatSetpointLimit: minSetpointLimit * 100,
maxHeatSetpointLimit: maxSetpointLimit * 100,
absMaxHeatSetpointLimit: maxSetpointLimit * 100,
occupiedCoolingSetpoint: occupiedCoolingSetpoint * 100,
minCoolSetpointLimit: minSetpointLimit * 100,
absMinCoolSetpointLimit: minSetpointLimit * 100,
maxCoolSetpointLimit: maxSetpointLimit * 100,
absMaxCoolSetpointLimit: maxSetpointLimit * 100,
}));
return this;
}
addClusterServerCompleteFanControl(endpoint, fanMode = FanControl.FanMode.Off, fanModeSequence = FanControl.FanModeSequence.OffLowMedHighAuto, percentSetting = 0, percentCurrent = 0, rockSupport = { rockLeftRight: false, rockUpDown: false, rockRound: true }, rockSetting = { rockLeftRight: false, rockUpDown: false, rockRound: true }, airflowDirection = FanControl.AirflowDirection.Forward) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(FanControl.Cluster.id, MatterbridgeFanControlServer.with(FanControl.Feature.Auto, FanControl.Feature.Step, FanControl.Feature.Rocking, FanControl.Feature.AirflowDirection), {
fanMode,
fanModeSequence,
percentSetting,
percentCurrent,
rockSupport,
rockSetting,
airflowDirection,
}));
return this;
}
addVacuum(endpoint) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(RvcRunMode.Cluster.id, MatterbridgeRvcRunModeServer, {
supportedModes: [
{ label: 'Idle', mode: 1, modeTags: [{ value: RvcRunMode.ModeTag.Idle }] },
{ label: 'Cleaning', mode: 2, modeTags: [{ value: RvcRunMode.ModeTag.Cleaning }] },
],
currentMode: 1,
}));
device.clusterServersObjs.push(getClusterServerObj(RvcCleanMode.Cluster.id, MatterbridgeRvcCleanModeServer, {
supportedModes: [{ label: 'Vacuum', mode: 1, modeTags: [{ value: RvcCleanMode.ModeTag.Vacuum }] }],
currentMode: 1,
}));
device.clusterServersObjs.push(getClusterServerObj(RvcOperationalState.Cluster.id, MatterbridgeRvcOperationalStateServer, {
operationalStateList: [
{ operationalStateId: RvcOperationalState.OperationalState.Stopped },
{ operationalStateId: RvcOperationalState.OperationalState.Running },
{ operationalStateId: RvcOperationalState.OperationalState.Paused },
{ operationalStateId: RvcOperationalState.OperationalState.Error },
{ operationalStateId: RvcOperationalState.OperationalState.SeekingCharger },
{ operationalStateId: RvcOperationalState.OperationalState.Charging },
{ operationalStateId: RvcOperationalState.OperationalState.Docked },
],
operationalState: RvcOperationalState.OperationalState.Docked,
operationalError: { errorStateId: RvcOperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' },
}));
return this;
}
addSelect(endpoint, name, items) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(ModeSelect.Cluster.id, MatterbridgeModeSelectServer, {
description: name,
supportedModes: items.map((item, index) => ({ label: item, mode: index + 1, semanticTags: [] })),
currentMode: 1,
}));
return this;
}
addOnOff(endpoint, onOff) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(OnOff.Cluster.id, MatterbridgeOnOffServer.with(), {
onOff,
}));
return this;
}
addBasicVideoPlayer(endpoint) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(MediaPlayback.Cluster.id, MatterbridgeMediaPlaybackServer.enable({
commands: { next: true, previous: true, skipForward: true, skipBackward: true },
}), {
currentState: MediaPlayback.PlaybackState.NotPlaying,
}));
return this;
}
addKeypadInput(endpoint) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(getClusterServerObj(KeypadInput.Cluster.id, MatterbridgeKeypadInputServer, {}));
return this;
}
addBasicInformationClusterServer() {
const device = this.getEndpoint('');
device.log.logName = this.deviceName;
device.deviceName = this.deviceName;
device.serialNumber = this.serialNumber;
device.uniqueId = this.createUniqueId(this.deviceName, this.serialNumber, this.vendorName, this.productName);
device.productId = this.productId;
device.productName = this.productName;
device.vendorId = this.vendorId;
device.vendorName = this.vendorName;
device.softwareVersion = this.softwareVersion;
device.softwareVersionString = this.softwareVersionString;
device.hardwareVersion = this.hardwareVersion;
device.hardwareVersionString = this.hardwareVersionString;
return this;
}
addBridgedDeviceBasicInformationClusterServer() {
const device = this.getEndpoint('');
device.log.logName = this.deviceName;
device.deviceName = this.deviceName;
device.serialNumber = this.serialNumber;
device.uniqueId = this.createUniqueId(this.deviceName, this.serialNumber, this.vendorName, this.productName);
device.productId = undefined;
device.productName = this.productName;
device.vendorId = this.vendorId;
device.vendorName = this.vendorName;
device.softwareVersion = this.softwareVersion;
device.softwareVersionString = this.softwareVersionString;
device.hardwareVersion = this.hardwareVersion;
device.hardwareVersionString = this.hardwareVersionString;
this.addClusterServerObjs('', getClusterServerObj(BridgedDeviceBasicInformation.Cluster.id, BridgedDeviceBasicInformationServer, {
vendorId: this.vendorId,
vendorName: this.vendorName.slice(0, 32),
productName: this.productName.slice(0, 32),
productLabel: this.deviceName.slice(0, 64),
nodeLabel: this.deviceName.slice(0, 32),
serialNumber: this.serialNumber.slice(0, 32),
uniqueId: this.createUniqueId(this.deviceName, this.serialNumber, this.vendorName, this.productName),
softwareVersion: isValidNumber(this.softwareVersion, 0, UINT32_MAX) ? this.softwareVersion : undefined,
softwareVersionString: isValidString(this.softwareVersionString) ? this.softwareVersionString.slice(0, 64) : undefined,
hardwareVersion: isValidNumber(this.hardwareVersion, 0, UINT16_MAX) ? this.hardwareVersion : undefined,
hardwareVersionString: isValidString(this.hardwareVersionString) ? this.hardwareVersionString.slice(0, 64) : undefined,
reachable: true,
}));
return this;
}
createUniqueId(param1, param2, param3, param4) {
const hash = createHash('md5');
hash.update(param1 + param2 + param3 + param4);
return hash.digest('hex');
}
create(remap = false) {
this.removeDuplicatedAndSupersetDeviceTypes();
if (remap) {
for (const [_endpoint, device] of Array.from(this.mutableDevices.entries()).filter(([endpoint]) => endpoint !== '')) {
device.deviceTypes.forEach((deviceType) => {
deviceType.requiredServerClusters.forEach((clusterId) => {
device.clusterServersIds.push(clusterId);
});
});
}
}
this.removeDuplicatedClusterServers();
if (remap) {
for (const [endpoint, device] of Array.from(this.mutableDevices.entries()).filter(([endpoint]) => endpoint !== '')) {
let remapEndpoint = true;
for (const deviceType of device.deviceTypes) {
const duplicatedDeviceTypes = Array.from(this.mutableDevices.entries())
.filter(([e, _d]) => e !== endpoint)
.find(([_e, d]) => d.deviceTypes.includes(deviceType));
if (duplicatedDeviceTypes) {
remapEndpoint = false;
}
}
for (const clusterServerId of device.clusterServersIds) {
const duplicatedClusterServersIds = Array.from(this.mutableDevices.entries())
.filter(([e, _d]) => e !== endpoint)
.find(([_e, d]) => d.clusterServersIds.includes(clusterServerId) || d.clusterServersObjs.find((obj) => obj.id === clusterServerId));
if (duplicatedClusterServersIds && clusterServerId !== Identify.Cluster.id && clusterServerId !== Groups.Cluster.id) {
remapEndpoint = false;
}
}
for (const clusterServerObjs of device.clusterServersObjs) {
const duplicatedClusterServersObjs = Array.from(this.mutableDevices.entries())
.filter(([e, _d]) => e !== endpoint)
.find(([_e, d]) => d.clusterServersIds.includes(clusterServerObjs.id) || d.clusterServersObjs.find((obj) => obj.id === clusterServerObjs.id));
if (duplicatedClusterServersObjs && clusterServerObjs.id !== Identify.Cluster.id && clusterServerObjs.id !== Groups.Cluster.id) {
remapEndpoint = false;
}
}
if (remapEndpoint) {
const mainDevice = this.get('');
mainDevice.deviceTypes.push(...device.deviceTypes);
mainDevice.clusterServersIds.push(...device.clusterServersIds);
mainDevice.clusterServersObjs.push(...device.clusterServersObjs);
mainDevice.clusterClientsIds.push(...device.clusterClientsIds);
mainDevice.clusterClientsObjs.push(...device.clusterClientsObjs);
mainDevice.commandHandlers.push(...device.commandHandlers);
mainDevice.subscribeHandlers.push(...device.subscribeHandlers);
this.mutableDevices.delete(endpoint);
this.remappedEndpoints.add(endpoint);
this.log.debug(`*Remapped endpoint ${endpoint} of ${this.deviceName}`);
}
else {
this.splitEndpoints.add(endpoint);
this.log.debug(`***Failed to remap endpoint ${endpoint} of ${this.deviceName}`);
}
}
}
this.createMainEndpoint();
this.createChildEndpoints();
for (const [endpoint] of this.mutableDevices) {
this.createClusters(endpoint);
}
return this.getEndpoint();
}
removeDuplicatedAndSupersetDeviceTypes() {
for (const device of this.mutableDevices.values()) {
const deviceTypesMap = new Map();
device.deviceTypes.forEach((deviceType) => {
deviceTypesMap.set(deviceType.code, deviceType);
});
if (deviceTypesMap.has(onOffSwitch.code) && deviceTypesMap.has(dimmableSwitch.code))
deviceTypesMap.delete(onOffSwitch.code);
if (deviceTypesMap.has(onOffSwitch.code) && deviceTypesMap.has(colorTemperatureSwitch.code))
deviceTypesMap.delete(onOffSwitch.code);
if (deviceTypesMap.has(dimmableSwitch.code) && deviceTypesMap.has(colorTemperatureSwitch.code))
deviceTypesMap.delete(dimmableSwitch.code);
if (deviceTypesMap.has(onOffOutlet.code) && deviceTypesMap.has(dimmableOutlet.code))
deviceTypesMap.delete(onOffOutlet.code);
if (deviceTypesMap.has(onOffLight.code) && deviceTypesMap.has(dimmableLight.code))
deviceTypesMap.delete(onOffLight.code);
if (deviceTypesMap.has(onOffLight.code) && deviceTypesMap.has(colorTemperatureLight.code))
deviceTypesMap.delete(onOffLight.code);
if (deviceTypesMap.has(onOffLight.code) && deviceTypesMap.has(extendedColorLight.code))
deviceTypesMap.delete(onOffLight.code);
if (deviceTypesMap.has(dimmableLight.code) && deviceTypesMap.has(colorTemperatureLight.code))
deviceTypesMap.delete(dimmableLight.code);
if (deviceTypesMap.has(dimmableLight.code) && deviceTypesMap.has(extendedColorLight.code))
deviceTypesMap.delete(dimmableLight.code);
if (deviceTypesMap.has(colorTemperatureLight.code) && deviceTypesMap.has(extendedColorLight.code))
deviceTypesMap.delete(colorTemperatureLight.code);
device.deviceTypes = Array.from(deviceTypesMap.values());
}
return this;
}
createMainEndpoint() {
this.removeDuplicatedAndSupersetDeviceTypes();
const mainDevice = this.mutableDevices.get('');
if (this.mode === 'server') {
mainDevice.deviceTypes = mainDevice.deviceTypes.filter((deviceType) => deviceType.code !== bridgedNode.code);
}
mainDevice.friendlyName = this.deviceName;
mainDevice.endpoint = new MatterbridgeEndpoint(mainDevice.deviceTypes, { id: this.deviceName, mode: this.mode });
mainDevice.endpoint.log.logName = this.deviceName;
this.endpoints.set('', mainDevice.endpoint);
return mainDevice.endpoint;
}
createChildEndpoints() {
this.removeDuplicatedAndSupersetDeviceTypes();
const mainDevice = this.mutableDevices.get('');
if (!mainDevice.endpoint)
throw new Error('Main endpoint is not defined. Call createMainEndpoint() first.');
for (const [endpoint, device] of Array.from(this.mutableDevices.entries()).filter(([endpoint]) => endpoint !== '')) {
device.endpoint = mainDevice.endpoint.addChildDeviceType(endpoint, device.deviceTypes, device.tagList.length ? { tagList: device.tagList } : {});
device.endpoint.log.logName = device.friendlyName;
this.endpoints.set(endpoint, device.endpoint);
}
return this;
}
removeDuplicatedClusterServers() {
for (const device of this.mutableDevices.values()) {
const deviceClusterServersIdMap = new Map();
device.clusterServersIds.forEach((clusterServerId) => {
deviceClusterServersIdMap.set(clusterServerId, clusterServerId);
});
const deviceClusterServersObjMap = new Map();
device.clusterServersObjs.forEach((clusterServerObj) => {
deviceClusterServersIdMap.delete(clusterServerObj.id);
deviceClusterServersObjMap.set(clusterServerObj.id, clusterServerObj);
});
device.clusterServersIds = Array.from(deviceClusterServersIdMap.values());
device.clusterServersObjs = Array.from(deviceClusterServersObjMap.values());
}
return this;
}
createClusters(endpoint) {
this.removeDuplicatedClusterServers();
if (endpoint === '') {
const mainDevice = this.get(endpoint);
if (!mainDevice.endpoint)
throw new Error('Main endpoint is not defined');
if (this.mode === 'server')
this.addBasicInformationClusterServer();
else
this.addBridgedDeviceBasicInformationClusterServer();
for (const clusterServerObj of mainDevice.clusterServersObjs) {
mainDevice.endpoint.behaviors.require(clusterServerObj.type, clusterServerObj.options);
}
mainDevice.endpoint.addClusterServers(mainDevice.clusterServersIds);
mainDevice.endpoint.addRequiredClusterServers();
if (this.composedType)
void mainDevice.endpoint.addFixedLabel('composed', this.composedType).catch(() => { });
if (this.configUrl)
mainDevice.endpoint.configUrl = this.configUrl;
for (const commandHandler of mainDevice.commandHandlers) {
mainDevice.endpoint.addCommandHandler(commandHandler.command, async (data) => {
await commandHandler.handler(data, commandHandler.endpointName, commandHandler.command);
});
}
for (const subscribeHandler of mainDevice.subscribeHandlers) {
if (mainDevice.endpoint.hasAttributeServer(subscribeHandler.clusterId, subscribeHandler.attribute))
void mainDevice.endpoint
.subscribeAttribute(subscribeHandler.clusterId, subscribeHandler.attribute, (newValue, oldValue, context) => {
subscribeHandler.listener(newValue, oldValue, context, subscribeHandler.endpointName, subscribeHandler.clusterId, subscribeHandler.attribute);
}, mainDevice.endpoint.log)
.catch(() => { });
}
return this;
}
const device = this.get(endpoint);
if (!device.endpoint)
throw new Error('Child endpoint is not defined');
for (const clusterServerObj of device.clusterServersObjs) {
device.endpoint.behaviors.require(clusterServerObj.type, clusterServerObj.options);
}
device.endpoint.addClusterServers(device.clusterServersIds);
device.endpoint.addRequiredClusterServers();
for (const commandHandler of device.commandHandlers) {
device.endpoint.addCommandHandler(commandHandler.command, (data) => {
void commandHandler.handler(data, commandHandler.endpointName, commandHandler.command);
});
}
for (const subscribeHandler of device.subscribeHandlers) {
if (device.endpoint.hasAttributeServer(subscribeHandler.clusterId, subscribeHandler.attribute))
void device.endpoint
.subscribeAttribute(subscribeHandler.clusterId, subscribeHandler.attribute, (newValue, oldValue, context) => {
subscribeHandler.listener(newValue, oldValue, context, subscribeHandler.endpointName, subscribeHandler.clusterId, subscribeHandler.attribute);
}, device.endpoint.log)
.catch(() => { });
}
return this;
}
logMutableDevice() {
this.log.debug(`Device ${idn}${this.deviceName}${rs}${db} serial number ${CYAN}${this.serialNumber}${rs}${db} vendor id ${CYAN}${this.vendorId}${rs}${db} ` +
`vendor name ${CYAN}${this.vendorName}${rs}${db} product name ${CYAN}${this.productName}${rs}${db} software version ${CYAN}${this.softwareVersion}${rs}${db} ` +
`software version string ${CYAN}${this.softwareVersionString}${rs}${db} hardware version ${CYAN}${this.hardwareVersion}${rs}${db} hardware version string ${CYAN}${this.hardwareVersionString}`);
for (const [endpoint, device] of this.mutableDevices) {
const deviceTypes = device.deviceTypes.map((d) => '0x' + d.code.toString(16) + '-' + d.name);
const clusterServersIds = device.clusterServersIds.map((clusterServerId) => '0x' + clusterServerId.toString(16) + '-' + getClusterNameById(clusterServerId));
const clusterServersObjsIds = device.clusterServersObjs.map((clusterServerObj) => '0x' + clusterServerObj.id.toString(16) + '-' + getClusterNameById(clusterServerObj.id));
this.log.debug(`- endpoint: ${ign}${endpoint === '' ? 'main' : endpoint}${rs}${db} => friendlyName ${CYAN}${device.friendlyName}${db} ` +
`${db}tagList: ${debugStringify(device.tagList)}${db} deviceTypes: ${debugStringify(deviceTypes)}${db} ` +
`clusterServersIds: ${debugStringify(clusterServersIds)}${db} clusterServersObjs: ${debugStringify(clusterServersObjsIds)}${db} ` +
`commandHandlers: ${debugStringify(device.commandHandlers)}${db} subscribeHandlers: ${debugStringify(device.subscribeHandlers)}${db}`);
}
return this;
}
}