matterbridge-bthome
Version:
Matterbridge BTHome plugin
208 lines (207 loc) • 11.9 kB
JavaScript
import { bridgedNode, contactSensor, genericSwitch, humiditySensor, lightSensor, MatterbridgeDynamicPlatform, MatterbridgeEndpoint, occupancySensor, powerSource, pressureSensor, temperatureSensor, } from 'matterbridge';
import { db, debugStringify, idn, rs, BLUE, nf } from 'matterbridge/logger';
import { isValidArray } from 'matterbridge/utils';
import { NumberTag } from 'matterbridge/matter';
import { BTHome } from './BTHome.js';
export class Platform extends MatterbridgeDynamicPlatform {
btHome = new BTHome();
bridgedDevices = new Map();
constructor(matterbridge, log, config) {
super(matterbridge, log, config);
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.0.0')) {
throw new Error(`This plugin requires Matterbridge version >= "3.0.0". Please update Matterbridge to the latest version in the frontend.`);
}
this.log.info('Initializing platform:', this.config.name);
if (!isValidArray(config.whiteList))
config.whiteList = [];
if (!isValidArray(config.blackList))
config.blackList = [];
this.btHome.on('discovered', async (device) => {
this.log.notice(`Discovered new BTHome device: ${device.mac}`);
this.log.info('- name:', device.localName);
this.log.info('- rssi:', device.rssi);
this.log.info('- version:', device.version);
this.log.info('- encrypted:', device.encrypted);
this.log.info('- trigger:', device.trigger);
this.log.info('- data:', debugStringify(device.data));
this.addDevice(device);
await this.savePeripherals();
});
this.btHome.on('update', async (device) => {
this.log.info(`${db}BTHome message from ${idn}${device.mac}${rs}${db} rssi ${BLUE}${device.rssi}${db} name ${BLUE}${device.localName}${db} version ${BLUE}${device.version}${db} ${BLUE}${device.encrypted ? 'encrypted ' : ''}${device.trigger ? 'trigger ' : ''}${db}data ${debugStringify(device.data)}`);
await this.updateDevice(device);
});
this.log.info('Finished initializing platform:', this.config.name);
}
async onStart(reason) {
this.log.info('onStart called with reason:', reason ?? 'none');
await this.ready;
await this.clearSelect();
await this.loadPeripherals();
await this.btHome.start();
}
async onConfigure() {
await super.onConfigure();
this.log.info('onConfigure called');
this.btHome.bthomePeripherals.forEach(async (device) => {
this.updateDevice(device);
});
}
async onAction(action, value, id) {
this.log.info('onAction called with action:', action, 'and value:', value ?? 'none', 'and id:', id ?? 'none');
if (action === 'delete' && value) {
value = value.toLowerCase().trimStart().trimEnd();
if (!this.btHome.bthomePeripherals.has(value)) {
this.log.error(`The device ${value} is not registered. Please check the MAC address.`);
return;
}
await this.btHome.stop();
this.btHome.bthomePeripherals.delete(value);
await this.savePeripherals();
const device = this.bridgedDevices.get(value);
if (device)
this.unregisterDevice(device);
this.bridgedDevices.delete(value);
this.log.notice(`The device ${value} has been deleted. Please restart the plugin.`);
}
if (action === 'reset') {
await this.btHome.stop();
this.btHome.bthomePeripherals.clear();
await this.savePeripherals();
this.unregisterAllDevices();
this.bridgedDevices.clear();
this.log.notice('The storage has been reset');
}
}
async onChangeLoggerLevel(logLevel) {
this.log.info(`Changing logger level for platform ${idn}${this.config.name}${rs}${nf} to ${logLevel}`);
this.bridgedDevices.forEach((device) => (device.log.logLevel = logLevel));
}
async onShutdown(reason) {
this.log.info('onShutdown called with reason:', reason ?? 'none');
await this.savePeripherals();
this.btHome.logDevices();
await this.btHome.stop();
await super.onShutdown(reason);
if (this.config.unregisterOnShutdown === true)
await this.unregisterAllDevices();
this.bridgedDevices.clear();
this.log.info('onShutdown finished');
}
converter = [
{ reading: 'battery', deviceType: powerSource, cluster: 'PowerSource', attribute: 'batPercentRemaining', factor: 2 },
{ reading: 'temperature', deviceType: temperatureSensor, cluster: 'TemperatureMeasurement', attribute: 'measuredValue', factor: 100 },
{ reading: 'humidity', deviceType: humiditySensor, cluster: 'RelativeHumidityMeasurement', attribute: 'measuredValue', factor: 100 },
{ reading: 'pressure', deviceType: pressureSensor, cluster: 'PressureMeasurement', attribute: 'measuredValue', factor: 100 },
{ reading: 'motionState', deviceType: occupancySensor, cluster: 'OccupancySensing', attribute: 'occupancy', property: 'occupied', type: 'boolean' },
{ reading: 'movingState', deviceType: occupancySensor, cluster: 'OccupancySensing', attribute: 'occupancy', property: 'occupied', type: 'boolean' },
{ reading: 'occupancyState', deviceType: occupancySensor, cluster: 'OccupancySensing', attribute: 'occupancy', property: 'occupied', type: 'boolean' },
{ reading: 'illuminance', deviceType: lightSensor, cluster: 'IlluminanceMeasurement', attribute: 'measuredValue', type: 'lux' },
{ reading: 'doorState', deviceType: contactSensor, cluster: 'BooleanState', attribute: 'stateValue', type: 'boolean_inverted' },
{ reading: 'garageDoorState', deviceType: contactSensor, cluster: 'BooleanState', attribute: 'stateValue', type: 'boolean_inverted' },
{ reading: 'windowState', deviceType: contactSensor, cluster: 'BooleanState', attribute: 'stateValue', type: 'boolean_inverted' },
{ reading: 'button', deviceType: genericSwitch, cluster: 'Switch' },
{ reading: 'rotation_deg' },
{ reading: 'packetId' },
{ reading: 'deviceTypeId' },
{ reading: 'firmwareVersion' },
{ reading: 'firmwareVersionShort' },
{ reading: 'text' },
{ reading: 'raw' },
];
async addDevice(device) {
this.setSelectDevice(device.mac, device.localName, undefined, 'ble');
if (!this.validateDevice(device.mac, true))
return;
const matterbridgeDevice = new MatterbridgeEndpoint([bridgedNode], { uniqueStorageKey: 'BTHome ' + device.mac }, this.config.debug).createDefaultBridgedDeviceBasicInformationClusterServer('BTHome ' + device.mac, device.mac, this.matterbridge.aggregatorVendorId, this.matterbridge.aggregatorVendorName, 'BTHomeDevice');
for (const property in device.data) {
const [name, index] = property.split(':');
const converter = this.converter.find((converter) => converter.reading === name);
if (converter && converter.deviceType) {
this.setSelectDeviceEntity(device.mac, property, `${name}${index ? ' n. ' + index : ''}`, 'ble');
const child = matterbridgeDevice.addChildDeviceType(property, converter.deviceType, index
? {
uniqueStorageKey: property,
tagList: [{ mfgCode: null, namespaceId: NumberTag.Zero.namespaceId, tag: parseInt(index), label: null }],
}
: {
uniqueStorageKey: property,
});
if (converter.cluster === 'PowerSource')
child.createDefaultPowerSourceReplaceableBatteryClusterServer();
child.addRequiredClusterServers();
}
else if (converter && !converter.deviceType) {
}
else {
this.log.warn(`No converter found for property ${name} in device ${device.mac}`);
}
}
await this.registerDevice(matterbridgeDevice);
this.bridgedDevices.set(device.mac, matterbridgeDevice);
await this.updateDevice(device);
}
async updateDevice(device) {
if (!this.validateDevice(device.mac, false))
return;
const matterbridgeDevice = this.bridgedDevices.get(device.mac);
if (!matterbridgeDevice)
return;
for (const property in device.data) {
const [name, _index] = property.split(':');
const converter = this.converter.find((converter) => converter.reading === name);
if (!converter) {
this.log.debug(`***No converter found for property ${property} in device mac ${device.mac} model ${device.localName}`);
continue;
}
if (converter && converter.deviceType && converter.cluster && converter.attribute) {
const child = matterbridgeDevice.getChildEndpointByName(property);
let value = device.data[property];
if (converter.factor && typeof value === 'number')
value = value * converter.factor;
if (converter.type === 'boolean' && typeof value === 'number')
value = device.data[property] !== 0;
if (converter.type === 'boolean_inverted' && typeof value === 'number')
value = device.data[property] === 0;
if (converter.type === 'lux' && typeof value === 'number')
value = Math.round(Math.max(Math.min(10000 * Math.log10(value), 0xfffe), 0));
if (converter.property) {
await child?.updateAttribute(converter.cluster, converter.attribute, { [converter.property]: value }, child.log);
}
else {
await child?.updateAttribute(converter.cluster, converter.attribute, value, child.log);
}
}
if (converter && converter.deviceType && converter.cluster === 'Switch') {
const child = matterbridgeDevice.getChildEndpointByName(property);
const value = device.data[property];
if (child) {
if (value === 'single_press')
await child.triggerSwitchEvent('Single', child.log);
else if (value === 'double_press')
await child.triggerSwitchEvent('Double', child.log);
else if (value === 'long_press')
await child.triggerSwitchEvent('Long', child.log);
}
device.data[property] = 'none';
}
}
}
async loadPeripherals() {
if (!this.context)
throw new Error('Plugin context is not available');
const bthomePeripherals = await this.context.get('bthomePeripherals', []);
this.log.info(`Loading ${bthomePeripherals.length} BTHome devices from the storage...`);
for (const peripheral of bthomePeripherals) {
await this.addDevice(peripheral);
this.btHome.bthomePeripherals.set(peripheral.mac, peripheral);
this.log.debug(`Loaded BTHome device ${idn}${peripheral.mac}${rs}${db} ${peripheral.localName} from the storage`);
}
}
async savePeripherals() {
if (!this.context)
throw new Error('Plugin context is not available');
await this.context.set('bthomePeripherals', Array.from(this.btHome.bthomePeripherals.values()));
this.log.info(`Saved ${this.btHome.bthomePeripherals.size} BTHome devices in the storage`);
}
}