diff --git a/packages/cc/src/cc/NotificationCC.ts b/packages/cc/src/cc/NotificationCC.ts index dff96136aff..225f378122b 100644 --- a/packages/cc/src/cc/NotificationCC.ts +++ b/packages/cc/src/cc/NotificationCC.ts @@ -119,6 +119,29 @@ export const NotificationCCValues = Object.freeze({ autoCreate: shouldAutoCreateSimpleDoorSensorValue, } as const, ), + + // Binary tilt value extracted from the Door state variable. + ...V.staticPropertyAndKeyWithName( + "doorTiltState", + "Access Control", + "Door tilt state", + { + // Must be a number for compatibility reasons + ...ValueMetadata.ReadOnlyUInt8, + label: "Door tilt state", + states: { + [0x00]: "Window/door is not tilted", + [0x01]: "Window/door is tilted", + }, + ccSpecific: { + notificationType: 0x06, + }, + } as const, + { + // This is created when the tilt state is first received. + autoCreate: false, + } as const, + ), }), ...V.defineDynamicCCValues(CommandClasses.Notification, { diff --git a/packages/zwave-js/src/lib/node/Node.ts b/packages/zwave-js/src/lib/node/Node.ts index 95205ade347..f181be1239e 100644 --- a/packages/zwave-js/src/lib/node/Node.ts +++ b/packages/zwave-js/src/lib/node/Node.ts @@ -5252,15 +5252,33 @@ protocol version: ${this.protocolVersion}`; // actually support them, which makes working with the Door state variable // very cumbersome. Also, this is currently the only notification where the enum values // extend the state value. + // To work around this, we hard-code a notification value for the door status // which only includes the "legacy" states for open/closed. - this.valueDB.setValue( NotificationCCValues.doorStateSimple.endpoint( command.endpointIndex, ), command.notificationEvent === 0x17 ? 0x17 : 0x16, ); + + // In addition to that, we also hard-code a notification value for only the tilt status. + // This will only be created after receiving a notification for the tilted state. + // Only after it exists, it will be updated. Otherwise, we'd get phantom + // values, since some devices send the enum value, even when they don't support tilt. + const tiltValue = NotificationCCValues.doorTiltState; + const tiltValueId = tiltValue.endpoint(command.endpointIndex); + let tiltValueWasCreated = this.valueDB.hasMetadata(tiltValueId); + if (command.eventParameters === 0x01 && !tiltValueWasCreated) { + this.valueDB.setMetadata(tiltValueId, tiltValue.meta); + tiltValueWasCreated = true; + } + if (tiltValueWasCreated) { + this.valueDB.setValue( + tiltValueId, + command.eventParameters === 0x01 ? 0x01 : 0x00, + ); + } } } diff --git a/packages/zwave-js/src/lib/test/cc-specific/notificationEnums.test.ts b/packages/zwave-js/src/lib/test/cc-specific/notificationEnums.test.ts index b90276c72ef..6a5f2407b92 100644 --- a/packages/zwave-js/src/lib/test/cc-specific/notificationEnums.test.ts +++ b/packages/zwave-js/src/lib/test/cc-specific/notificationEnums.test.ts @@ -314,6 +314,171 @@ integrationTest("The 'simple' Door state value works correctly", { }, }); +integrationTest("The synthetic 'Door tilt state' value works correctly", { + // debug: true, + + nodeCapabilities: { + commandClasses: [ + CommandClasses.Version, + { + ccId: CommandClasses.Notification, + isSupported: true, + version: 8, + supportsV1Alarm: false, + notificationTypesAndEvents: { + // Access Control - Window open and Window closed + [0x06]: [0x16, 0x17], + }, + }, + ], + }, + + testBody: async (t, driver, node, mockController, mockNode) => { + await node.commandClasses.Notification.getSupportedEvents(0x06); + + const tiltVID = NotificationCCValues.doorTiltState.id; + + const hasTiltVID = () => + node.getDefinedValueIDs().some( + (vid) => NotificationCCValues.doorTiltState.is(vid), + ); + // Before receiving any notifications with the tilt enum, the synthetic value should not exist + t.false(hasTiltVID()); + + // Send a notification to the node where the window is not tilted + let cc = new NotificationCCReport(mockNode.host, { + nodeId: mockController.host.ownNodeId, + notificationType: 0x06, + notificationEvent: 0x16, // Window/door is open + eventParameters: Buffer.from([0x00]), // ... in regular position + }); + await mockNode.sendToController( + createMockZWaveRequestFrame(cc, { + ackRequested: false, + }), + ); + // wait a bit for the value to be updated + await wait(100); + + // The value should still not exist + t.false(hasTiltVID()); + + // === + + // Again with tilt + cc = new NotificationCCReport(mockNode.host, { + nodeId: mockController.host.ownNodeId, + notificationType: 0x06, + notificationEvent: 0x16, // Window/door is open + eventParameters: Buffer.from([0x01]), // ... in tilt position + }); + await mockNode.sendToController( + createMockZWaveRequestFrame(cc, { + ackRequested: false, + }), + ); + // wait a bit for the value to be updated + await wait(100); + + // The value should now exist + t.true(hasTiltVID()); + t.is(node.getValue(tiltVID), 0x01); + + // === + + // Again without tilt + cc = new NotificationCCReport(mockNode.host, { + nodeId: mockController.host.ownNodeId, + notificationType: 0x06, + notificationEvent: 0x16, // Window/door is open + eventParameters: Buffer.from([0x00]), // ... in regular position + }); + await mockNode.sendToController( + createMockZWaveRequestFrame(cc, { + ackRequested: false, + }), + ); + // wait a bit for the value to be updated + await wait(100); + + t.is(node.getValue(tiltVID), 0x00); + + // === + + // Again with tilt to be able to detect changes + cc = new NotificationCCReport(mockNode.host, { + nodeId: mockController.host.ownNodeId, + notificationType: 0x06, + notificationEvent: 0x16, // Window/door is open + eventParameters: Buffer.from([0x01]), // ... in tilt position + }); + await mockNode.sendToController( + createMockZWaveRequestFrame(cc, { + ackRequested: false, + }), + ); + // wait a bit for the value to be updated + await wait(100); + + t.is(node.getValue(tiltVID), 0x01); + + // === + + // And now without the enum + cc = new NotificationCCReport(mockNode.host, { + nodeId: mockController.host.ownNodeId, + notificationType: 0x06, + notificationEvent: 0x17, // Window/door is closed + }); + await mockNode.sendToController( + createMockZWaveRequestFrame(cc, { + ackRequested: false, + }), + ); + // wait a bit for the value to be updated + await wait(100); + + t.is(node.getValue(tiltVID), 0x00); + + // === + + // Again with tilt to be able to detect changes + cc = new NotificationCCReport(mockNode.host, { + nodeId: mockController.host.ownNodeId, + notificationType: 0x06, + notificationEvent: 0x16, // Window/door is open + eventParameters: Buffer.from([0x01]), // ... in tilt position + }); + await mockNode.sendToController( + createMockZWaveRequestFrame(cc, { + ackRequested: false, + }), + ); + // wait a bit for the value to be updated + await wait(100); + + t.is(node.getValue(tiltVID), 0x01); + + // === + + // And again without the enum + cc = new NotificationCCReport(mockNode.host, { + nodeId: mockController.host.ownNodeId, + notificationType: 0x06, + notificationEvent: 0x16, // Window/door is open + }); + await mockNode.sendToController( + createMockZWaveRequestFrame(cc, { + ackRequested: false, + }), + ); + // wait a bit for the value to be updated + await wait(100); + + t.is(node.getValue(tiltVID), 0x00); + }, +}); + integrationTest( "Notification types with 'replace'-type enums fall back to the default value if the event parameter is not contained in the CC", {