Skip to content

Commit

Permalink
Introduce Device callbacks for session and commissioning changes (#468)
Browse files Browse the repository at this point in the history
* Expose fabric index too to use to filter data for fabrics

* Add Fabric removal callback

Used to inform upper layers of a change in fabrics

* Add Subscription changed callback

Used to inform upper layers of a change in subscriptions

* Add high level commissioning/session changed callbacks to device

To inform the developer about things happend with the device.

* Add needed MatterDevice code

* Adjust tests

* Add callbacks to example

* Enhance methods to filter by fabricIndex

* Adjust tests

* Remove whats already implemented elswhere

* Call callback a bit earlier

* Expose nodeId from controller

* make sure to use current fabric

(because could be added later then initialization)

* Add server callbacks to tests

* Rename variable to know meaning

* typo

* Address review feedback

* adjust tests
  • Loading branch information
Apollon77 authored Nov 1, 2023
1 parent 1b09b05 commit a54aec2
Show file tree
Hide file tree
Showing 17 changed files with 463 additions and 193 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The main work (all changes without a GitHub username in brackets in the below li
* Fix: Handles event data correctly on subscription initially and also on updates to trigger the listeners
* Enhance (vilic): Added MDNS Memberships to sockets for better operation on Windows and other platforms
* Enhance: Refactor session management and make sure also controller handle session close requests from devices
* Enhance: Refactor close handing for exchanges and channels to make sure they are closed correctly
* Feature: Added detection of missing Subscription updates from a device and allow to react to such a timeout with callback
* Feature: Added generation method for random passcodes to PaseClient
* Feature: Generalized Discovery logic and allow discoveries via different methods (BLE+IP) in parallel
Expand Down
12 changes: 12 additions & 0 deletions packages/matter-node.js-examples/src/examples/DeviceNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ class Device {
serialNumber: `node-matter-${uniqueId}`,
},
delayedAnnouncement: hasParameter("ble"), // Delay announcement when BLE is used to show how limited advertisement works
activeSessionsChangedCallback: fabricIndex => {
console.log(
`activeSessionsChangedCallback: Active sessions changed on Fabric ${fabricIndex}`,
commissioningServer.getActiveSessionInformation(fabricIndex),
);
},
commissioningChangedCallback: fabricIndex => {
console.log(
`commissioningChangedCallback: Commissioning changed on Fabric ${fabricIndex}`,
commissioningServer.getCommissionedFabricInformation(fabricIndex)[0],
);
},
});

