UNPKG

matterbridge-hass

Version:
677 lines (676 loc) 36.8 kB
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; } }