diff --git a/README.md b/README.md index 44f2193b..9ceac5df 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - fake delete (so the real deletion will be done in backend with factory reset of device) ## Changelog -### 0.1.9 (2023-11-12) +### **WORK IN PROGRESS** * (bluefox) Implemented the factory reset and re-announcing ### 0.1.2 (2023-10-25) diff --git a/src-admin/src/Tabs/Controller.js b/src-admin/src/Tabs/Controller.js index 5112847f..be2b83ea 100644 --- a/src-admin/src/Tabs/Controller.js +++ b/src-admin/src/Tabs/Controller.js @@ -97,6 +97,7 @@ class Controller extends React.Component { camera: '', hideVideo: false, nodes: {}, + states: {}, openedNodes, }; @@ -152,10 +153,15 @@ class Controller extends React.Component { .then(() => this.props.socket.subscribeObject(`matter.${this.props.instance}.controller.*`, this.onObjectChange) .catch(e => window.alert(`Cannot subscribe: ${e}`))) .then(() => this.props.socket.subscribeState(`matter.${this.props.instance}.controller.*`, this.onStateChange) - .catch(e => window.alert(`Cannot subscribe: ${e}`))); + .catch(e => { + window.alert(`Cannot subscribe 1: ${e}`); + })); } onObjectChange = (id, obj) => { + if (!this.state.nodes) { + return; + } const nodes = JSON.parse(JSON.stringify(this.state.nodes)); if (obj) { nodes[id] = obj; @@ -165,7 +171,10 @@ class Controller extends React.Component { this.setState({ nodes }); }; - onStateChange(id, state) { + onStateChange = (id, state) => { + if (!this.state.states) { + return; + } const states = JSON.parse(JSON.stringify(this.state.states)); if (state) { states[id] = state; @@ -173,7 +182,7 @@ class Controller extends React.Component { delete states[id]; } this.setState({ states }); - } + }; async componentWillUnmount() { this.props.registerMessageHandler(null); @@ -322,15 +331,13 @@ class Controller extends React.Component { {this.state.discoveryRunning ? : null} - - - {I18n.t('Name')} - - - {I18n.t('Identifier')} - - - + + {I18n.t('Name')} + + + {I18n.t('Identifier')} + + {this.state.discovered.map(device => @@ -384,7 +391,7 @@ class Controller extends React.Component { {icon} {stateId.split('.').pop()} - {this.state.states[stateId]?.val || '--'} + {this.state.states[stateId] && this.state.states[stateId].val !== null && this.state.states[stateId].val !== undefined ? this.state.states[stateId].val.toString() : '--'} ; } @@ -496,9 +503,11 @@ class Controller extends React.Component { : null}
- - {I18n.t('Name')} - {I18n.t('Value')} + + + {I18n.t('Name')} + {I18n.t('Value')} + {this.renderNodes()} diff --git a/src-admin/src/components/ConfigHandler.js b/src-admin/src/components/ConfigHandler.js index 3f762635..56f96330 100644 --- a/src-admin/src/components/ConfigHandler.js +++ b/src-admin/src/components/ConfigHandler.js @@ -322,7 +322,20 @@ class ConfigHandler { // compare config with this.config if (JSON.stringify(config.controller) !== JSON.stringify(this.config.controller)) { - const controller = await this.socket.getObject(`matter.${this.instance}.controller`); + let controller = await this.socket.getObject(`matter.${this.instance}.controller`); + if (!controller) { + controller = { + type: 'folder', + common: { + name: 'Matter controller', + }, + native: { + enabled: false, + ble: false, + uuid: uuidv4(), + }, + }; + } controller.native.enabled = config.controller.enabled; this.config.controller.enabled = config.controller.enabled; await this.socket.setObject(controller._id, controller); diff --git a/src/main.ts b/src/main.ts index a57c3ca0..64f8e224 100644 --- a/src/main.ts +++ b/src/main.ts @@ -116,7 +116,14 @@ export class MatterAdapter extends utils.Adapter { this.log.debug('Resetting'); await this.matterServer?.close(); await this.storage?.clearAll(); - await this.onReady(); + // clear all nodes in the controller + await this.delObjectAsync('controller', { recursive: true }); + + // restart adapter + const obj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`); + if (obj) { + await this.setForeignObjectAsync(obj._id, obj); + } } async onMessage(obj: ioBroker.Message): Promise { diff --git a/src/matter/ControllerNode.ts b/src/matter/ControllerNode.ts index 46dff687..a4368bd9 100644 --- a/src/matter/ControllerNode.ts +++ b/src/matter/ControllerNode.ts @@ -76,6 +76,13 @@ const IGNORE_CLUSTERS: ClusterId[] = [ 0x0046 as ClusterId, // ICD Management S ]; +interface Device { + clusters: Base[]; + nodeId: string; + connectionStateId?: string; + connectionStatusId?: string; +} + class Controller { private matterServer: MatterServer | undefined; private parameters: ControllerOptions; @@ -83,7 +90,8 @@ class Controller { private adapter: MatterAdapter; private commissioningController: CommissioningController | null = null; private matterNodeIds: NodeId[] = []; - private clusters: Base[] = []; + private devices: Device[] = []; + private delayedStates: { [nodeId: string]: NodeStateInformation } = {}; constructor(options: ControllerCreateOptions) { this.adapter = options.adapter; @@ -126,7 +134,18 @@ class Controller { )}`, ); }, - stateInformationCallback: (peerNodeId: NodeId, info: NodeStateInformation) => { + stateInformationCallback: async (peerNodeId: NodeId, info: NodeStateInformation) => { + const jsonNodeId = Logger.toJSON(peerNodeId).replace(/"/g, ''); + const device: Device | undefined = this.devices.find(device => device.nodeId === jsonNodeId); + if (device) { + device.connectionStateId && (await this.adapter.setStateAsync(device.connectionStateId, info === NodeStateInformation.Connected, true)); + device.connectionStatusId && (await this.adapter.setStateAsync(device.connectionStatusId, info, true)); + } else { + this.adapter.log.warn(`Device ${Logger.toJSON(peerNodeId)} not found`); + // delayed state + this.delayedStates[jsonNodeId] = info; + } + switch (info) { case NodeStateInformation.Connected: this.adapter.log.debug(`stateInformationCallback ${peerNodeId}: Node ${originalNodeId} connected`); @@ -346,9 +365,12 @@ class Controller { changed = true; deviceObj = { _id: id, - type: 'device', + type: 'folder', common: { name: nodeIdString, + statusStates: { + onlineId: 'info.connection', + }, }, native: { nodeId: Logger.toJSON(nodeObject.nodeId), @@ -357,6 +379,83 @@ class Controller { await this.adapter.setObjectAsync(deviceObj._id, deviceObj); } + const device: Device = { + nodeId: Logger.toJSON(nodeObject.nodeId), + clusters: [], + }; + + this.devices.push(device); + + const infoId = `controller.${nodeIdString}.info`; + let infoObj = await this.adapter.getObjectAsync(infoId); + if (!infoObj) { + infoObj = { + _id: infoId, + type: 'channel', + common: { + name: 'Connection info', + }, + native: { + + }, + }; + await this.adapter.setObjectAsync(infoObj._id, infoObj); + } + + const infoConnectionId = `controller.${nodeIdString}.info.connection`; + let infoConnectionObj = await this.adapter.getObjectAsync(infoConnectionId); + if (!infoConnectionObj) { + infoConnectionObj = { + _id: infoConnectionId, + type: 'state', + common: { + name: 'Connected', + role: 'indicator.connected', + type: 'boolean', + read: true, + write: false, + }, + native: { + + }, + }; + await this.adapter.setObjectAsync(infoConnectionObj._id, infoConnectionObj); + } + device.connectionStateId = infoConnectionId; + + const infoStatusId = `controller.${nodeIdString}.info.status`; + let infoStatusObj = await this.adapter.getObjectAsync(infoStatusId); + if (!infoStatusObj) { + infoStatusObj = { + _id: infoStatusId, + type: 'state', + common: { + name: 'Connection status', + role: 'state', + type: 'number', + states: { + [NodeStateInformation.Connected]: 'connected', + [NodeStateInformation.Disconnected]: 'disconnected', + [NodeStateInformation.Reconnecting]: 'reconnecting', + [NodeStateInformation.WaitingForDeviceDiscovery]: 'waitingForDeviceDiscovery', + [NodeStateInformation.StructureChanged]: 'structureChanged', + }, + read: true, + write: false, + }, + native: { + + }, + }; + await this.adapter.setObjectAsync(infoStatusObj._id, infoStatusObj); + } + device.connectionStatusId = infoStatusId; + if (this.delayedStates[nodeIdString] !== undefined) { + await this.adapter.setStateAsync(infoConnectionId, this.delayedStates[nodeIdString] === NodeStateInformation.Connected, true); + await this.adapter.setStateAsync(infoStatusId, this.delayedStates[nodeIdString], true); + delete this.delayedStates[nodeIdString]; + } + // Example to initialize a ClusterClient and access concrete fields as API methods // const descriptor = nodeObject.getRootClusterClient(DescriptorCluster); // if (descriptor !== undefined) { @@ -382,22 +481,22 @@ class Controller { await this.adapter.setObjectAsync(deviceObj._id, deviceObj); } - await this.endPointToIoBrokerStructure(nodeObject.nodeId, rootEndpoint, 0); + await this.endPointToIoBrokerStructure(nodeObject.nodeId, rootEndpoint, 0, [], device); } } - addCluster(cluster: Base | undefined) { + addCluster(device: Device, cluster: Base | undefined) { if (cluster) { - this.clusters.push(cluster); + device.clusters.push(cluster); } } - async endPointToIoBrokerStructure(nodeId: NodeId, endpoint: Endpoint, level: number): Promise { + async endPointToIoBrokerStructure(nodeId: NodeId, endpoint: Endpoint, level: number, path: number[], device: Device): Promise { this.adapter.log.info(`${''.padStart(level * 2)}Endpoint ${endpoint.id} (${endpoint.name}):`); - const keys = Object.keys(Factories); - for (let f = 0; f < Factories.length; f++) { - const factory = Factories[f]; - this.addCluster(await factory(this.adapter, nodeId, endpoint)); + if (level) { + for (let f = 0; f < Factories.length; f++) { + this.addCluster(device, await Factories[f](this.adapter, nodeId, endpoint, path)); + } } // for (const clusterServer of endpoint.getAllClusterServers()) { @@ -414,8 +513,10 @@ class Controller { // } const endpoints = endpoint.getChildEndpoints(); - for (const childEndpoint of endpoints) { - await this.endPointToIoBrokerStructure(nodeId, childEndpoint, level + 1); + path.push(0); + for (let i = 0; i < endpoints.length; i++) { + path[path.length - 1] = i; + await this.endPointToIoBrokerStructure(nodeId, endpoints[i], level + 1, path, device); } } @@ -540,12 +641,13 @@ class Controller { } async stop(): Promise { - for (let i = 0; i < this.clusters.length; i++) { - const cluster = this.clusters[i]; - await cluster.destroy(); + for (let d = 0; d < this.devices.length; d++) { + for (let c = 0; c < this.devices[d].clusters.length; c++) { + await this.devices[d].clusters[c].destroy(); + } } - this.clusters = []; + this.devices = []; if (this.commissioningController) { this.matterServer?.removeCommissioningController(this.commissioningController); diff --git a/src/matter/clusters/Base.ts b/src/matter/clusters/Base.ts index f657b1ed..7efb495b 100644 --- a/src/matter/clusters/Base.ts +++ b/src/matter/clusters/Base.ts @@ -11,10 +11,36 @@ class Base { protected adapter: MatterAdapter; protected endpoint: Endpoint; private subscribes: Record void)[]> = {}; + protected prefix: string; + protected jsonNodeId: string; - constructor(adapter: MatterAdapter, endpoint: Endpoint) { + constructor(adapter: MatterAdapter, nodeId: NodeId, endpoint: Endpoint, path: number[]) { this.adapter = adapter; this.endpoint = endpoint; + this.jsonNodeId = Base.toJSON(nodeId).replace(/"/g, ''); + + if (path.length > 1) { + const newPath = [...path]; + newPath.shift(); + this.prefix = `${newPath.join('.')}.`; + // create folder + const id = `controller.${this.jsonNodeId.replace(/"/g, '')}.${this.prefix.substring(0, this.prefix.length - 1)}`; + this.adapter.getObjectAsync(id) + .then(obj => { + if (!obj) { + this.adapter.setObjectAsync(id, { + type: 'device', + common: { + name: `Device ${this.prefix.substring(0, this.prefix.length - 1)}`, + }, + native: { + }, + }); + } + }); + } else { + this.prefix = ''; + } } static toJSON(nodeId: NodeId): string { @@ -37,7 +63,7 @@ class Base { } } - async createChannel(nodeId: NodeId, deviceTypes: AtLeastOne) { + async createChannel(deviceTypes: AtLeastOne) { if (!deviceTypes) { return; } @@ -46,7 +72,7 @@ class Base { const deviceType = deviceTypes.find(type => type.name !== 'MA-bridgednode'); // create onOff - const id = `controller.${Base.toJSON(nodeId).replace(/"/g, '')}.states`; + const id = `controller.${this.jsonNodeId.replace(/"/g, '')}.${this.prefix}states`; let stateObj = await this.adapter.getObjectAsync(id); if (!stateObj) { stateObj = { @@ -57,7 +83,7 @@ class Base { (deviceTypes[0] ? deviceTypes[0].name.replace(/^MA-/, '') :'Unknown'), }, native: { - nodeId: Base.toJSON(nodeId), + nodeId: this.jsonNodeId, }, }; await this.adapter.setObjectAsync(stateObj._id, stateObj); @@ -67,16 +93,15 @@ class Base { async createState( id: string, common: ioBroker.StateCommon, - jsonNodeId: string, clusterId: number, currentValue: any | undefined = undefined, ): Promise { let _id; // create onOff if (id.includes('.')) { - _id = `controller.${jsonNodeId.replace(/"/g, '')}.${id}`; + _id = `controller.${this.jsonNodeId.replace(/"/g, '')}.${this.prefix}${id}`; } else { - _id = `controller.${jsonNodeId.replace(/"/g, '')}.states.${id}`; + _id = `controller.${this.jsonNodeId.replace(/"/g, '')}.${this.prefix}states.${id}`; } let stateObj = await this.adapter.getObjectAsync(id); @@ -88,8 +113,7 @@ class Base { ...common, }, native: { - nodeId: jsonNodeId, - clusterId: clusterId, + clusterId, }, }; await this.adapter.setObjectAsync(stateObj._id, stateObj); diff --git a/src/matter/clusters/BooleanState.ts b/src/matter/clusters/BooleanState.ts index 063275d8..23ec65de 100644 --- a/src/matter/clusters/BooleanState.ts +++ b/src/matter/clusters/BooleanState.ts @@ -8,12 +8,12 @@ import { MatterAdapter } from '../../main'; class BooleanState extends Base { private handler: ((value: boolean) => void) | undefined = undefined; - async init(nodeId: NodeId) { + async init() { const cluster = this.endpoint.getClusterClient(BooleanStateCluster); if (!cluster) { return; } - await this.createChannel(nodeId, this.endpoint.getDeviceTypes()); + await this.createChannel(this.endpoint.getDeviceTypes()); const id = await this.createState( 'booleanState', { @@ -23,7 +23,6 @@ class BooleanState extends Base { read: true, write: false, }, - Base.toJSON(nodeId), cluster.id, await cluster.getStateValueAttribute(), ); @@ -44,14 +43,14 @@ class BooleanState extends Base { } } - static async factory(adapter: MatterAdapter, nodeId: NodeId, endpoint: Endpoint): Promise { + static async factory(adapter: MatterAdapter, nodeId: NodeId, endpoint: Endpoint, path: number[]): Promise { const cluster = endpoint.getClusterClient(BooleanStateCluster); if (!cluster) { return; } - const result = new BooleanState(adapter, endpoint); + const result = new BooleanState(adapter, nodeId, endpoint, path); if (result) { - await result.init(nodeId); + await result.init(); } return result; } diff --git a/src/matter/clusters/Identify.ts b/src/matter/clusters/Identify.ts index b884b51b..4b568c7f 100644 --- a/src/matter/clusters/Identify.ts +++ b/src/matter/clusters/Identify.ts @@ -9,15 +9,15 @@ class Identify extends Base { private handlerType: ((value: number) => void) | undefined = undefined; private handlerTime: ((value: number) => void) | undefined = undefined; - async init(nodeId: NodeId) { + async init() { const cluster = this.endpoint.getClusterClient(IdentifyCluster); if (!cluster) { return; } - await this.createChannel(nodeId, this.endpoint.getDeviceTypes()); + await this.createChannel(this.endpoint.getDeviceTypes()); // create Identify channel - const _id = `controller.${Base.toJSON(nodeId).replace(/"/g, '')}.identify`; + const _id = `controller.${this.jsonNodeId.replace(/"/g, '')}.${this.prefix}identify`; let channelObj = await this.adapter.getObjectAsync(_id); if (!channelObj) { channelObj = { @@ -27,7 +27,7 @@ class Identify extends Base { name: 'Identify', }, native: { - nodeId: Base.toJSON(nodeId), + nodeId: this.jsonNodeId, clusterId: cluster.id, }, }; @@ -52,7 +52,6 @@ class Identify extends Base { write: true, read: false, }, - Base.toJSON(nodeId), cluster.id, await cluster.getIdentifyTypeAttribute(), ); @@ -67,7 +66,6 @@ class Identify extends Base { write: true, read: false, }, - Base.toJSON(nodeId), cluster.id, await cluster.getIdentifyTimeAttribute(), ); @@ -92,7 +90,6 @@ class Identify extends Base { write: true, read: false, }, - Base.toJSON(nodeId), cluster.id, false, ); @@ -126,14 +123,14 @@ class Identify extends Base { } } - static async factory(adapter: MatterAdapter, nodeId: NodeId, endpoint: Endpoint): Promise { + static async factory(adapter: MatterAdapter, nodeId: NodeId, endpoint: Endpoint, path: number[]): Promise { const cluster = endpoint.getClusterClient(IdentifyCluster); if (!cluster) { return; } - const result = new Identify(adapter, endpoint); + const result = new Identify(adapter, nodeId, endpoint, path); if (result) { - await result.init(nodeId); + await result.init(); } return result; } diff --git a/src/matter/clusters/LevelControl.ts b/src/matter/clusters/LevelControl.ts index c46a4b0e..7799fe9b 100644 --- a/src/matter/clusters/LevelControl.ts +++ b/src/matter/clusters/LevelControl.ts @@ -8,61 +8,31 @@ import { MatterAdapter } from '../../main'; class LevelControl extends Base { private handler: ((value: number | null) => void) | undefined = undefined; - async init(nodeId: NodeId) { + async init() { const cluster = this.endpoint.getClusterClient(LevelControlCluster); if (!cluster) { return; } - await this.createChannel(nodeId, this.endpoint.getDeviceTypes()); + await this.createChannel(this.endpoint.getDeviceTypes()); const features = await cluster.getFeatureMapAttribute(); - // create onOff - const id = `controller.${Base.toJSON(nodeId).replace(/"/g, '')}.states.level`; - let stateObj = await this.adapter.getObjectAsync(id); const max = await cluster.getMaxLevelAttribute(); const min = await cluster.getMinLevelAttribute(); - let changed = false; - - if (!stateObj) { - changed = true; - stateObj = { - _id: id, - type: 'state', - common: { - name: 'OnOff', - type: 'boolean', - role: features.lighting ? 'level.dimmer' : 'level', - read: true, - write: true, - min, - max, - }, - native: { - nodeId: Base.toJSON(nodeId), - clusterId: cluster.id, - }, - }; - } else { - if (stateObj.common.min !== min) { - changed = true; - stateObj.common.min = min; - } - if (stateObj.common.max !== max) { - changed = true; - stateObj.common.max = max; - } - } - if (changed) { - await this.adapter.setObjectAsync(stateObj._id, stateObj); - } - - const state = await this.adapter.getStateAsync(id); - - // init state - let level = await cluster.getCurrentLevelAttribute(); - if (!state || state.val !== level) { - await this.adapter.setStateAsync(id, level, true); - } + // create onOff + const id = await this.createState( + 'level', + { + name: 'OnOff', + type: 'boolean', + role: features.lighting ? 'level.dimmer' : 'level', + read: true, + write: true, + min, + max, + }, + cluster.id, + await cluster.getCurrentLevelAttribute() + ); this.handler = async (value: number | null) => { await this.adapter.setStateAsync(id, value, true); @@ -103,14 +73,14 @@ class LevelControl extends Base { } } - static async factory(adapter: MatterAdapter, nodeId: NodeId, endpoint: Endpoint): Promise { + static async factory(adapter: MatterAdapter, nodeId: NodeId, endpoint: Endpoint, path: number[]): Promise { const cluster = endpoint.getClusterClient(LevelControlCluster); if (!cluster) { return; } - const result = new LevelControl(adapter, endpoint); + const result = new LevelControl(adapter, nodeId, endpoint, path); if (result) { - await result.init(nodeId); + await result.init(); } return result; } diff --git a/src/matter/clusters/OnOff.ts b/src/matter/clusters/OnOff.ts index 4da5993e..8f340f7e 100644 --- a/src/matter/clusters/OnOff.ts +++ b/src/matter/clusters/OnOff.ts @@ -8,12 +8,12 @@ import { MatterAdapter } from '../../main'; class OnOff extends Base { private handler: ((value: boolean) => void) | undefined = undefined; - async init(nodeId: NodeId) { + async init() { const cluster = this.endpoint.getClusterClient(OnOffCluster); if (!cluster) { return; } - await this.createChannel(nodeId, this.endpoint.getDeviceTypes()); + await this.createChannel(this.endpoint.getDeviceTypes()); const features = await cluster.getFeatureMapAttribute(); // create onOff const id = await this.createState( @@ -25,7 +25,6 @@ class OnOff extends Base { read: true, write: true, }, - Base.toJSON(nodeId), cluster.id, await cluster.getOnOffAttribute(), ); @@ -62,14 +61,14 @@ class OnOff extends Base { } } - static async factory(adapter: MatterAdapter, nodeId: NodeId, endpoint: Endpoint): Promise { + static async factory(adapter: MatterAdapter, nodeId: NodeId, endpoint: Endpoint, path: number[]): Promise { const cluster = endpoint.getClusterClient(OnOffCluster); if (!cluster) { return; } - const result = new OnOff(adapter, endpoint); + const result = new OnOff(adapter, nodeId, endpoint, path); if (result) { - await result.init(nodeId); + await result.init(); } return result; }