matterbridge-test
Version:
Matterbridge test plugin
359 lines (358 loc) • 24.3 kB
JavaScript
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.`);
}
}