UNPKG

matterbridge-test

Version:
359 lines (358 loc) 24.3 kB
import { bridgedNode, colorTemperatureLight, electricalSensor, MatterbridgeDynamicPlatform, MatterbridgeEndpoint, modeSelect, onOffOutlet, onOffSwitch, powerSource, } from 'matterbridge'; import { CYAN, er, nf } from 'matterbridge/logger'; import { BridgedDeviceBasicInformation, BridgedDeviceBasicInformationCluster, ElectricalEnergyMeasurement, ElectricalEnergyMeasurementCluster, ElectricalPowerMeasurement, ElectricalPowerMeasurementCluster, ModeSelect, ModeSelectCluster, OnOffCluster, PowerSource, } from 'matterbridge/matter/clusters'; import { isValidString, waiter } from 'matterbridge/utils'; export default function initializePlugin(matterbridge, log, config) { return new TestPlatform(matterbridge, log, config); } export class TestPlatform extends MatterbridgeDynamicPlatform { config; interval; bridgedDevices = new Map(); constructor(matterbridge, log, config) { super(matterbridge, log, config); this.config = config; if (typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.4.0')) { throw new Error(`The test plugin requires Matterbridge version >= "3.4.0". Please update Matterbridge to the latest version in the frontend.`); } this.log.info('Initializing platform:', this.config.name); this.log.debug('- with matterbridge version ', matterbridge.matterbridgeVersion); this.log.debug('- with config:', this.config); this.log.debug('- with noDevice:', this.config.noDevices); this.log.debug('- with delayStart:', this.config.delayStart); this.log.debug('- with enableElectrical:', this.config.enableElectrical); this.log.debug('- with enableModeSelect:', this.config.enableModeSelect); this.log.debug('- with enablePowerSource:', this.config.enablePowerSource); this.log.debug('- with loadSwitches:', this.config.loadSwitches); this.log.debug('- with loadOutlets:', this.config.loadOutlets); this.log.debug('- with loadLights:', this.config.loadLights); this.log.debug('- with setUpdateInterval:', this.config.setUpdateInterval); this.log.debug('- with throwLoad:', this.config.throwLoad); this.log.debug('- with throwStart:', this.config.throwStart); this.log.debug('- with throwConfigure:', this.config.throwConfigure); this.log.debug('- with throwShutdown:', this.config.throwShutdown); if (this.config.throwLoad) { setImmediate(async () => { await this.onShutdown('Throwing error in load'); }); throw new Error('Throwing error in load'); } this.log.info('Finished initializing platform:', this.config.name); } async onStart(reason) { this.log.info('onStart called with reason:', reason ?? 'none'); if (this.config.throwStart) { await this.onShutdown('Throwing error in start'); throw new Error('Throwing error in start'); } if (this.config.delayStart) await waiter('Delay start', () => false, false, 20000, 1000); if (this.config.longDelayStart) await waiter('Long delay start', () => false, false, 150000, 1000); await this.ready; await this.clearSelect(); for (let i = 0; i < this.config.loadSwitches; i++) { this.setSelectDevice('serial_switch_' + i, 'Switch ' + i); if (!this.validateDevice('Switch ' + i)) continue; const switchDevice = new MatterbridgeEndpoint([onOffSwitch, bridgedNode, ...(this.config.enableElectrical ? [electricalSensor] : []), ...(this.config.enablePowerSource ? [powerSource] : [])], { id: 'Switch' + i }, this.config.debug); switchDevice.log.logName = 'Switch' + i; switchDevice.createDefaultBridgedDeviceBasicInformationClusterServer('Switch ' + i, 'serial_switch_' + i, 0xfff1, 'Matterbridge', 'Matterbridge test plugin', parseInt(this.version.replace(/\D/g, '')), this.version, parseInt(this.matterbridge.matterbridgeVersion.replace(/\D/g, '')), this.matterbridge.matterbridgeVersion); switchDevice.addCommandHandler('identify', async (data) => { this.log.info(`Received identify command request ${data.request.identifyTime} for endpoint ${data.endpoint?.number}`); }); switchDevice.addCommandHandler('on', async (data) => { this.log.info(`Received on command for endpoint ${data.endpoint?.number}`); }); switchDevice.addCommandHandler('off', async (data) => { this.log.info(`Received off command for endpoint ${data.endpoint?.number}`); }); if (this.config.enableElectrical) this.addElectricalMeasurements(switchDevice); if (this.config.enablePowerSource) this.addPowerSource(switchDevice, 'wired'); if (this.config.enableModeSelect) { const composed = await this.addModeSelect(switchDevice, 'Switch ' + i); composed.addCommandHandler('changeToMode', async (data) => { this.log.info(`Received changeToMode command with request ${data.request.newMode} for endpoint ${data.endpoint?.number}`); }); } switchDevice.addRequiredClusterServers(); if (!this.config.noDevices) await this.registerDevice(switchDevice); this.bridgedDevices.set('Switch ' + i, switchDevice); } for (let i = 0; i < this.config.loadOutlets; i++) { this.setSelectDevice('serial_outlet_' + i, 'Outlet ' + i); if (!this.validateDevice('Outlet ' + i)) continue; const outletDevice = new MatterbridgeEndpoint([onOffOutlet, bridgedNode, ...(this.config.enableElectrical ? [electricalSensor] : []), ...(this.config.enablePowerSource ? [powerSource] : [])], { id: 'Outlet' + i }, this.config.debug); outletDevice.log.logName = 'Outlet' + i; outletDevice.createDefaultBridgedDeviceBasicInformationClusterServer('Outlet ' + i, 'serial_outlet_' + i, 0xfff1, 'Matterbridge', 'Matterbridge test plugin', parseInt(this.version.replace(/\D/g, '')), this.version, parseInt(this.matterbridge.matterbridgeVersion.replace(/\D/g, '')), this.matterbridge.matterbridgeVersion); outletDevice.addCommandHandler('identify', async (data) => { this.log.info(`Received identify command request ${data.request.identifyTime} for endpoint ${data.endpoint?.number}`); }); outletDevice.addCommandHandler('on', async (data) => { this.log.info(`Received on command for endpoint ${data.endpoint?.number}`); }); outletDevice.addCommandHandler('off', async (data) => { this.log.info(`Received off command for endpoint ${data.endpoint?.number}`); }); if (this.config.enableElectrical) this.addElectricalMeasurements(outletDevice); if (this.config.enablePowerSource) this.addPowerSource(outletDevice, 'replaceable'); if (this.config.enableModeSelect) { const composed = await this.addModeSelect(outletDevice, 'Outlet ' + i); composed.addCommandHandler('changeToMode', async (data) => { this.log.info(`Received command changeToMode with request ${data.request.newMode} for endpoint ${data.endpoint?.number}`); }); } outletDevice.addRequiredClusterServers(); if (!this.config.noDevices) await this.registerDevice(outletDevice); this.bridgedDevices.set('Outlet ' + i, outletDevice); } for (let i = 0; i < this.config.loadLights; i++) { this.setSelectDevice('serial_light_' + i, 'Light ' + i); if (!this.validateDevice('Light ' + i)) continue; const lightDevice = new MatterbridgeEndpoint([colorTemperatureLight, bridgedNode, ...(this.config.enableElectrical ? [electricalSensor] : []), ...(this.config.enablePowerSource ? [powerSource] : [])], { id: 'Light' + i }, this.config.debug); lightDevice.log.logName = 'Light' + i; lightDevice.createDefaultBridgedDeviceBasicInformationClusterServer('Light ' + i, 'serial_light_' + i, 0xfff1, 'Matterbridge', 'Matterbridge test plugin', parseInt(this.version.replace(/\D/g, '')), this.version, parseInt(this.matterbridge.matterbridgeVersion.replace(/\D/g, '')), this.matterbridge.matterbridgeVersion); lightDevice.addCommandHandler('identify', async (data) => { this.log.info(`Received identify command request ${data.request.identifyTime} for endpoint ${data.endpoint?.number}`); }); lightDevice.addCommandHandler('on', async (data) => { this.log.info(`Received on command for endpoint ${data.endpoint?.number}`); }); lightDevice.addCommandHandler('off', async (data) => { this.log.info(`Received off command for endpoint ${data.endpoint?.number}`); }); lightDevice.addCommandHandler('moveToLevel', async (data) => { this.log.info(`Received moveToLevel command request ${data.request.level} for endpoint ${data.endpoint?.number}`); }); lightDevice.addCommandHandler('moveToLevelWithOnOff', async (data) => { this.log.info(`Received moveToLevelWithOnOff command request ${data.request.level} for endpoint ${data.endpoint?.number}`); }); lightDevice.addCommandHandler('moveToColor', async ({ request: { colorX, colorY } }) => { this.log.info(`Received moveToColor command request X ${colorX / 65536} Y ${colorY / 65536}`); }); lightDevice.addCommandHandler('moveToHueAndSaturation', async ({ request: { hue, saturation } }) => { this.log.info(`Received moveToHueAndSaturation command request hue ${hue} saturation ${saturation}`); }); lightDevice.addCommandHandler('moveToHue', async ({ request: { hue } }) => { this.log.info(`Received moveToHue command request ${hue}`); }); lightDevice.addCommandHandler('moveToSaturation', async ({ request: { saturation } }) => { this.log.info(`Received moveToSaturation command request ${saturation}`); }); lightDevice.addCommandHandler('moveToColorTemperature', async ({ request }) => { this.log.info(`Received moveToColorTemperature command request ${request.colorTemperatureMireds}`); }); if (this.config.enableElectrical) this.addElectricalMeasurements(lightDevice); if (this.config.enablePowerSource) this.addPowerSource(lightDevice, 'rechargeable'); if (this.config.enableModeSelect) { const composed = await this.addModeSelect(lightDevice, 'Light ' + i); composed.addCommandHandler('changeToMode', async (data) => { this.log.info(`Received command changeToMode with request ${data.request.newMode} for endpoint ${data.endpoint?.number}`); }); } lightDevice.addRequiredClusterServers(); if (!this.config.noDevices) await this.registerDevice(lightDevice); this.bridgedDevices.set('Light ' + i, lightDevice); } this.log.info(`Finished starting platform ${this.config.name} with ${this.bridgedDevices.size} devices:`); for (const device of this.bridgedDevices.values()) { this.log.info(`- device ${device.deviceName} with serial ${device.serialNumber}`); } } addPowerSource(device, type) { if (type === 'wired') device.createDefaultPowerSourceWiredClusterServer(PowerSource.WiredCurrentType.Ac); else if (type === 'replaceable') device.createDefaultPowerSourceReplaceableBatteryClusterServer(100); else if (type === 'rechargeable') device.createDefaultPowerSourceRechargeableBatteryClusterServer(100); } addElectricalMeasurements(device) { device.createDefaultPowerTopologyClusterServer(); device.createDefaultElectricalPowerMeasurementClusterServer(220 * 1000, 2.5 * 1000, 220 * 2.5 * 1000, 50 * 1000); device.createDefaultElectricalEnergyMeasurementClusterServer(1500 * 1000); } async addModeSelect(device, description) { const composed = new MatterbridgeEndpoint(modeSelect, { id: device.id + '_modeSelect' }, this.config.debug); composed.createDefaultModeSelectClusterServer(description + ' Led Mode Select', [ { label: 'Led ON', mode: 1, semanticTags: [] }, { label: 'Led OFF', mode: 2, semanticTags: [] }, ], 1, 1); device.parts.add(composed); return composed; } async onConfigure() { await super.onConfigure(); this.log.info('onConfigure called'); if (this.config.throwConfigure) { await this.onShutdown('Throwing error in configure'); throw new Error('Throwing error in configure'); } if (this.config.setUpdateInterval === 0) return; const getRandomNumberInRange = (min, max) => { return Math.floor(Math.random() * (max - min + 1)) + min; }; this.interval = setInterval(async () => { this.log.info('Interval called'); for (let i = 0; i < this.config.loadSwitches; i++) { const device = this.bridgedDevices.get('Switch ' + i); const state = device?.getAttribute(OnOffCluster.id, 'onOff'); await device?.setAttribute(OnOffCluster.id, 'onOff', !state, device?.log); if (this.config.enableReachable) await device?.setAttribute(BridgedDeviceBasicInformationCluster.id, 'reachable', state, device?.log); if (this.config.enableElectrical) { const voltage = getRandomNumberInRange(220, 240); const current = getRandomNumberInRange(20, 30); await device?.setAttribute(ElectricalPowerMeasurement.Complete, 'voltage', voltage * 1000, device.log); await device?.setAttribute(ElectricalPowerMeasurement.Complete, 'activeCurrent', current * 1000, device.log); await device?.setAttribute(ElectricalPowerMeasurement.Complete, 'activePower', voltage * current * 1000, device.log); const cumulativeEnergy = device?.getAttribute(ElectricalEnergyMeasurement.Complete.id, 'cumulativeEnergyImported', device.log); await device?.setAttribute(ElectricalEnergyMeasurement.Complete, 'cumulativeEnergyImported', { energy: cumulativeEnergy ? cumulativeEnergy.energy + 1000 : 1500 * 1000 }, device.log); } if (this.config.enableModeSelect) { const composed = device?.getChildEndpointById(device.id + '_modeSelect'); const currentMode = composed?.getAttribute(ModeSelect.Cluster, 'currentMode', device?.log); await composed?.setAttribute(ModeSelect.Cluster, 'currentMode', currentMode === 1 ? 2 : 1, device?.log); } if (this.config.enablePowerSource) { if (device?.hasAttributeServer(PowerSource.Complete, 'wiredCurrentType')) { const type = device?.getAttribute(PowerSource.Complete, 'wiredCurrentType', device?.log); await device?.setAttribute(PowerSource.Complete, 'wiredCurrentType', type === PowerSource.WiredCurrentType.Ac ? PowerSource.WiredCurrentType.Dc : PowerSource.WiredCurrentType.Ac); await device?.setAttribute(PowerSource.Complete, 'description', type === PowerSource.WiredCurrentType.Ac ? 'AC Power' : 'DC Power'); } } } for (let i = 0; i < this.config.loadOutlets; i++) { const device = this.bridgedDevices.get('Outlet ' + i); const state = device?.getAttribute(OnOffCluster.id, 'onOff'); await device?.setAttribute(OnOffCluster.id, 'onOff', !state, device?.log); if (this.config.enableReachable) await device?.setAttribute(BridgedDeviceBasicInformation.Cluster, 'reachable', state, device?.log); if (this.config.enableElectrical) { const voltage = getRandomNumberInRange(220, 240); const current = getRandomNumberInRange(20, 30); await device?.setAttribute(ElectricalPowerMeasurementCluster.id, 'voltage', voltage * 1000, device.log); await device?.setAttribute(ElectricalPowerMeasurementCluster.id, 'activeCurrent', current * 1000, device.log); await device?.setAttribute(ElectricalPowerMeasurementCluster.id, 'activePower', voltage * current * 1000, device.log); const cumulativeEnergy = device?.getAttribute(ElectricalEnergyMeasurementCluster.id, 'cumulativeEnergyImported', device.log); await device?.setAttribute(ElectricalEnergyMeasurementCluster.id, 'cumulativeEnergyImported', { energy: cumulativeEnergy ? cumulativeEnergy.energy + 1000 : 1500 * 1000 }, device.log); } if (this.config.enableModeSelect) { const composed = device?.getChildEndpointById(device.id + '_modeSelect'); const currentMode = composed?.getAttribute(ModeSelectCluster.id, 'currentMode', device?.log); await composed?.setAttribute(ModeSelectCluster.id, 'currentMode', currentMode === 1 ? 2 : 1, device?.log); } if (this.config.enablePowerSource) { if (device?.hasAttributeServer(PowerSource.Cluster.id, 'batPercentRemaining')) { const battery = device?.getAttribute(PowerSource.Cluster.id, 'batPercentRemaining', device?.log); await device?.setAttribute(PowerSource.Cluster.id, 'batPercentRemaining', battery + 20 > 200 ? 20 : battery + 20, device?.log); await device?.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', battery + 20 > 200 ? PowerSource.BatChargeLevel.Critical : PowerSource.BatChargeLevel.Ok, device?.log); } } } for (let i = 0; i < this.config.loadLights; i++) { const device = this.bridgedDevices.get('Light ' + i); const state = device?.getAttribute(OnOffCluster.id, 'onOff'); await device?.setAttribute(OnOffCluster.id, 'onOff', !state, device?.log); if (this.config.enableReachable) await device?.setAttribute(BridgedDeviceBasicInformationCluster.id, 'reachable', state, device?.log); if (this.config.enableElectrical) { const voltage = getRandomNumberInRange(220, 240); const current = getRandomNumberInRange(20, 30); await device?.setAttribute(ElectricalPowerMeasurementCluster.id, 'voltage', voltage * 1000, device.log); await device?.setAttribute(ElectricalPowerMeasurementCluster.id, 'activeCurrent', current * 1000, device.log); await device?.setAttribute(ElectricalPowerMeasurementCluster.id, 'activePower', voltage * current * 1000, device.log); const cumulativeEnergy = device?.getAttribute(ElectricalEnergyMeasurementCluster.id, 'cumulativeEnergyImported', device.log); await device?.setAttribute(ElectricalEnergyMeasurementCluster.id, 'cumulativeEnergyImported', { energy: cumulativeEnergy ? cumulativeEnergy.energy + 1000 : 1500 * 1000 }, device.log); } if (this.config.enableModeSelect) { const composed = device?.getChildEndpointById(device.id + '_modeSelect'); const currentMode = composed?.getAttribute(ModeSelectCluster.id, 'currentMode', device?.log); await composed?.setAttribute(ModeSelectCluster.id, 'currentMode', currentMode === 1 ? 2 : 1, device?.log); } if (this.config.enablePowerSource) { if (device?.hasAttributeServer(PowerSource.Cluster.id, 'batPercentRemaining')) { const battery = device?.getAttribute(PowerSource.Cluster.id, 'batPercentRemaining', device?.log); await device?.setAttribute(PowerSource.Cluster.id, 'batPercentRemaining', battery + 20 > 200 ? 20 : battery + 20, device?.log); await device?.setAttribute(PowerSource.Cluster.id, 'batChargeLevel', battery + 20 > 200 ? PowerSource.BatChargeLevel.Critical : PowerSource.BatChargeLevel.Ok, device?.log); } } } }, this.config.setUpdateInterval * 1000); } async onShutdown(reason) { await super.onShutdown(reason); this.log.info('onShutdown called with reason:', reason ?? 'none'); if (this.interval) clearInterval(this.interval); this.interval = undefined; if (this.config.throwShutdown) throw new Error('Throwing error in shutdown'); } async onChangeLoggerLevel(logLevel) { this.log.info(`Logger level set to: ${logLevel}`); for (const device of this.bridgedDevices.values()) { device.log.logLevel = logLevel; } } async onAction(action, value, id) { this.log.info(`Received action ${CYAN}${action}${nf}${value ? ' with ' + CYAN + value + nf : ''} ${id ? 'for schema ' + CYAN + id + nf : ''}`); if (action === 'turnOn') { this.log.info('Turning on all the devices'); for (const device of this.bridgedDevices.values()) { await device.setAttribute(BridgedDeviceBasicInformationCluster.id, 'reachable', true, device.log); await device.setAttribute(OnOffCluster.id, 'onOff', true, device.log); } } if (action === 'turnOff') { this.log.info('Turning off all the devices'); for (const device of this.bridgedDevices.values()) { await device.setAttribute(BridgedDeviceBasicInformationCluster.id, 'reachable', true, device.log); await device.setAttribute(OnOffCluster.id, 'onOff', false, device.log); } } if (action === 'turnOnDevice' && isValidString(value, 5)) { this.log.info(`Turning on the device ${CYAN}${value}${nf}`); const device = this.bridgedDevices.get(value); if (device) { await device.setAttribute(BridgedDeviceBasicInformationCluster.id, 'reachable', true, device.log); await device.setAttribute(OnOffCluster.id, 'onOff', true, device.log); } else { this.log.error(`Device ${CYAN}${value}${er} not found:`); for (const device of this.bridgedDevices.keys()) { this.log.info(`- device ${device}`); } } } if (action === 'turnOffDevice' && isValidString(value, 5)) { this.log.info(`Turning off the device ${CYAN}${value}${nf}`); const device = this.bridgedDevices.get(value); if (device) { await device.setAttribute(BridgedDeviceBasicInformationCluster.id, 'reachable', true, device.log); await device.setAttribute(OnOffCluster.id, 'onOff', false, device.log); } else { this.log.error(`Device ${CYAN}${value}${er} not found:`); for (const device of this.bridgedDevices.keys()) { this.log.info(`- device ${device}`); } } } } async onConfigChanged(config) { this.log.info(`The config for plugin ${CYAN}${config.name}${nf} has been updated.`); } }