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;
}