UNPKG

matterbridge-zigbee2mqtt

Version:
540 lines (539 loc) 30.6 kB
import path from 'node:path'; import { addVirtualDevice, MatterbridgeDynamicPlatform } from 'matterbridge'; import { dn, gn, db, wr, zb, payloadStringify, rs, debugStringify, CYAN, er, nf } from 'matterbridge/logger'; import { isValidNumber, isValidString, waiter } from 'matterbridge/utils'; import { BridgedDeviceBasicInformation, DoorLock } from 'matterbridge/matter/clusters'; import { ZigbeeDevice, ZigbeeGroup } from './entity.js'; import { Zigbee2MQTT } from './zigbee2mqtt.js'; export class ZigbeePlatform extends MatterbridgeDynamicPlatform { bridgedDevices = []; zigbeeEntities = []; namePostfix = 1; connectTimeout = 30000; availabilityTimeout = 10000; injectTimer; mqttHost = 'mqtt://localhost'; mqttPort = 1883; mqttTopic = 'zigbee2mqtt'; mqttUsername = undefined; mqttPassword = undefined; mqttProtocol = 5; lightList = []; outletList = []; switchList = []; featureBlackList = []; deviceFeatureBlackList = {}; postfix = ''; debugEnabled; shouldStart; shouldConfigure; z2m; z2mDevicesRegistered = false; z2mGroupsRegistered = false; z2mBridgeOnline; z2mBridgeInfo; z2mBridgeDevices; z2mBridgeGroups; z2mEntityAvailability = new Map(); z2mEntityPayload = new Map(); availabilityTimer; constructor(matterbridge, log, config) { super(matterbridge, log, config); if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.0.4')) { throw new Error(`This plugin requires Matterbridge version >= "3.0.4". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend."`); } this.debugEnabled = config.debug; this.shouldStart = false; this.shouldConfigure = false; if (config.host && typeof config.host === 'string') { this.mqttHost = config.host; this.mqttHost = !this.mqttHost.startsWith('mqtt://') && !this.mqttHost.startsWith('mqtts://') ? 'mqtt://' + this.mqttHost : this.mqttHost; } if (config.port) this.mqttPort = config.port; if (config.topic) this.mqttTopic = config.topic; if (config.username) this.mqttUsername = config.username; if (config.password) this.mqttPassword = config.password; if (config.protocolVersion && typeof config.protocolVersion === 'number' && config.protocolVersion >= 3 && config.protocolVersion <= 5) { this.mqttProtocol = config.protocolVersion; } else { this.mqttProtocol = 5; } if (config.switchList) this.switchList = config.switchList; if (config.lightList) this.lightList = config.lightList; if (config.outletList) this.outletList = config.outletList; if (config.featureBlackList) this.featureBlackList = config.featureBlackList; if (config.deviceFeatureBlackList) this.deviceFeatureBlackList = config.deviceFeatureBlackList; if (config.postfix && typeof config.postfix === 'string') { this.postfix = config.postfix; } this.postfix = this.postfix.trim().slice(0, 3); config.host = this.mqttHost; config.port = this.mqttPort; config.protocolVersion = this.mqttProtocol; config.topic = this.mqttTopic; config.username = this.mqttUsername; config.password = this.mqttPassword; config.postfix = this.postfix; if (config.postfixHostname !== undefined) delete config.postfixHostname; if (config.deviceScenes !== undefined) delete config.deviceScenes; if (config.groupScenes !== undefined) delete config.groupScenes; if (config.scenesType === undefined) config.scenesType = 'outlet'; if (config.scenesPrefix === undefined) config.scenesPrefix = true; this.log.info(`Initializing platform: ${CYAN}${this.config.name}${nf} version: ${CYAN}${this.config.version}${rs}`); this.log.info(`Loaded zigbee2mqtt parameters from ${CYAN}${path.join(matterbridge.matterbridgeDirectory, 'matterbridge-zigbee2mqtt.config.json')}${rs}`); this.z2m = new Zigbee2MQTT(this.mqttHost, this.mqttPort, this.mqttTopic, this.mqttUsername, this.mqttPassword, this.mqttProtocol, this.config.ca, this.config.rejectUnauthorized, this.config.cert, this.config.key, this.debugEnabled); this.z2m.setLogDebug(this.debugEnabled); this.z2m.setDataPath(path.join(matterbridge.matterbridgePluginDirectory, 'matterbridge-zigbee2mqtt')); if (isValidString(this.mqttHost) && isValidNumber(this.mqttPort, 1, 65535)) { this.log.info(`Connecting to MQTT broker: ${this.mqttHost + ':' + this.mqttPort.toString()}`); this.z2m.start(); } else { this.log.error(`Invalid MQTT broker host: ${this.mqttHost} or port: ${this.mqttPort}`); } this.z2m.on('mqtt_connect', () => { this.log.info(`MQTT broker at ${this.z2m.mqttHost}:${this.z2m.mqttPort} connected`); this.z2m.subscribe(this.z2m.mqttTopic + '/#'); }); this.z2m.on('mqtt_subscribed', () => { this.log.info(`MQTT broker at ${this.z2m.mqttHost}:${this.z2m.mqttPort} subscribed to: ${this.z2m.mqttTopic + '/#'}`); }); this.z2m.on('close', () => { this.log.warn(`MQTT broker at ${this.z2m.mqttHost}:${this.z2m.mqttPort} closed the connection`); }); this.z2m.on('end', () => { this.log.warn(`MQTT broker at ${this.z2m.mqttHost}:${this.z2m.mqttPort} ended the connection`); }); this.z2m.on('mqtt_error', (error) => { this.log.error(`MQTT broker at ${this.z2m.mqttHost}:${this.z2m.mqttPort} error:`, error); }); this.z2m.on('online', () => { this.log.info('zigbee2MQTT is online'); this.z2mBridgeOnline = true; this.updateAvailability(true); }); this.z2m.on('offline', () => { this.log.warn('zigbee2MQTT is offline'); this.z2mBridgeOnline = false; this.updateAvailability(false); }); this.z2m.on('bridge-info', async (bridgeInfo) => { if (bridgeInfo === null || bridgeInfo === undefined) return; this.z2mBridgeInfo = bridgeInfo; this.log.info(`zigbee2MQTT version ${this.z2mBridgeInfo.version} zh version ${this.z2mBridgeInfo.zigbee_herdsman.version} zhc version ${this.z2mBridgeInfo.zigbee_herdsman_converters.version}`); if (this.z2mBridgeInfo.config.advanced.output === 'attribute') this.log.error(`zigbee2MQTT advanced.output must be 'json' or 'attribute_and_json'. Now is ${this.z2mBridgeInfo.config.advanced.output}`); if (this.z2mBridgeInfo.config.advanced.legacy_api === true) this.log.info(`zigbee2MQTT advanced.legacy_api is ${this.z2mBridgeInfo.config.advanced.legacy_api}`); if (this.z2mBridgeInfo.config.advanced.legacy_availability_payload === true) this.log.info(`zigbee2MQTT advanced.legacy_availability_payload is ${this.z2mBridgeInfo.config.advanced.legacy_availability_payload}`); }); this.z2m.on('bridge-devices', async (devices) => { if (devices === null || devices === undefined) return; this.log.info(`zigbee2MQTT sent ${devices.length} devices ${this.z2mDevicesRegistered ? 'already registered' : ''}`); if (config.injectDevices) { this.log.warn(`***Injecting virtual devices from ${path.join(matterbridge.matterbridgeDirectory, config.injectDevices)}`); const data = this.z2m.readConfig(path.join(matterbridge.matterbridgeDirectory, config.injectDevices)); this.log.warn(`***Injecting ${data.devices.length} devices from ${config.injectDevices}`); this.z2mBridgeDevices = [devices, data.devices].flat(); } else this.z2mBridgeDevices = devices; if (this.shouldStart) { if (!this.z2mDevicesRegistered && this.z2mBridgeDevices) { for (const device of this.z2mBridgeDevices) { await this.registerZigbeeDevice(device); } this.z2mDevicesRegistered = true; } } if (this.shouldConfigure) { this.log.info(`Configuring ${this.zigbeeEntities.length} zigbee entities.`); for (const bridgedEntity of this.zigbeeEntities) { if (bridgedEntity.isDevice && bridgedEntity.device) await this.requestDeviceUpdate(bridgedEntity.device); bridgedEntity.configure(); } } }); this.z2m.on('bridge-groups', async (groups) => { if (groups === null || groups === undefined) return; this.log.info(`zigbee2MQTT sent ${groups.length} groups ${this.z2mGroupsRegistered ? 'already registered' : ''}`); this.z2mBridgeGroups = groups; if (this.shouldStart) { if (!this.z2mGroupsRegistered && this.z2mBridgeGroups) { for (const group of this.z2mBridgeGroups) { await this.registerZigbeeGroup(group); } this.z2mGroupsRegistered = true; } } if (this.shouldConfigure) { this.log.info(`Configuring ${this.zigbeeEntities.length} zigbee entities.`); for (const bridgedEntity of this.zigbeeEntities) { if (bridgedEntity.isGroup && bridgedEntity.group) await this.requestGroupUpdate(bridgedEntity.group); bridgedEntity.configure(); } } }); this.z2m.on('availability', (device, available) => { this.z2mEntityAvailability.set(device, available); if (available) this.log.info(`zigbee2MQTT entity ${device} is ${available ? 'online' : 'offline'}`); else this.log.warn(`zigbee2MQTT entity ${device} is ${available ? 'online' : 'offline'}`); }); this.z2m.on('message', (device, payload) => { this.z2mEntityPayload.set(device, payload); }); this.z2m.on('permit_join', async (device, time, status) => { this.log.info(`zigbee2MQTT sent permit_join device: ${device} time: ${time} status: ${status}`); for (const zigbeeEntity of this.zigbeeEntities) { if (zigbeeEntity.isRouter && (device === undefined || device === zigbeeEntity.bridgedDevice?.deviceName)) { this.log.info(`*- ${zigbeeEntity.bridgedDevice?.deviceName} ${zigbeeEntity.bridgedDevice?.number} (${zigbeeEntity.bridgedDevice?.name})`); if (zigbeeEntity.device && status) { zigbeeEntity.bridgedDevice?.setAttribute(DoorLock.Cluster.id, 'lockState', DoorLock.LockState.Unlocked, this.log); zigbeeEntity.bridgedDevice?.triggerEvent(DoorLock.Cluster.id, 'lockOperation', { lockOperationType: DoorLock.LockOperationType.Unlock, operationSource: DoorLock.OperationSource.Manual, userIndex: null, fabricIndex: null, sourceNode: null }, this.log); this.log.info(`Device ${zigbeeEntity.entityName} unlocked`); } if (zigbeeEntity.device && !status) { zigbeeEntity.bridgedDevice?.setAttribute(DoorLock.Cluster.id, 'lockState', DoorLock.LockState.Locked, this.log); zigbeeEntity.bridgedDevice?.triggerEvent(DoorLock.Cluster.id, 'lockOperation', { lockOperationType: DoorLock.LockOperationType.Lock, operationSource: DoorLock.OperationSource.Manual, userIndex: null, fabricIndex: null, sourceNode: null }, this.log); this.log.info(`Device ${zigbeeEntity.entityName} locked`); } } } }); this.z2m.on('device_joined', async (friendly_name, ieee_address) => { this.log.info(`zigbee2MQTT sent device_joined device: ${friendly_name} ieee_address: ${ieee_address}`); }); this.z2m.on('device_announce', async (friendly_name, ieee_address) => { this.log.info(`zigbee2MQTT sent device_announce device: ${friendly_name} ieee_address: ${ieee_address}`); }); this.z2m.on('device_leave', async (friendly_name, ieee_address) => { this.log.info(`zigbee2MQTT sent device_leave device: ${friendly_name} ieee_address: ${ieee_address}`); await this.unregisterZigbeeEntity(friendly_name); }); this.z2m.on('device_remove', async (friendly_name, status, block, force) => { this.log.info(`zigbee2MQTT sent device_remove device: ${friendly_name} status: ${status} block: ${block} force: ${force}`); if (status === 'ok') await this.unregisterZigbeeEntity(friendly_name); }); this.z2m.on('device_interview', async (friendly_name, ieee_address, status, supported) => { this.log.info(`zigbee2MQTT sent device_interview device: ${friendly_name} ieee_address: ${ieee_address} status: ${status} supported: ${supported}`); if (status === 'successful' && supported) { if (!this.validateDevice(friendly_name)) return; this.log.info(`Registering device: ${friendly_name}`); const bridgedDevice = this.z2mBridgeDevices?.find((device) => device.friendly_name === friendly_name); if (bridgedDevice) await this.registerZigbeeDevice(bridgedDevice); } }); this.z2m.on('device_rename', async (ieee_address, from, to) => { this.log.info(`zigbee2MQTT sent device_rename ieee_address: ${ieee_address} from: ${from} to: ${to}`); await this.unregisterZigbeeEntity(from); const bridgedDevice = this.z2mBridgeDevices?.find((device) => device.ieee_address === ieee_address); if (bridgedDevice) await this.registerZigbeeDevice(bridgedDevice); }); this.z2m.on('device_options', async (ieee_address, status, from, to) => { this.log.info(`zigbee2MQTT sent device_options ieee_address: ${ieee_address} status ${status} from: ${debugStringify(from)} to: ${debugStringify(to)}`); }); this.z2m.on('group_add', async (friendly_name, id, status) => { this.log.info(`zigbee2MQTT sent group_add friendly_name: ${friendly_name} id ${id} status ${status}`); if (!this.validateDevice(friendly_name)) return; this.log.info(`Registering group: ${friendly_name}`); const bridgedGroup = this.z2mBridgeGroups?.find((group) => group.friendly_name === friendly_name); if (bridgedGroup) await this.registerZigbeeGroup(bridgedGroup); }); this.z2m.on('group_remove', async (friendly_name, status) => { this.log.info(`zigbee2MQTT sent group_remove friendly_name: ${friendly_name} status ${status}`); if (status === 'ok') await this.unregisterZigbeeEntity(friendly_name); }); this.z2m.on('group_rename', async (from, to, status) => { this.log.info(`zigbee2MQTT sent group_rename from: ${from} to ${to} status ${status}`); if (status === 'ok') { await this.unregisterZigbeeEntity(from); const bridgedGroup = this.z2mBridgeGroups?.find((group) => group.friendly_name === to); if (bridgedGroup) await this.registerZigbeeGroup(bridgedGroup); } }); this.z2m.on('group_add_member', async (group_friendly_name, device_ieee_address, status) => { this.log.info(`zigbee2MQTT sent group_add_member group ${group_friendly_name} add device ieee_address ${device_ieee_address} status ${status}`); if (status === 'ok') { await this.unregisterZigbeeEntity(group_friendly_name); const bridgedGroup = this.z2mBridgeGroups?.find((group) => group.friendly_name === group_friendly_name); if (bridgedGroup) await this.registerZigbeeGroup(bridgedGroup); } }); this.z2m.on('group_remove_member', async (group_friendly_name, device_friendly_name, status) => { this.log.info(`zigbee2MQTT sent group_remove_member group ${group_friendly_name} remove device friendly_name ${device_friendly_name} status ${status}`); if (status === 'ok') { await this.unregisterZigbeeEntity(group_friendly_name); const bridgedGroup = this.z2mBridgeGroups?.find((group) => group.friendly_name === group_friendly_name); if (bridgedGroup) await this.registerZigbeeGroup(bridgedGroup); } }); this.log.debug('Created zigbee2mqtt dynamic platform'); } async onStart(reason) { this.log.info(`Starting zigbee2mqtt dynamic platform v${this.version}: ` + reason); await this.ready; await this.clearSelect(); this.setSelectEntity('scenes', 'Scenes', 'component'); await waiter('zigbee2mqtt', () => this.z2mBridgeDevices !== undefined && this.z2mBridgeGroups !== undefined && (this.z2mBridgeOnline !== undefined || this.z2mBridgeInfo !== undefined), false, this.connectTimeout, 1000, true); if (this.z2mBridgeOnline === undefined) this.log.error('The plugin did not receive zigbee2mqtt bridge state. Check if zigbee2mqtt is running and connected to the MQTT broker.'); if (this.z2mBridgeInfo === undefined) this.log.error('The plugin did not receive zigbee2mqtt bridge info. Check if zigbee2mqtt is running and connected to the MQTT broker.'); if (this.z2mBridgeDevices === undefined && this.z2mBridgeGroups === undefined) this.log.error('The plugin did not receive zigbee2mqtt bridge devices/groups. Check if zigbee2mqtt is running and connected to the MQTT broker.'); if (this.z2mBridgeOnline === undefined || this.z2mBridgeInfo === undefined || (this.z2mBridgeDevices === undefined && this.z2mBridgeGroups === undefined)) { throw new Error('The plugin did not receive zigbee2mqtt bridge state or info or devices/groups. Check if zigbee2mqtt is running and connected to the MQTT broker.'); } if (!this.z2mDevicesRegistered && this.z2mBridgeDevices) { this.log.info(`Registering ${this.z2mBridgeDevices.length} devices`); for (const device of this.z2mBridgeDevices) { await this.registerZigbeeDevice(device); } this.z2mDevicesRegistered = true; } if (!this.z2mGroupsRegistered && this.z2mBridgeGroups) { this.log.info(`Registering ${this.z2mBridgeGroups.length} groups`); for (const group of this.z2mBridgeGroups) { await this.registerZigbeeGroup(group); } this.z2mGroupsRegistered = true; } this.log.info(`Started zigbee2mqtt dynamic platform v${this.version}: ` + reason); } async onConfigure() { await super.onConfigure(); this.log.info(`Configuring ${this.zigbeeEntities.length} zigbee entities.`); for (const bridgedEntity of this.zigbeeEntities) { await bridgedEntity.configure(); if (bridgedEntity.isRouter && bridgedEntity.bridgedDevice) { this.log.info(`Configuring router ${bridgedEntity.bridgedDevice.deviceName}.`); if (this.z2mBridgeInfo?.permit_join) { bridgedEntity.bridgedDevice.setAttribute(DoorLock.Cluster.id, 'lockState', DoorLock.LockState.Unlocked, this.log); if (bridgedEntity.bridgedDevice.maybeNumber) bridgedEntity.bridgedDevice.triggerEvent(DoorLock.Cluster.id, 'lockOperation', { lockOperationType: DoorLock.LockOperationType.Unlock, operationSource: DoorLock.OperationSource.Manual, userIndex: null, fabricIndex: null, sourceNode: null }, this.log); } else { bridgedEntity.bridgedDevice.setAttribute(DoorLock.Cluster.id, 'lockState', DoorLock.LockState.Locked, this.log); if (bridgedEntity.bridgedDevice.maybeNumber) bridgedEntity.bridgedDevice.triggerEvent(DoorLock.Cluster.id, 'lockOperation', { lockOperationType: DoorLock.LockOperationType.Lock, operationSource: DoorLock.OperationSource.Manual, userIndex: null, fabricIndex: null, sourceNode: null }, this.log); } } if (bridgedEntity.isDevice && bridgedEntity.device) await this.requestDeviceUpdate(bridgedEntity.device); if (bridgedEntity.isGroup && bridgedEntity.group) await this.requestGroupUpdate(bridgedEntity.group); } this.availabilityTimer = setTimeout(() => { this.log.info(`Setting availability for ${this.z2mEntityAvailability.size} entities`); for (const [entity, available] of this.z2mEntityAvailability) { if (available) this.z2m.emit('ONLINE-' + entity); else this.z2m.emit('OFFLINE-' + entity); } this.log.info(`Setting retained values for ${this.z2mEntityPayload.size} entities`); for (const [entity, payload] of this.z2mEntityPayload) { this.z2m.emit('MESSAGE-' + entity, payload); } }, this.availabilityTimeout).unref(); if (this.config.injectPayloads) { this.injectTimer = setInterval(() => { const data = this.z2m.readConfig(path.join(this.matterbridge.matterbridgeDirectory, this.config.injectPayloads)); this.log.warn(`***Injecting ${data.payloads.length} payloads from ${this.config.injectPayloads}`); for (const payload of data.payloads) { this.z2m.emitPayload(payload.topic, payload.payload); } }, 60 * 1000).unref(); } this.log.info(`Configured zigbee2mqtt dynamic platform v${this.version}`); } async onChangeLoggerLevel(logLevel) { this.log.info(`Configuring zigbee2mqtt platform logger level to ${CYAN}${logLevel}${nf}`); this.log.logLevel = logLevel; this.z2m.setLogLevel(logLevel); for (const bridgedDevice of this.bridgedDevices) { bridgedDevice.log.logLevel = logLevel; } for (const entity of this.zigbeeEntities) { entity.log.logLevel = logLevel; } this.log.debug('Changed logger level to ' + logLevel); } async onShutdown(reason) { await super.onShutdown(reason); this.z2m.removeAllListeners(); this.z2m.stop(); this.log.debug('Shutting down zigbee2mqtt platform: ' + reason); for (const entity of this.zigbeeEntities) { entity.destroy(); } if (this.injectTimer) clearInterval(this.injectTimer); this.injectTimer = undefined; if (this.availabilityTimer) clearInterval(this.availabilityTimer); this.availabilityTimer = undefined; if (this.config.unregisterOnShutdown === true) await this.unregisterAllDevices(); this.bridgedDevices = []; this.zigbeeEntities = []; this.z2mBridgeDevices = undefined; this.z2mBridgeGroups = undefined; this.z2mBridgeInfo = undefined; this.z2mEntityAvailability.clear(); this.z2mEntityPayload.clear(); this.log.info(`Shutdown zigbee2mqtt dynamic platform v${this.version}`); } async publish(topic, subTopic, message) { this.log.info(`MQTT publish topic: ${CYAN}${this.z2m.mqttTopic + '/' + topic + (subTopic === '' ? '' : '/' + subTopic)}${nf} payload: ${CYAN}${message}${nf}`); await this.z2m.publish(this.z2m.mqttTopic + '/' + topic + (subTopic === '' ? '' : '/' + subTopic), message); } async requestDeviceUpdate(device) { this.log.debug(`Requesting update for ${device.friendly_name} model_id: ${device.model_id} manufacturer: ${device.manufacturer}`); const payload = {}; if (device.power_source === 'Battery' || !device.definition || !device.definition.exposes) return; for (const feature of device.definition.exposes) { if (feature.features) { for (const subFeature of feature.features) { if (subFeature.access & 0b100) { payload[subFeature.property] = ''; } } } if (feature.access & 0b100) { payload[feature.property] = ''; } } if (payload && Object.keys(payload).length > 0) { const topic = this.z2m.mqttTopic + '/' + device.friendly_name + '/get'; await this.z2m.publish(topic, payloadStringify(payload), false); } } async requestGroupUpdate(group) { this.log.debug(`Requesting update for ${group.friendly_name}`); const payload = {}; payload['state'] = ''; if (payload && Object.keys(payload).length > 0) { const topic = this.z2m.mqttTopic + '/' + group.friendly_name + '/get'; await this.z2m.publish(topic, payloadStringify(payload), false); } } async registerZigbeeDevice(device) { this.setSelectDevice(device.ieee_address, device.friendly_name, undefined, 'wifi'); if (!this.validateDevice([device.friendly_name, device.ieee_address], true)) { return undefined; } this.log.debug(`Registering device ${dn}${device.friendly_name}${db} ID: ${zb}${device.ieee_address}${db}`); let matterDevice; try { matterDevice = await ZigbeeDevice.create(this, device); if (matterDevice.bridgedDevice) { matterDevice.bridgedDevice.configUrl = `${this.config.zigbeeFrontend}/#/device/${device.ieee_address}/info`; await this.registerDevice(matterDevice.bridgedDevice); this.bridgedDevices.push(matterDevice.bridgedDevice); this.zigbeeEntities.push(matterDevice); this.log.debug(`Registered device ${dn}${device.friendly_name}${db} ID: ${zb}${device.ieee_address}${db}`); } else this.log.warn(`Device ${dn}${device.friendly_name}${wr} ID: ${device.ieee_address} not registered`); } catch (error) { this.log.error(`Error registering device ${dn}${device.friendly_name}${er} ID: ${device.ieee_address}: ${error}`); } return matterDevice; } async registerZigbeeGroup(group) { this.setSelectDevice(`group-${group.id}`, group.friendly_name, undefined, 'wifi'); if (!this.validateDevice([group.friendly_name, `group-${group.id}`], true)) { return undefined; } this.log.debug(`Registering group ${gn}${group.friendly_name}${db} ID: ${zb}${group.id}${db}`); let matterGroup; try { matterGroup = await ZigbeeGroup.create(this, group); if (matterGroup.bridgedDevice) { matterGroup.bridgedDevice.configUrl = `${this.config.zigbeeFrontend}/#/group/${group.id}`; await this.registerDevice(matterGroup.bridgedDevice); this.bridgedDevices.push(matterGroup.bridgedDevice); this.zigbeeEntities.push(matterGroup); this.log.debug(`Registered group ${gn}${group.friendly_name}${db} ID: ${zb}${group.id}${db}`); } else this.log.warn(`Group ${gn}${group.friendly_name}${wr} ID: ${group.id} not registered`); } catch (error) { this.log.error(`Error registering group ${gn}${group.friendly_name}${er} ID: ${group.id}: ${error}`); } return matterGroup; } registerVirtualDevice(name, callback) { let aggregator; if (this.matterbridge.bridgeMode === 'bridge') { aggregator = this.matterbridge.aggregatorNode; } else if (this.matterbridge.bridgeMode === 'childbridge') { aggregator = this.matterbridge.plugins.get(this.name)?.aggregatorNode; } if (aggregator) { if (aggregator.parts.has(name.replaceAll(' ', '') + ':' + this.config.scenesType)) { this.log.warn(`Scene name ${name} already registered. Please use a different name. Changed to ${name + ' ' + this.namePostfix}`); name = name + ' ' + this.namePostfix++; } addVirtualDevice(aggregator, name.slice(0, 32), this.config.scenesType, callback); } } async unregisterZigbeeEntity(friendly_name) { const entity = this.zigbeeEntities.find((entity) => entity.entityName === friendly_name); if (entity) { this.log.info(`Removing device: ${friendly_name}`); await this.unregisterDevice(entity.bridgedDevice); entity.destroy(); this.zigbeeEntities = this.zigbeeEntities.filter((entity) => entity.entityName !== friendly_name); this.bridgedDevices = this.bridgedDevices.filter((device) => device.deviceName !== friendly_name); } } async updateAvailability(available) { if (this.bridgedDevices.length === 0) return; this.log.info(`Setting availability for ${this.bridgedDevices.length} devices to ${available}`); for (const bridgedDevice of this.bridgedDevices) { await bridgedDevice.setAttribute(BridgedDeviceBasicInformation.Cluster.id, 'reachable', available, this.log); if (bridgedDevice.maybeNumber) await bridgedDevice.triggerEvent(BridgedDeviceBasicInformation.Cluster.id, 'reachableChanged', { reachableNewValue: available }, this.log); } } }