UNPKG

matterbridge-bthome

Version:
208 lines (207 loc) 12.1 kB
import { bridgedNode, contactSensor, genericSwitch, humiditySensor, lightSensor, MatterbridgeDynamicPlatform, MatterbridgeEndpoint, occupancySensor, powerSource, pressureSensor, temperatureSensor, } from 'matterbridge'; import { BLUE, db, debugStringify, idn, nf, rs } from 'matterbridge/logger'; import { NumberTag } from 'matterbridge/matter'; import { fireAndForget } from 'matterbridge/utils'; import { BTHome } from './BTHome.js'; export default function initializePlugin(matterbridge, log, config) { return new Platform(matterbridge, log, config); } export class Platform extends MatterbridgeDynamicPlatform { config; btHome = new BTHome(); bridgedDevices = new Map(); constructor(matterbridge, log, config) { super(matterbridge, log, config); this.config = config; if (typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.8.0')) { throw new Error(`This plugin requires Matterbridge version >= "3.8.0". Please update Matterbridge to the latest version in the frontend.`); } this.log.info('Initializing platform:', this.config.name); this.btHome.on('discovered', (device) => { fireAndForget((async () => { 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)); await this.addDevice(device); await this.savePeripherals(); })(), this.log, 'Error while handling discovered BTHome device'); }); this.btHome.on('update', (device) => { fireAndForget((async () => { 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, 'Error while handling updated BTHome 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(); this.btHome.log.logLevel = this.log.logLevel; await this.btHome.start(); } async onConfigure() { await super.onConfigure(); this.log.info('onConfigure called'); for (const device of this.btHome.bthomePeripherals.values()) { await 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) await 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(); await 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.btHome.log.logLevel = 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], { id: '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 ? { id: property, tagList: [{ mfgCode: null, namespaceId: NumberTag.Zero.namespaceId, tag: parseInt(index), label: null }] } : { id: 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.getChildEndpointById(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.getChildEndpointById(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`); } }