UNPKG

matterbridge-hass

Version:
476 lines (475 loc) 25.1 kB
import { createHash, randomBytes } from 'node:crypto'; import { MatterbridgeEndpoint, MatterbridgeSmokeCoAlarmServer, colorTemperatureLight, colorTemperatureSwitch, dimmableLight, dimmableOutlet, dimmableSwitch, extendedColorLight, onOffLight, onOffOutlet, onOffSwitch, MatterbridgeColorControlServer, MatterbridgeThermostatServer, } from 'matterbridge'; import { db, debugStringify, idn, ign, rs, CYAN } from 'matterbridge/logger'; import { VendorId, ClusterRegistry } from 'matterbridge/matter/types'; import { BooleanState, BridgedDeviceBasicInformation, ColorControl, PowerSource, SmokeCoAlarm, Thermostat } from 'matterbridge/matter/clusters'; import { BooleanStateServer, BridgedDeviceBasicInformationServer, PowerSourceServer } from 'matterbridge/matter/behaviors'; export function getClusterServerObj(clusterId, type, options) { return { id: clusterId, type, options }; } export class MutableDevice { mutableDevice = new Map(); matterbridge; deviceName; serialNumber; vendorId; vendorName; productName; softwareVersion; softwareVersionString; hardwareVersion; hardwareVersionString; composedType = undefined; configUrl = undefined; constructor(matterbridge, deviceName, serialNumber, vendorId = 0xfff1, vendorName = 'Matterbridge', productName = 'Matterbridge Device', softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString) { this.matterbridge = matterbridge; this.deviceName = deviceName; this.serialNumber = serialNumber ?? '0x' + randomBytes(8).toString('hex'); this.vendorId = VendorId(vendorId); this.vendorName = vendorName; this.productName = productName; this.softwareVersion = softwareVersion ?? parseInt(matterbridge.matterbridgeVersion.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(''); } size() { return this.mutableDevice.size; } has(endpoint) { return this.mutableDevice.has(endpoint); } get(endpoint = '') { if (this.mutableDevice.get(endpoint) === undefined) throw new Error(`Device ${endpoint} is not defined`); return this.mutableDevice.get(endpoint); } getEndpoint(endpoint = '') { if (this.mutableDevice.get(endpoint)?.endpoint === undefined) throw new Error(`Device ${endpoint} endpoint is not defined`); return this.mutableDevice.get(endpoint)?.endpoint; } initializeEndpoint(endpoint) { if (!this.mutableDevice.has(endpoint)) { this.mutableDevice.set(endpoint, { friendlyName: endpoint, tagList: [], deviceTypes: [], clusterServersIds: [], clusterServersObjs: [], clusterClientsIds: [], clusterClientsObjs: [], commandHandlers: [], subscribeHandlers: [], }); } return this.mutableDevice.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; } 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; } addClusterServerPowerSource(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, coAlarm: false, interconnectCoAlarm: 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: { smokeAlarm: false, interconnectSmokeAlarm: false, 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, colorTemperatureMireds, 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, colorTempPhysicalMinMireds, colorTempPhysicalMaxMireds, coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds, remainingTime: 0, startUpColorTemperatureMireds: null, })); return this; } addClusterServerColorControl(endpoint, colorTemperatureMireds, 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, 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: localTemperature * 100, 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: 1 * 100, 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: localTemperature * 100, 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: localTemperature * 100, 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; } createUniqueId(param1, param2, param3, param4) { const hash = createHash('md5'); hash.update(param1 + param2 + param3 + param4); return hash.digest('hex'); } 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: this.softwareVersion, softwareVersionString: this.softwareVersionString.slice(0, 64), hardwareVersion: this.hardwareVersion, hardwareVersionString: this.hardwareVersionString.slice(0, 64), reachable: true, })); return this; } async create() { await this.createMainEndpoint(); await this.createChildEndpoints(); for (const [endpoint] of this.mutableDevice) { await this.createClusters(endpoint); } return this.getEndpoint(); } removeDuplicateAndSupersetDeviceTypes() { for (const device of this.mutableDevice.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; } async createMainEndpoint() { this.removeDuplicateAndSupersetDeviceTypes(); const mainDevice = this.mutableDevice.get(''); mainDevice.friendlyName = this.deviceName; mainDevice.endpoint = new MatterbridgeEndpoint(mainDevice.deviceTypes, { uniqueStorageKey: this.deviceName }, true); mainDevice.endpoint.log.logName = this.deviceName; return mainDevice.endpoint; } async createChildEndpoint(endpoint) { this.removeDuplicateAndSupersetDeviceTypes(); const mainDevice = this.mutableDevice.get(''); if (!mainDevice.endpoint) throw new Error('Main endpoint is not defined. Call createMainEndpoint() first.'); const device = this.mutableDevice.get(endpoint); if (!device) throw new Error(`Device ${endpoint} is not defined.`); device.endpoint = mainDevice.endpoint.addChildDeviceType(endpoint, device.deviceTypes, device.tagList.length ? { tagList: device.tagList } : {}, true); device.endpoint.log.logName = device.friendlyName; return device.endpoint; } async createChildEndpoints() { this.removeDuplicateAndSupersetDeviceTypes(); const mainDevice = this.mutableDevice.get(''); if (!mainDevice.endpoint) throw new Error('Main endpoint is not defined. Call createMainEndpoint() first.'); for (const [endpoint, device] of Array.from(this.mutableDevice.entries()).filter(([endpoint]) => endpoint !== '')) { device.endpoint = mainDevice.endpoint.addChildDeviceType(endpoint, device.deviceTypes, device.tagList.length ? { tagList: device.tagList } : {}, true); device.endpoint.log.logName = device.friendlyName; } return this; } removeDuplicateClusterServers() { for (const device of this.mutableDevice.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; } async createClusters(endpoint) { this.removeDuplicateClusterServers(); if (endpoint === '') { const mainDevice = this.get(endpoint); if (!mainDevice.endpoint) throw new Error('Main endpoint is not defined'); 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) await mainDevice.endpoint.addFixedLabel('composed', this.composedType); 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) { mainDevice.endpoint.subscribeAttribute(subscribeHandler.clusterId, subscribeHandler.attribute, (newValue, oldValue, context) => { subscribeHandler.listener(newValue, oldValue, context, subscribeHandler.endpointName, subscribeHandler.clusterId, subscribeHandler.attribute); }, mainDevice.endpoint.log); } 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, async (data) => { commandHandler.handler(data, commandHandler.endpointName, commandHandler.command); }); } for (const subscribeHandler of device.subscribeHandlers) { device.endpoint.subscribeAttribute(subscribeHandler.clusterId, subscribeHandler.attribute, (newValue, oldValue, context) => { subscribeHandler.listener(newValue, oldValue, context, subscribeHandler.endpointName, subscribeHandler.clusterId, subscribeHandler.attribute); }, device.endpoint.log); } return this; } logMutableDevice() { this.matterbridge.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.mutableDevice) { const deviceTypes = device.deviceTypes.map((d) => '0x' + d.code.toString(16) + '-' + d.name); const clusterServersIds = device.clusterServersIds.map((clusterServerId) => '0x' + clusterServerId.toString(16) + '-' + ClusterRegistry.get(clusterServerId)?.name); const clusterServersObjsIds = device.clusterServersObjs.map((clusterServerObj) => '0x' + clusterServerObj.id.toString(16) + '-' + ClusterRegistry.get(clusterServerObj.id)?.name); this.matterbridge.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; } }