diff --git a/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts b/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts index eefb6fa8a748..4caf7de2319f 100644 --- a/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts +++ b/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts @@ -644,6 +644,31 @@ const forwardCommandClassesToHost: MockControllerBehavior = { }, }; +const forwardUnsolicitedNIF: MockControllerBehavior = { + async onNodeFrame(host, controller, node, frame) { + if ( + frame.type === MockZWaveFrameType.Request + && frame.payload instanceof ZWaveProtocolCCNodeInformationFrame + ) { + const updateRequest = new ApplicationUpdateRequestNodeInfoReceived( + host, + { + nodeInformation: { + ...frame.payload, + nodeId: frame.payload.nodeId as number, + }, + }, + ); + // Simulate a serialized frame being transmitted via radio + const data = updateRequest.serialize(); + await wait(node.capabilities.txDelay); + // Then receive it + await controller.sendToHost(data); + return true; + } + }, +}; + /** Predefined default behaviors that are required for interacting with the driver correctly */ export function createDefaultBehaviors(): MockControllerBehavior[] { return [ @@ -660,5 +685,6 @@ export function createDefaultBehaviors(): MockControllerBehavior[] { handleRequestNodeInfo, handleAssignSUCReturnRoute, forwardCommandClassesToHost, + forwardUnsolicitedNIF, ]; } diff --git a/packages/zwave-js/src/lib/node/Node.ts b/packages/zwave-js/src/lib/node/Node.ts index 812ef28983bd..6bbbb7163c4c 100644 --- a/packages/zwave-js/src/lib/node/Node.ts +++ b/packages/zwave-js/src/lib/node/Node.ts @@ -1652,6 +1652,10 @@ export class ZWaveNode extends Endpoint && this.canSleep && this.supportsCC(CommandClasses["Wake Up"]) ) { + this.driver.controllerLog.logNode( + this.nodeId, + "Re-interview scheduled, waiting for node to wake up...", + ); didWakeUp = await this.waitForWakeup() .then(() => true) .catch(() => false); diff --git a/packages/zwave-js/src/lib/test/compat/fixtures/reInterviewWakeUpNIF/7e570001.jsonl b/packages/zwave-js/src/lib/test/compat/fixtures/reInterviewWakeUpNIF/7e570001.jsonl new file mode 100644 index 000000000000..c79b2895cfed --- /dev/null +++ b/packages/zwave-js/src/lib/test/compat/fixtures/reInterviewWakeUpNIF/7e570001.jsonl @@ -0,0 +1,37 @@ +{"k":"cacheFormat","v":1} +{"k":"node.1.isListening","v":true} +{"k":"node.1.isFrequentListening","v":false} +{"k":"node.1.isRouting","v":true} +{"k":"node.1.supportedDataRates","v":[40000,9600,100000]} +{"k":"node.1.protocolVersion","v":3} +{"k":"node.1.nodeType","v":"Controller"} +{"k":"node.1.supportsSecurity","v":false} +{"k":"node.1.supportsBeaming","v":true} +{"k":"node.1.deviceClass","v":{"basic":2,"generic":2,"specific":7}} +{"k":"node.1.endpoint.0.commandClass.0x72","v":{"isSupported":true,"isControlled":false,"secure":false,"version":0}} +{"k":"node.1.endpoint.0.commandClass.0x86","v":{"isSupported":true,"isControlled":false,"secure":false,"version":0}} +{"k":"node.1.endpoint.0.commandClass.0x20","v":{"isSupported":false,"isControlled":true,"secure":false,"version":0}} +{"k":"node.1.endpoint.0.commandClass.0x60","v":{"isSupported":false,"isControlled":true,"secure":false,"version":0}} +{"k":"node.1.interviewStage","v":"Complete"} + + +{"k":"node.2.isListening","v":false} +{"k":"node.2.isFrequentListening","v":false} +{"k":"node.2.isRouting","v":true} +{"k":"node.2.supportedDataRates","v":[40000,9600,100000]} +{"k":"node.2.protocolVersion","v":3} +{"k":"node.2.nodeType","v":"End Node"} +{"k":"node.2.supportsSecurity","v":false} +{"k":"node.2.supportsBeaming","v":true} +{"k":"node.2.deviceClass","v":{"basic":4,"generic":6,"specific":1}} +// Version CC +{"k":"node.2.endpoint.0.commandClass.0x86","v":{"isSupported":true,"isControlled":false,"secure":false,"version":1}} +// Wake Up CC +{"k":"node.2.endpoint.0.commandClass.0x84","v":{"isSupported":true,"isControlled":false,"secure":false,"version":3}} + +{"k":"node.2.securityClasses.S2_AccessControl","v":false} +{"k":"node.2.securityClasses.S2_Authenticated","v":false} +{"k":"node.2.securityClasses.S2_Unauthenticated","v":false} +{"k":"node.2.securityClasses.S0_Legacy","v":false} +{"k":"node.2.interviewStage","v":"Complete"} +{"k":"node.2.hasSUCReturnRoute","v":true} diff --git a/packages/zwave-js/src/lib/test/compat/reInterviewWakeUpNIF.test.ts b/packages/zwave-js/src/lib/test/compat/reInterviewWakeUpNIF.test.ts new file mode 100644 index 000000000000..4fa3c8a31040 --- /dev/null +++ b/packages/zwave-js/src/lib/test/compat/reInterviewWakeUpNIF.test.ts @@ -0,0 +1,54 @@ +import { ZWaveProtocolCCNodeInformationFrame } from "@zwave-js/cc"; +import { CommandClasses } from "@zwave-js/core"; +import { createMockZWaveRequestFrame } from "@zwave-js/testing"; +import { wait } from "alcalzone-shared/async"; +import path from "node:path"; +import { integrationTest } from "../integrationTestSuite"; + +integrationTest( + "Re-interviews that wait for a wake-up start when receiving a NIF", + { + // debug: true, + + provisioningDirectory: path.join( + __dirname, + "fixtures/reInterviewWakeUpNIF", + ), + + nodeCapabilities: { + isFrequentListening: false, + isListening: false, + + commandClasses: [ + CommandClasses["Wake Up"], + CommandClasses.Version, + ], + }, + + async testBody(t, driver, node, mockController, mockNode) { + const promise = node.refreshInfo(); + + await wait(500); + + // Send a NIF to trigger the re-interview + const cc = new ZWaveProtocolCCNodeInformationFrame(mockNode.host, { + nodeId: mockNode.id, + ...mockNode.capabilities, + supportedCCs: [...mockNode.implementedCCs] + .filter(([, info]) => info.isSupported) + .map(([ccId]) => ccId), + }); + await mockNode.sendToController( + createMockZWaveRequestFrame(cc, { + ackRequested: false, + }), + ); + + // The interview should now happen + await promise; + + await wait(500); + t.pass(); + }, + }, +);