diff --git a/src-admin/src/components/Bridges.js b/src-admin/src/components/Bridges.js index e30f13c5..2cada800 100644 --- a/src-admin/src/components/Bridges.js +++ b/src-admin/src/components/Bridges.js @@ -126,6 +126,7 @@ class Bridges extends React.Component { enabled: true, productID: this.state.editDialog.productID, vendorID: this.state.editDialog.vendorID, + noComposed: this.state.editDialog.noComposed, list: [], uuid: uuidv4(), }); @@ -133,6 +134,7 @@ class Bridges extends React.Component { matter.bridges[this.state.editDialog.bridge].name = this.state.editDialog.name; matter.bridges[this.state.editDialog.bridge].productID = this.state.editDialog.productID; matter.bridges[this.state.editDialog.bridge].vendorID = this.state.editDialog.vendorID; + matter.bridges[this.state.editDialog.bridge].noComposed = this.state.editDialog.noComposed; } else if (this.state.editDialog.bridge !== undefined) { matter.bridges[this.state.editDialog.bridge].list[this.state.editDialog.device].name = this.state.editDialog.name; } diff --git a/src/ioBrokerStorageTypes.ts b/src/ioBrokerStorageTypes.ts new file mode 100644 index 00000000..e6c9a1e1 --- /dev/null +++ b/src/ioBrokerStorageTypes.ts @@ -0,0 +1,21 @@ +export interface MatterAdapterConfig extends ioBroker.AdapterConfig { + interface: string; +} + +export interface DeviceDescription { + uuid: string; + name: string; + oid: string; + type: string; + enabled: boolean; +} + +export interface BridgeDescription { + uuid: string; + enabled: boolean; + productID: string; + vendorID: string; + passcode: string; + name: string; + list: DeviceDescription[]; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index a5d4bf0d..91609f08 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,28 +9,8 @@ import { StorageIoBroker } from './matter/StorageIoBroker'; import { SubscribeManager, DeviceFabric, GenericDevice } from './lib'; import { DetectedDevice } from './lib/devices/GenericDevice'; import BridgedDevice from './matter/BridgedDevicesNode'; - -interface MatterAdapterConfig extends ioBroker.AdapterConfig { - interface: string; -} - -interface DeviceDescription { - uuid: string; - name: string; - oid: string; - type: string; - enabled: boolean; -} - -interface BridgeDescription { - uuid: string; - enabled: boolean; - productID: string; - vendorID: string; - passcode: string; - name: string; - list: DeviceDescription[]; -} +import { MatterAdapterConfig, DeviceDescription, BridgeDescription } from './ioBrokerStorageTypes'; +import { Level, Logger } from '@project-chip/matter-node.js/log'; export class MatterAdapter extends utils.Adapter { private detector: ChannelDetectorType; @@ -48,7 +28,7 @@ export class MatterAdapter extends utils.Adapter { name: 'matter', uiClientSubscribe: data => this.onClientSubscribe(data.clientId), uiClientUnsubscribe: data => { - const { clientId, message, reason } = data; + const { clientId, reason } = data; if (reason === 'client') { this.log.debug(`GUI Client "${clientId} disconnected`); } else { @@ -66,7 +46,7 @@ export class MatterAdapter extends utils.Adapter { this.detector = new ChannelDetector(); } - async onClientSubscribe(clientId: string): Promise<{error?: string, accepted: boolean, heartbeat?: number}> { + async onClientSubscribe(clientId: string): Promise<{ error?: string, accepted: boolean, heartbeat?: number }> { this.log.debug(`Subscribe from ${clientId}`); if (!this._guiSubscribes) { return { error: `Adapter is still initializing`,accepted: false }; @@ -93,7 +73,7 @@ export class MatterAdapter extends utils.Adapter { return { accepted: true, heartbeat: 120000 }; } - onClientUnsubscribe(clientId: string) { + onClientUnsubscribe(clientId: string): void { this.log.debug(`Unsubscribe from ${clientId}`); if (!this._guiSubscribes) { return; @@ -109,7 +89,7 @@ export class MatterAdapter extends utils.Adapter { } while(deleted); } - sendToGui = async (data: any): Promise => { + sendToGui = async(data: any): Promise => { if (!this._guiSubscribes) { return; } @@ -118,10 +98,30 @@ export class MatterAdapter extends utils.Adapter { await this.sendToUI({ clientId: this._guiSubscribes[i].clientId, data }); } } - } + }; - async createMatterServer() { + async createMatterServer(): Promise { const config: MatterAdapterConfig = this.config as MatterAdapterConfig; + Logger.defaultLogLevel = Level.DEBUG; + Logger.log = (level: Level, formattedLog: string) => { + switch (level) { + case Level.DEBUG: + this.log.silly(formattedLog); + break; + case Level.INFO: + this.log.debug(formattedLog); + break; + case Level.WARN: + this.log.info(formattedLog); + break; + case Level.ERROR: + this.log.warn(formattedLog); + break; + case Level.FATAL: + this.log.error(formattedLog); + break; + } + }; /** * Initialize the storage system. @@ -161,7 +161,7 @@ export class MatterAdapter extends utils.Adapter { } async requestNodeStates(): Promise { - for (let uuid in this.bridges) { + for (const uuid in this.bridges) { const state = await this.bridges[uuid].getState(); this.log.debug(`State of ${uuid} is ${state}`); } @@ -279,11 +279,9 @@ export class MatterAdapter extends utils.Adapter { async createBridge(uuid: string, options: BridgeDescription): Promise { if (this.matterServer) { const devices = []; - for (let l = 0; l < options.list.length; l++) { - const device = options.list[l]; - if (device.enabled === false) { - continue; - } + const optionsList = (options.list || []).filter(item => item.enabled !== false); + for (let l = 0; l < optionsList.length; l++) { + const device = optionsList[l]; const detectedDevice = await this.getDeviceStates(device.oid) as DetectedDevice; if (detectedDevice) { const deviceObject = await DeviceFabric(detectedDevice, this); @@ -303,8 +301,8 @@ export class MatterAdapter extends utils.Adapter { devicename: options.name, productname: `Product ${options.name}`, }, - adapter: this, devices, + devicesOptions: optionsList, matterServer: this.matterServer, sendToGui: this.sendToGui, }); diff --git a/src/matter/BridgedDevicesNode.ts b/src/matter/BridgedDevicesNode.ts index 5054529c..aa4eaeb1 100644 --- a/src/matter/BridgedDevicesNode.ts +++ b/src/matter/BridgedDevicesNode.ts @@ -1,17 +1,19 @@ import { CommissioningServer, MatterServer } from '@project-chip/matter-node.js'; import { VendorId } from '@project-chip/matter-node.js/datatype'; -import { Aggregator, DeviceTypes, OnOffPluginUnitDevice } from '@project-chip/matter-node.js/device'; -import { toJson } from '@project-chip/matter.js/storage'; +import { Aggregator, DeviceTypes } from '@project-chip/matter-node.js/device'; -import { GenericDevice, Socket } from '../lib'; +import { GenericDevice } from '../lib'; +import { DeviceDescription } from '../ioBrokerStorageTypes'; + +import matterDeviceFabric from './matterFabric'; export interface BridgeCreateOptions { - adapter: ioBroker.Adapter; parameters: BridgeOptions, devices: GenericDevice[]; sendToGui: (data: any) => Promise; matterServer: MatterServer; + devicesOptions: DeviceDescription[]; } export interface BridgeOptions { @@ -30,23 +32,24 @@ export enum BridgeStates { Commissioned = 'commissioned', } + class BridgedDevice { private matterServer: MatterServer | undefined; - private adapter: ioBroker.Adapter; private parameters: BridgeOptions; private devices: GenericDevice[]; private sendToGui: (data: any) => Promise | undefined; private commissioningServer: CommissioningServer | undefined; + private devicesOptions: DeviceDescription[]; constructor(options: BridgeCreateOptions) { - this.adapter = options.adapter; this.parameters = options.parameters; this.devices = options.devices; this.sendToGui = options.sendToGui; this.matterServer = options.matterServer; + this.devicesOptions = options.devicesOptions; } - async init() { + async init(): Promise { /** * Collect all needed data * @@ -57,9 +60,9 @@ class BridgedDevice { * and easy reuse. When you also do that be careful to not overlap with Matter-Server own contexts * (so maybe better not ;-)). */ - const deviceName = this.parameters.devicename || "Matter Bridge device"; + const deviceName = this.parameters.devicename || 'Matter Bridge device'; const deviceType = DeviceTypes.AGGREGATOR.code; - const vendorName = "ioBroker"; + const vendorName = 'ioBroker'; const passcode = this.parameters.passcode; // 20202021; const discriminator = this.parameters.discriminator; // 3840); @@ -116,24 +119,30 @@ class BridgedDevice { const aggregator = new Aggregator(); for (let i = 1; i <= this.devices.length; i++) { - const device = this.devices[i - 1] as Socket; - const onOffDevice = new OnOffPluginUnitDevice(); - - onOffDevice.addOnOffListener(on => device.setPower(on)); - onOffDevice.addCommandHandler('identify', async ({request: {identifyTime}}) => { - console.log( - `Identify called for OnOffDevice ${onOffDevice.name} with id: ${i} and identifyTime: ${identifyTime}`, - ); - }); - - const name = `OnOff Socket ${i}`; - aggregator.addBridgedDevice(onOffDevice, { - nodeLabel: name, - productName: name, - productLabel: name, - uniqueId: i.toString().padStart(4, '0') + uniqueId.substring(4), - reachable: true, - }); + const ioBrokerDevice = this.devices[i - 1] as GenericDevice; + const mappingDevice = await matterDeviceFabric(ioBrokerDevice, this.devicesOptions[i - 1].name, this.devicesOptions[i - 1].uuid); + if (mappingDevice) { + const name = mappingDevice.getName();// `OnOff Socket ${i}`; + aggregator.addBridgedDevice(mappingDevice.getMatterDevice(), { + nodeLabel: name, + productName: name, + productLabel: name, + uniqueId: this.devicesOptions[i - 1].uuid[i - 1].replace(/-/g, ''), + reachable: true, + }); + } else { + console.error(`ioBroker Device "${this.devices[i - 1].getDeviceType()}" is not supported`); + } + + // const onOffDevice = new OnOffPluginUnitDevice(); + // + // onOffDevice.setOnOff(true); + // onOffDevice.addOnOffListener(on => device.setPower(on)); + // onOffDevice.addCommandHandler('identify', async ({request: {identifyTime}}) => { + // console.log( + // `Identify called for OnOffDevice ${onOffDevice.name} with id: ${i} and identifyTime: ${identifyTime}`, + // ); + // }); } this.commissioningServer.addDevice(aggregator); @@ -178,19 +187,34 @@ class BridgedDevice { const activeSession = this.commissioningServer.getActiveSessionInformation(); const fabric = this.commissioningServer.getCommissionedFabricInformation(); + const connectionInfo: any = activeSession.map(session => ({ + vendor: session?.fabric?.rootVendorId, + connected: !!session.numberOfActiveSubscriptions, + label: session?.fabric?.label, + })); + + fabric.forEach(fabric => { + if (!activeSession.find(session => session.fabric?.fabricId === fabric.fabricId)) { + connectionInfo.push({ + vendor: fabric?.rootVendorId, + connected: false, + label: fabric?.label, + }); + } + }); + this.sendToGui({ uuid: this.parameters.uuid, command: 'status', data: 'connecting', - activeSession: toJson(activeSession), - fabric: toJson(fabric), + connectionInfo, }); console.log('Device is already commissioned. Waiting for controllers to connect ...'); return BridgeStates.Commissioned; } } - async stop() { + async stop(): Promise { for (let d = 0; d < this.devices.length; d++) { await this.devices[d].destroy(); } diff --git a/src/matter/StorageIoBroker.ts b/src/matter/StorageIoBroker.ts index 3cfb8dad..6d4f93be 100644 --- a/src/matter/StorageIoBroker.ts +++ b/src/matter/StorageIoBroker.ts @@ -16,10 +16,10 @@ export class StorageIoBroker implements Storage { this.createdKeys = {}; } - async initialize() { + async initialize(): Promise { let object; try { - object = await this.adapter.getForeignObjectAsync(this.oid); + object = await this.adapter.getForeignObjectAsync(this.oid); } catch (error) { // create object object = { @@ -46,14 +46,14 @@ export class StorageIoBroker implements Storage { // read all keys const states = await this.adapter.getForeignStatesAsync(`${this.oid}.*`); - const len = this.oid.length + 1 + const len = this.oid.length + 1; for (const key in states) { this.createdKeys[key] = true; this.data[key.substring(len)] = fromJson(states[key].val as string); } } - async close() { + async close(): Promise { const keys = Object.keys(this.savingPromises); if (keys.length) { await Promise.all(keys.map(key => this.savingPromises[key])); @@ -75,7 +75,7 @@ export class StorageIoBroker implements Storage { return value as T; } - saveKey(oid: string, value: string) { + saveKey(oid: string, value: string): void { const index = this.savingNumber++; if (this.savingNumber >= 0xFFFFFFFF) { this.savingNumber = 1; @@ -110,7 +110,7 @@ export class StorageIoBroker implements Storage { } } - deleteKey(oid: string) { + deleteKey(oid: string): void { const index = this.savingNumber++; if (this.createdKeys[oid]) { if (this.savingNumber >= 0xFFFFFFFF) { diff --git a/src/matter/matterFabric.ts b/src/matter/matterFabric.ts new file mode 100644 index 00000000..c6e406e9 --- /dev/null +++ b/src/matter/matterFabric.ts @@ -0,0 +1,28 @@ +import { GenericDevice } from '../lib'; +import { MappingGenericDevice } from './devices/MappingGenericDevice'; +import { DeviceType } from '../lib/devices/GenericDevice'; + +import { MappingLight } from './devices/MappingLight'; +import { MappingSocket } from './devices/MappingSocket'; + +async function matterDeviceFabric(ioBrokerDevice: GenericDevice, name: string, uuid?: string): Promise { + const ioBrokerDeviceType = ioBrokerDevice.getDeviceType(); + let matterDevice: MappingGenericDevice | null = null; + + if (ioBrokerDeviceType === DeviceType.Light) { + matterDevice = new MappingLight(ioBrokerDevice, name, uuid); + } + + if (ioBrokerDeviceType === DeviceType.Socket) { + matterDevice = new MappingSocket(ioBrokerDevice, name, uuid); + } + + if (matterDevice) { + await matterDevice.init(); + return matterDevice; + } + + return null; +} + +export default matterDeviceFabric; \ No newline at end of file