zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
405 lines • 20.5 kB
JavaScript
import { DoorLockCCValues, DoorLockMode, LockCCValues, NotificationCCValues, UserCodeCC, UserIDStatus, getEffectiveCCVersion, } from "@zwave-js/cc";
import { getNotificationEnumBehavior, getNotificationStateValueWithEnum, getNotificationValueMetadata, } from "@zwave-js/cc/NotificationCC";
import { CommandClasses, UNKNOWN_STATE, getNotification, getNotificationValue, valueIdToString, } from "@zwave-js/core";
import { isUint8Array, setTimer, stringify, } from "@zwave-js/shared";
export function getDefaultNotificationHandlerStore() {
return {
idleTimeouts: new Map(),
};
}
/** Handles the receipt of a Notification Report */
export function handleNotificationReport(ctx, node, command, store) {
if (command.notificationType == undefined) {
if (command.alarmType == undefined) {
ctx.logNode(node.id, {
message: `received unsupported notification ${stringify(command)}`,
direction: "inbound",
});
}
return;
}
// Apply notification remapping from compat config
const remappings = node.deviceConfig?.compat?.remapNotifications;
if (remappings?.length) {
const match = remappings.find((m) => m.from.notificationType === command.notificationType
&& m.from.notificationEvent === command.notificationEvent);
if (match) {
ctx.logNode(node.id, {
message: `Notification remapped via compat flag`,
direction: "inbound",
level: "verbose",
});
if (match.action.type === "clear" || match.action.type === "idle") {
for (const target of match.action.targets) {
const targetNotification = getNotification(target.notificationType);
if (!targetNotification)
continue;
const targetValueConfig = getNotificationValue(targetNotification, target.notificationEvent);
if (targetValueConfig?.type !== "state")
continue;
const valueId = NotificationCCValues
.notificationVariable(targetNotification.name, targetValueConfig.variableName).endpoint(command.endpointIndex);
if (match.action.type === "clear") {
// Only clear if the variable currently reflects this specific event,
// to avoid spurious updates when multiple targets share the same variable.
if (node.valueDB.getValue(valueId)
=== target.notificationEvent) {
node.valueDB.setValue(valueId, UNKNOWN_STATE);
}
// Special case for doorStateSimple. If the parent value
// gets cleared, this also needs to get cleared.
if (target.notificationType === 0x06
&& (target.notificationEvent === 0x16
|| target.notificationEvent === 0x17)) {
const simpleDoorStateId = NotificationCCValues
.deprecated_doorStateSimple
.endpoint(command.endpointIndex);
if (node.valueDB.getValue(simpleDoorStateId)
=== target.notificationEvent) {
node.valueDB.setValue(simpleDoorStateId, UNKNOWN_STATE);
}
}
}
else {
node.valueDB.setValue(valueId, 0 /* idle */);
}
}
return;
}
else {
// Normal remap: overwrite notification type and event, then fall through
// to normal handling.
const to = match.action.to;
command.notificationType = to.notificationType;
command.notificationEvent = to.notificationEvent;
}
}
}
const ccVersion = getEffectiveCCVersion(ctx, command);
// Look up the received notification in the config
const notification = getNotification(command.notificationType);
if (notification) {
// This is a notification (status or event) with a known type
const notificationName = notification.name;
ctx.logNode(node.id, {
message: `[handleNotificationReport] notificationName: ${notificationName}`,
level: "silly",
});
/** Returns a single notification state to idle */
const setStateIdle = (prevValue) => {
manuallyIdleNotificationValueInternal(ctx, node, store, notification, prevValue, command.endpointIndex);
};
const setUnknownStateIdle = (prevValue) => {
// Find the value for the unknown notification variable bucket
const unknownNotificationVariableValueId = NotificationCCValues
.unknownNotificationVariable(command.notificationType, notificationName).endpoint(command.endpointIndex);
const currentValue = node.valueDB.getValue(unknownNotificationVariableValueId);
// ... and if it exists
if (currentValue == undefined)
return;
// ... reset it to idle
if (prevValue == undefined || currentValue === prevValue) {
node.valueDB.setValue(unknownNotificationVariableValueId, 0);
}
};
const value = command.notificationEvent;
if (value === 0) {
// Generic idle notification, this contains a value to be reset
if (isUint8Array(command.eventParameters)
&& command.eventParameters.length) {
// The target value is the first byte of the event parameters
setStateIdle(command.eventParameters[0]);
setUnknownStateIdle(command.eventParameters[0]);
}
else {
// Reset all values to idle
const nonIdleValues = node.valueDB
.getValues(CommandClasses.Notification)
.filter((v) => (v.endpoint || 0) === command.endpointIndex
&& v.property === notificationName
&& typeof v.value === "number"
&& v.value !== 0);
for (const v of nonIdleValues) {
setStateIdle(v.value);
}
setUnknownStateIdle();
}
return;
}
// Find out which property we need to update
const valueConfig = getNotificationValue(notification, value);
if (valueConfig) {
ctx.logNode(node.id, {
message: `[handleNotificationReport] valueConfig:
label: ${valueConfig.label}
${valueConfig.type === "event"
? "type: event"
: `type: state
variableName: ${valueConfig.variableName}`}`,
level: "silly",
});
}
else {
ctx.logNode(node.id, {
message: `[handleNotificationReport] valueConfig: undefined`,
level: "silly",
});
}
// Perform some heuristics on the known notification
handleKnownNotification(ctx, node, command);
let allowIdleReset;
if (!valueConfig) {
// We don't know what this notification refers to, so we don't force a reset
allowIdleReset = false;
}
else if (valueConfig.type === "state") {
allowIdleReset = valueConfig.idle;
}
else {
// This is an event
const endpoint = node.getEndpoint(command.endpointIndex)
?? node;
// Build the notification event args
const eventArgs = {
type: command.notificationType,
event: value,
label: notification.name,
eventLabel: valueConfig.label,
parameters: command.eventParameters,
};
// If the lookupUserIdInNotificationEvents preference is enabled and the event contains a userId,
// look up the user code and status and add them to the parameters
const prefs = ctx.getUserPreferences();
if (prefs.lookupUserIdInNotificationEvents
&& command.eventParameters != null
&& typeof command.eventParameters === "object"
&& !isUint8Array(command.eventParameters)
&& "userId" in command.eventParameters
&& typeof command.eventParameters.userId === "number") {
const userId = command.eventParameters.userId;
const nodeEndpoint = {
nodeId: node.id,
index: command.endpointIndex,
virtual: false,
};
// Create a new parameters object with the additional fields
const enhancedParameters = {
...command.eventParameters,
};
const userIdStatus = UserCodeCC.getUserIdStatusCached(ctx, nodeEndpoint, userId);
if (userIdStatus != null) {
enhancedParameters.userIdStatus = userIdStatus;
}
const userCode = UserCodeCC.getUserCodeCached(ctx, nodeEndpoint, userId);
if (userCode != null) {
enhancedParameters.userCode = userCode;
}
eventArgs.parameters = enhancedParameters;
}
node.emit("notification", endpoint, CommandClasses.Notification, eventArgs);
// We may need to reset some linked states to idle
if (valueConfig.idleVariables?.length) {
for (const variable of valueConfig.idleVariables) {
setStateIdle(variable);
}
}
return;
}
// Now that we've gathered all we need to know, update the value in our DB
let valueId;
if (valueConfig) {
valueId = NotificationCCValues.notificationVariable(notificationName, valueConfig.variableName).endpoint(command.endpointIndex);
extendNotificationValueMetadata(ctx, node, valueId, notification, valueConfig);
}
else {
// Collect unknown values in an "unknown" bucket
const unknownValue = NotificationCCValues
.unknownNotificationVariable(command.notificationType, notificationName);
valueId = unknownValue.endpoint(command.endpointIndex);
if (ccVersion >= 2) {
if (!node.valueDB.hasMetadata(valueId)) {
node.valueDB.setMetadata(valueId, unknownValue.meta);
}
}
}
if (typeof command.eventParameters === "number") {
// This notification contains an enum value. Depending on how the enum behaves,
// we may need to set "fake" values for these to distinguish them
// from states without enum values
const enumBehavior = valueConfig
? getNotificationEnumBehavior(notification, valueConfig)
: "extend";
const valueWithEnum = enumBehavior === "replace"
? command.eventParameters
: getNotificationStateValueWithEnum(value, command.eventParameters);
node.valueDB.setValue(valueId, valueWithEnum);
}
else {
node.valueDB.setValue(valueId, value);
}
// Nodes before V8 (and some misbehaving V8 ones) don't necessarily reset the notification to idle.
// The specifications advise to auto-reset the variables, but it has been found that this interferes
// with some motion sensors that don't refresh their active notification. Therefore, we set a fallback
// timer if the `forceNotificationIdleReset` compat flag is set.
if (allowIdleReset
&& !!node.deviceConfig?.compat?.forceNotificationIdleReset) {
ctx.logNode(node.id, {
message: `[handleNotificationReport] scheduling idle reset`,
level: "silly",
});
scheduleNotificationIdleReset(store, valueId, () => setStateIdle(value));
}
}
else {
// This is an unknown notification
const unknownValue = NotificationCCValues.unknownNotificationType(command.notificationType);
const valueId = unknownValue.endpoint(command.endpointIndex);
// Make sure the metdata exists
if (ccVersion >= 2) {
if (!node.valueDB.hasMetadata(valueId)) {
node.valueDB.setMetadata(valueId, unknownValue.meta);
}
}
// And set its value
node.valueDB.setValue(valueId, command.notificationEvent);
// We don't know what this notification refers to, so we don't force a reset
}
}
function handleKnownNotification(ctx, node, command) {
const lockEvents = new Set([0x01, 0x03, 0x05, 0x09]);
const unlockEvents = new Set([0x02, 0x04, 0x06]);
const doorStatusEvents = [
// Actual status
0x16,
0x17,
// Synthetic status with enum (deprecated)
0x1600,
0x1601,
];
if (
// Access Control, manual/keypad/rf/auto (un)lock operation
command.notificationType === 0x06
&& (lockEvents.has(command.notificationEvent)
|| unlockEvents.has(command.notificationEvent))
&& (node.supportsCC(CommandClasses["Door Lock"])
|| node.supportsCC(CommandClasses.Lock))) {
// The Door Lock Command Class is constrained to the S2 Access Control key,
// while the Notification Command Class, in the same device, could use a
// different key. This way the device can notify devices which don't belong
// to the S2 Access Control key group of changes in its state.
const isLocked = lockEvents.has(command.notificationEvent);
// Update the current lock status
if (node.supportsCC(CommandClasses["Door Lock"])) {
node.valueDB.setValue(DoorLockCCValues.currentMode.endpoint(command.endpointIndex), isLocked ? DoorLockMode.Secured : DoorLockMode.Unsecured);
}
if (node.supportsCC(CommandClasses.Lock)) {
node.valueDB.setValue(LockCCValues.locked.endpoint(command.endpointIndex), isLocked);
}
}
else if (command.notificationType === 0x06
&& doorStatusEvents.includes(command.notificationEvent)) {
// https://github.com/zwave-js/zwave-js/pull/5394 added support for
// notification enums. Unfortunately, there's no way to discover which nodes
// 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 synthesize a stable door/window state that only
// exposes tilt after we've actually seen a tilted notification.
const isTilted = command.notificationEvent === 0x16
&& command.eventParameters === 0x01;
const openingStateValue = NotificationCCValues.openingState;
const openingStateValueId = openingStateValue.endpoint(command.endpointIndex);
const openingStateTiltMetadata = {
...openingStateValue.meta,
states: {
...openingStateValue.meta.states,
[0x02]: "Tilted",
},
};
const openingStateMetadata = node.valueDB.getMetadata(openingStateValueId);
if (isTilted && !openingStateMetadata?.states?.[2]) {
node.valueDB.setMetadata(openingStateValueId, openingStateTiltMetadata);
}
node.valueDB.setValue(openingStateValueId, command.notificationEvent === 0x17 ? 0x00 : isTilted ? 0x02 : 0x01);
// Keep the legacy compatibility value limited to the historic open/closed states.
node.valueDB.setValue(NotificationCCValues.deprecated_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.deprecated_doorTiltState;
const tiltValueId = tiltValue.endpoint(command.endpointIndex);
let tiltValueWasCreated = node.valueDB.hasMetadata(tiltValueId);
if (isTilted && !tiltValueWasCreated) {
node.valueDB.setMetadata(tiltValueId, tiltValue.meta);
tiltValueWasCreated = true;
}
if (tiltValueWasCreated) {
node.valueDB.setValue(tiltValueId, isTilted ? 0x01 : 0x00);
}
}
else if (
// Access Control, all user codes deleted
command.notificationType === 0x06
&& command.notificationEvent === 0x0c
&& node.supportsCC(CommandClasses["User Code"])) {
// Clear all user codes from the cache
const endpoint = {
nodeId: node.id,
index: command.endpointIndex,
virtual: false,
};
const numUsers = UserCodeCC.getSupportedUsersCached(ctx, endpoint) ?? 0;
// Clear each user code by setting status to Available and code to empty
for (let userId = 1; userId <= numUsers; userId++) {
UserCodeCC.setUserIdStatusCached(ctx, endpoint, userId, UserIDStatus.Available);
UserCodeCC.setUserCodeCached(ctx, endpoint, userId, "");
}
}
}
/** Manually resets a single notification value to idle */
export function manuallyIdleNotificationValueInternal(ctx, node, store, notification, prevValue, endpointIndex) {
const valueConfig = getNotificationValue(notification, prevValue);
// Only known variables may be reset to idle
if (!valueConfig || valueConfig.type !== "state")
return;
// Some properties may not be reset to idle
if (!valueConfig.idle)
return;
const notificationName = notification.name;
const variableName = valueConfig.variableName;
const valueId = NotificationCCValues.notificationVariable(notificationName, variableName).endpoint(endpointIndex);
// Make sure the value is actually set to the previous value
if (node.valueDB.getValue(valueId) !== prevValue)
return;
// Since the node has reset the notification itself, we don't need the idle reset
clearNotificationIdleReset(store, valueId);
extendNotificationValueMetadata(ctx, node, valueId, notification, valueConfig);
node.valueDB.setValue(valueId, 0 /* idle */);
}
/** Schedules a notification value to be reset */
function scheduleNotificationIdleReset(store, valueId, handler) {
clearNotificationIdleReset(store, valueId);
const key = valueIdToString(valueId);
store.idleTimeouts.set(key,
// Unref'ing long running timeouts allows to quit the application before the timeout elapses
setTimer(handler, 5 * 60 * 1000 /* 5 minutes */).unref());
}
/** Removes a scheduled notification reset */
function clearNotificationIdleReset(store, valueId) {
const key = valueIdToString(valueId);
if (store.idleTimeouts.has(key)) {
store.idleTimeouts.get(key)?.clear();
store.idleTimeouts.delete(key);
}
}
// Fallback for V2 notifications that don't allow us to predefine the metadata during the interview.
// Instead of defining useless values for each possible notification event, we build the metadata on demand
function extendNotificationValueMetadata(ctx, node, valueId, notification, valueConfig) {
const ccVersion = ctx.getSupportedCCVersion(CommandClasses.Notification, node.id, node.index);
if (ccVersion === 2 || !node.valueDB.hasMetadata(valueId)) {
const metadata = getNotificationValueMetadata(node.valueDB.getMetadata(valueId), notification, valueConfig);
node.valueDB.setMetadata(valueId, metadata);
}
}
//# sourceMappingURL=NotificationCC.js.map