// optionally add a listener for the testEventTrigger command from the GeneralDiagnostics cluster
Expand Down
126 changes: 123 additions & 3 deletions packages/matter-node.js/test/IntegrationTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ describe("Integration Test", () => {
let serverMdnsScanner: MdnsScanner;
let clientMdnsScanner: MdnsScanner;
let mdnsBroadcaster: MdnsBroadcaster;
const commissioningChangedCallsServer = new Array<{ fabricIndex: FabricIndex; time: number }>();
const commissioningChangedCallsServer2 = new Array<{ fabricIndex: FabricIndex; time: number }>();
const sessionChangedCallsServer = new Array<{ fabricIndex: FabricIndex; time: number }>();
const sessionChangedCallsServer2 = new Array<{ fabricIndex: FabricIndex; time: number }>();

before(async () => {
MockTime.reset(TIME_START);
Expand Down Expand Up @@ -133,6 +137,10 @@ describe("Integration Test", () => {
reachable: true,
},
delayedAnnouncement: true, // delay because we need to override Mdns classes
commissioningChangedCallback: (fabricIndex: FabricIndex) =>
commissioningChangedCallsServer.push({ fabricIndex, time: MockTime.nowMs() }),
activeSessionsChangedCallback: (fabricIndex: FabricIndex) =>
sessionChangedCallsServer.push({ fabricIndex, time: MockTime.nowMs() }),
});
assert.equal(commissioningServer.getPort(), undefined);

Expand Down Expand Up @@ -245,6 +253,9 @@ describe("Integration Test", () => {
const mockTimeInstance = Time.get();
Time.get = singleton(() => new TimeNode());

assert.equal(commissioningChangedCallsServer.length, 0);
assert.equal(sessionChangedCallsServer.length, 0);

await commissioningController.start();
const node = await commissioningController.commissionNode({
discovery: {
Expand All @@ -265,12 +276,30 @@ describe("Integration Test", () => {
};

assert.deepEqual(commissioningController.getCommissionedNodes(), [node.nodeId]);
assert.equal(commissioningChangedCallsServer.length, 1);
assert.equal(commissioningChangedCallsServer[0].fabricIndex, FabricIndex(1));
assert.equal(sessionChangedCallsServer.length, 1);
assert.equal(sessionChangedCallsServer[0].fabricIndex, FabricIndex(1));
const sessionInfo = commissioningServer.getActiveSessionInformation();
assert.equal(sessionInfo.length, 1);
assert.ok(sessionInfo[0].fabric);
assert.equal(sessionInfo[0].fabric.fabricIndex, FabricIndex(1));
assert.equal(sessionInfo[0].nodeId, node.nodeId);
});

it("We can connect to the new comissioned device", async () => {
it("We can connect to the new commissioned device", async () => {
const nodeId = commissioningController.getCommissionedNodes()[0];

await commissioningController.connectNode(nodeId);

assert.equal(commissioningChangedCallsServer.length, 1);
assert.equal(sessionChangedCallsServer.length, 1);
assert.equal(sessionChangedCallsServer[0].fabricIndex, FabricIndex(1));
const sessionInfo = commissioningServer.getActiveSessionInformation();
assert.equal(sessionInfo.length, 1);
assert.ok(sessionInfo[0].fabric);
assert.equal(sessionInfo[0].fabric.fabricIndex, FabricIndex(1));
assert.equal(sessionInfo[0].numberOfActiveSubscriptions, 0);
});

it("Subscribe to all Attributes and bind updates to them", async () => {
Expand All @@ -281,6 +310,15 @@ describe("Integration Test", () => {

assert.equal(Array.isArray(data.attributeReports), true);
assert.equal(Array.isArray(data.eventReports), true);

assert.equal(commissioningChangedCallsServer.length, 1);
assert.equal(sessionChangedCallsServer.length, 2);
assert.equal(sessionChangedCallsServer[1].fabricIndex, FabricIndex(1));
const sessionInfo = commissioningServer.getActiveSessionInformation();
assert.equal(sessionInfo.length, 1);
assert.ok(sessionInfo[0].fabric);
assert.equal(sessionInfo[0].fabric.fabricIndex, FabricIndex(1));
assert.equal(sessionInfo[0].numberOfActiveSubscriptions, 1);
});

it("Verify that commissioning changed the Regulatory Config/Location values", async () => {
Expand Down Expand Up @@ -827,6 +865,14 @@ describe("Integration Test", () => {
const lastReport = await lastPromise;

assert.deepEqual(lastReport, { value: false, time: startTime + (10 + 2) * 1000 + 200 });

assert.equal(commissioningChangedCallsServer.length, 1);
assert.equal(sessionChangedCallsServer.length, 3);
assert.equal(sessionChangedCallsServer[2].fabricIndex, FabricIndex(1));
const sessionInfo = commissioningServer.getActiveSessionInformation();
assert.equal(sessionInfo.length, 1);
assert.ok(sessionInfo[0].fabric);
assert.equal(sessionInfo[0].numberOfActiveSubscriptions, 2);
});

it("another additional subscription of one attribute with known data version only sends updates when the value changes", async () => {
Expand Down Expand Up @@ -1024,6 +1070,16 @@ describe("Integration Test", () => {
time: startTime + 200 + 101,
});
});

it("Check callback info", async () => {
assert.equal(commissioningChangedCallsServer.length, 1);
assert.ok(sessionChangedCallsServer.length >= 6); // not 100% accurate because of MockTime and not 100% finished responses and stuff like that
assert.equal(sessionChangedCallsServer[4].fabricIndex, FabricIndex(1));
const sessionInfo = commissioningServer.getActiveSessionInformation();
assert.equal(sessionInfo.length, 1);
assert.ok(sessionInfo[0].fabric);
assert.ok(sessionInfo[0].numberOfActiveSubscriptions >= 5);
});
});

describe("Access Control server fabric scoped attribute storage", () => {
Expand Down Expand Up @@ -1149,6 +1205,10 @@ describe("Integration Test", () => {
reachable: true,
},
delayedAnnouncement: true, // delay because we need to override Mdns classes
commissioningChangedCallback: (fabricIndex: FabricIndex) =>
commissioningChangedCallsServer2.push({ fabricIndex, time: MockTime.nowMs() }),
activeSessionsChangedCallback: (fabricIndex: FabricIndex) =>
sessionChangedCallsServer2.push({ fabricIndex, time: MockTime.nowMs() }),
});

onOffLightDeviceServer = new OnOffLightDevice();
Expand All @@ -1161,6 +1221,9 @@ describe("Integration Test", () => {
commissioningServer2.setMdnsBroadcaster(mdnsBroadcaster);

await assert.doesNotReject(async () => commissioningServer2.advertise());

assert.equal(commissioningChangedCallsServer2.length, 0);
assert.equal(sessionChangedCallsServer2.length, 0);
});

it("the client commissions the second device", async () => {
Expand All @@ -1186,12 +1249,28 @@ describe("Integration Test", () => {
Time.get = () => mockTimeInstance;

assert.deepEqual(commissioningController.getCommissionedNodes(), [...existingNodes, node.nodeId]);

assert.equal(commissioningChangedCallsServer2.length, 1);
assert.equal(sessionChangedCallsServer2.length, 1);
assert.equal(sessionChangedCallsServer2[0].fabricIndex, FabricIndex(1));
const sessionInfo = commissioningServer2.getActiveSessionInformation();
assert.equal(sessionInfo.length, 1);
assert.ok(sessionInfo[0].fabric);
assert.equal(sessionInfo[0].numberOfActiveSubscriptions, 0);
});

it("We can connect to the new commissioned device", async () => {
const nodeId = commissioningController.getCommissionedNodes()[1];

await commissioningController.connectNode(nodeId);

assert.equal(commissioningChangedCallsServer2.length, 1);
assert.equal(sessionChangedCallsServer2.length, 1);
assert.equal(sessionChangedCallsServer2[0].fabricIndex, FabricIndex(1));
const sessionInfo = commissioningServer2.getActiveSessionInformation();
assert.equal(sessionInfo.length, 1);
assert.ok(sessionInfo[0].fabric);
assert.equal(sessionInfo[0].numberOfActiveSubscriptions, 0);
});

it("Subscribe to all Attributes and bind updates to them for second device", async () => {
Expand All @@ -1202,6 +1281,14 @@ describe("Integration Test", () => {

assert.equal(Array.isArray(data.attributeReports), true);
assert.equal(Array.isArray(data.eventReports), true);

assert.equal(commissioningChangedCallsServer2.length, 1);
assert.equal(sessionChangedCallsServer2.length, 2);
assert.equal(sessionChangedCallsServer2[0].fabricIndex, FabricIndex(1));
const sessionInfo = commissioningServer2.getActiveSessionInformation();
assert.equal(sessionInfo.length, 1);
assert.ok(sessionInfo[0].fabric);
assert.equal(sessionInfo[0].numberOfActiveSubscriptions, 1);
});

it("controller storage is updated for second device", async () => {
Expand Down Expand Up @@ -1319,6 +1406,9 @@ describe("Integration Test", () => {
passcode,
}),
); // We can not check the real exception because text is dynamic

assert.equal(commissioningChangedCallsServer2.length, 1);
assert.equal(commissioningChangedCallsServer.length, 1);
});

it("connect this device to a new controller", async () => {
Expand Down Expand Up @@ -1352,6 +1442,15 @@ describe("Integration Test", () => {
passcode,
}),
);

assert.equal(commissioningChangedCallsServer.length, 2);
assert.ok(sessionChangedCallsServer.length >= 7);
assert.equal(sessionChangedCallsServer[7].fabricIndex, FabricIndex(2));
const sessionInfo = commissioningServer.getActiveSessionInformation();
assert.equal(sessionInfo.length, 2);
assert.ok(sessionInfo[1].fabric);
assert.equal(sessionInfo[1].numberOfActiveSubscriptions, 0);
assert.equal(commissioningChangedCallsServer2.length, 1);
}).timeout(10_000);

it("verify that the server storage got updated", async () => {
Expand Down Expand Up @@ -1454,13 +1553,17 @@ describe("Integration Test", () => {
const secondNodeId = commissioningController.getCommissionedNodes()[1];
const node = commissioningController.getConnectedNode(nodeId);
assert.ok(node);
await assert.doesNotReject(async () => node.decommission());
await assert.doesNotReject(async () => await node.decommission());

assert.equal(commissioningController.getCommissionedNodes().length, 1);
assert.equal(commissioningController.getCommissionedNodes()[0], secondNodeId);

assert.equal(commissioningChangedCallsServer.length, 3);
assert.equal(commissioningChangedCallsServer[2].fabricIndex, FabricIndex(1));
assert.equal(commissioningChangedCallsServer2.length, 1);
});

it("read and remove second node by removing fabric from device unplanned", async () => {
it("read and remove second node by removing fabric from device unplanned and doing factory reset", async () => {
// We remove the node ourselves (should not be done that way), but for testing we do
const nodeId = commissioningController.getCommissionedNodes()[0];
const node = commissioningController.getConnectedNode(nodeId);
Expand All @@ -1480,12 +1583,29 @@ describe("Integration Test", () => {
assert.equal(result.statusCode, OperationalCredentials.NodeOperationalCertStatus.Ok);
assert.deepEqual(result.fabricIndex, fabricIndex);

let i;
for (i = 0; i < 20; i++) {
if (commissioningChangedCallsServer2.length === 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
} else {
break;
}
}
assert.ok(i !== 0);
assert.equal(commissioningChangedCallsServer2.length, 2);

assert.equal(commissioningController.getCommissionedNodes().length, 1);

// Try to remove node now will throw an error
await assert.rejects(async () => await node.decommission());

await assert.doesNotReject(async () => await commissioningController.removeNode(nodeId, false));

Time.get = () => mockTimeInstance;

assert.equal(commissioningController.getCommissionedNodes().length, 0);
assert.equal(commissioningChangedCallsServer2.length, 2);
assert.equal(commissioningChangedCallsServer2[1].fabricIndex, FabricIndex(1));
}).timeout(30_000);

it("controller storage is updated for removed nodes", async () => {
Expand Down
28 changes: 14 additions & 14 deletions packages/matter-node.js/test/cluster/ClusterServerTestingUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,20 @@ export async function createTestSessionWithFabric() {
ZERO,
"",
);
return await SecureSession.create(
{} as any,
1,
testFabric,
NodeId(BigInt(1)),
1,
ZERO,
ZERO,
false,
false,
async () => {
return await SecureSession.create({
context: {} as any,
id: 1,
fabric: testFabric,
peerNodeId: NodeId(BigInt(1)),
peerSessionId: 1,
sharedSecret: ZERO,
salt: ZERO,
isInitiator: false,
isResumption: false,
closeCallback: async () => {
/* */
},
1,
2,
);
idleRetransmissionTimeoutMs: 1,
activeRetransmissionTimeoutMs: 2,
});
}
Loading

0 comments on commit a54aec2

Please sign in to comment.