Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cc): add value event and notifications for Multilevel Switch CC Set and Start/StopLevelChange #4282

Merged
merged 10 commits into from
Mar 14, 2022
6 changes: 5 additions & 1 deletion docs/config-files/file-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,11 +397,15 @@ Some devices spam the network with hundreds of invalid `ConfigurationCC::InfoRep

### `treatBasicSetAsEvent`

By default, `Basic CC::Set` commands are interpreted as status updates. This flag causes the driver to emit a `value event` for the `"value"` property instead. Note that this property is exclusively used in this case in order to avoid conflicts with regular value IDs.
By default, `Basic CC::Set` commands are interpreted as status updates. This flag causes the driver to emit a `value event` for the `"event"` property instead. Note that this property is exclusively used in this case in order to avoid conflicts with regular value IDs.

> [!NOTE]
> If this option is `true`, it has precedence over `disableBasicMapping`.

### `treatMultilevelSwitchSetAsEvent`

By default, `Multilevel Switch CC::Set` commands are ignored, because they are meant to control end devices. This flag causes the driver to emit a `value event` for the `"event"` property instead, so applications can react to these commands, e.g. for remotes.

### `treatDestinationEndpointAsSource`

Some devices incorrectly use the multi channel **destination** endpoint in reports to indicate the **source** endpoint the report originated from. When this flag is `true`, the destination endpoint is instead interpreted to be the source and the original source endpoint gets ignored.
3 changes: 3 additions & 0 deletions maintenance/schemas/device-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@
"treatBasicSetAsEvent": {
"const": true
},
"treatMultilevelSwitchSetAsEvent": {
"const": true
},
"treatDestinationEndpointAsSource": {
"const": true
}
Expand Down
14 changes: 14 additions & 0 deletions packages/config/src/CompatConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@ error in compat option treatBasicSetAsEvent`,
this.treatBasicSetAsEvent = definition.treatBasicSetAsEvent;
}

if (definition.treatMultilevelSwitchSetAsEvent != undefined) {
if (definition.treatMultilevelSwitchSetAsEvent !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option treatMultilevelSwitchSetAsEvent`,
);
}

this.treatMultilevelSwitchSetAsEvent =
definition.treatMultilevelSwitchSetAsEvent;
}

if (definition.treatDestinationEndpointAsSource != undefined) {
if (definition.treatDestinationEndpointAsSource !== true) {
throwInvalidConfig(
Expand Down Expand Up @@ -459,6 +472,7 @@ compat option alarmMapping must be an array where all items are objects!`,
public readonly preserveEndpoints?: "*" | readonly number[];
public readonly skipConfigurationInfoQuery?: boolean;
public readonly treatBasicSetAsEvent?: boolean;
public readonly treatMultilevelSwitchSetAsEvent?: boolean;
public readonly treatDestinationEndpointAsSource?: boolean;
public readonly queryOnWakeup?: readonly [
string,
Expand Down
70 changes: 59 additions & 11 deletions packages/zwave-js/src/lib/commandclass/MultilevelSwitchCC.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type { MessageRecord, ValueID } from "@zwave-js/core";
import {
CommandClasses,
Duration,
Maybe,
MessageOrCCLogEntry,
MessageRecord,
parseMaybeNumber,
parseNumber,
validatePayload,
ValueID,
ValueMetadata,
ZWaveError,
ZWaveErrorCodes,
} from "@zwave-js/core";
import { getEnumMemberName, pick } from "@zwave-js/shared";
import type { Driver } from "../driver/Driver";
import { MessagePriority } from "../message/Constants";
import type { ZWaveNode } from "../node/Node";
import { VirtualEndpoint } from "../node/VirtualEndpoint";
import {
CCAPI,
Expand Down Expand Up @@ -109,6 +111,35 @@ function getSuperviseStartStopLevelChangeValueId(): ValueID {
};
}

export function getCompatEventValueId(endpoint?: number): ValueID {
return {
commandClass: CommandClasses["Multilevel Switch"],
endpoint,
property: "event",
};
}

/**
* @publicAPI
* This is emitted when a start or stop event is received
*/
export interface ZWaveNotificationCallbackArgs_MultilevelSwitchCC {
eventType:
| MultilevelSwitchCommand.StartLevelChange
| MultilevelSwitchCommand.StopLevelChange;
direction?: string;
}

/**
* @publicAPI
* Parameter types for the MultilevelSwitch CC specific version of ZWaveNotificationCallback
*/
export type ZWaveNotificationCallbackParams_MultilevelSwitchCC = [
node: ZWaveNode,
ccId: typeof CommandClasses["Multilevel Switch"],
args: ZWaveNotificationCallbackArgs_MultilevelSwitchCC,
];

@API(CommandClasses["Multilevel Switch"])
export class MultilevelSwitchCCAPI extends CCAPI {
public supportsCommand(cmd: MultilevelSwitchCommand): Maybe<boolean> {
Expand Down Expand Up @@ -497,6 +528,17 @@ export class MultilevelSwitchCC extends CommandClass {

await this.refreshValues();

// create compat event value if necessary
if (node.deviceConfig?.compat?.treatMultilevelSwitchSetAsEvent) {
const valueId = getCompatEventValueId(this.endpointIndex);
if (!node.valueDB.hasMetadata(valueId)) {
node.valueDB.setMetadata(valueId, {
...ValueMetadata.ReadOnlyUInt8,
label: "Event value",
});
}
}

// Remember that the interview is complete
this.interviewComplete = true;
}
Expand Down Expand Up @@ -581,11 +623,12 @@ export class MultilevelSwitchCCSet extends MultilevelSwitchCC {
) {
super(driver, options);
if (gotDeserializationOptions(options)) {
// TODO: Deserialize payload
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
validatePayload(this.payload.length >= 1);
this.targetValue = this.payload[0];

if (this.payload.length >= 2) {
this.duration = Duration.parseReport(this.payload[1]);
}
} else {
this.targetValue = options.targetValue;
this.duration = options.duration;
Expand Down Expand Up @@ -705,11 +748,16 @@ export class MultilevelSwitchCCStartLevelChange extends MultilevelSwitchCC {
) {
super(driver, options);
if (gotDeserializationOptions(options)) {
// TODO: Deserialize payload
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
validatePayload(this.payload.length >= 3);
const direction = (this.payload[0] & 0b0_1_0_00000) >>> 6;
const ignoreStartLevel = (this.payload[0] & 0b0_0_1_00000) >>> 5;
const startLevel = this.payload[1];
const duration = this.payload[2];

this.duration = Duration.parseReport(duration);
this.ignoreStartLevel = !!ignoreStartLevel;
this.startLevel = startLevel;
this.direction = direction ? "down" : "up";
} else {
this.duration = options.duration;
this.ignoreStartLevel = options.ignoreStartLevel;
Expand Down
6 changes: 5 additions & 1 deletion packages/zwave-js/src/lib/commandclass/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,11 @@ export {
MultilevelSwitchCCSupportedReport,
SwitchType,
} from "./MultilevelSwitchCC";
export type { MultilevelSwitchLevelChangeMetadata } from "./MultilevelSwitchCC";
export type {
MultilevelSwitchLevelChangeMetadata,
ZWaveNotificationCallbackArgs_MultilevelSwitchCC,
ZWaveNotificationCallbackParams_MultilevelSwitchCC,
} from "./MultilevelSwitchCC";
export {
NodeNamingAndLocationCC,
NodeNamingAndLocationCCLocationGet,
Expand Down
54 changes: 54 additions & 0 deletions packages/zwave-js/src/lib/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ import {
getEndpointDeviceClassValueId,
getEndpointIndizesValueId,
} from "../commandclass/MultiChannelCC";
import {
getCompatEventValueId as getMultilevelSwitchCCCompatEventValueId,
MultilevelSwitchCC,
MultilevelSwitchCCSet,
MultilevelSwitchCCStartLevelChange,
MultilevelSwitchCCStopLevelChange,
MultilevelSwitchCommand,
} from "../commandclass/MultilevelSwitchCC";
import {
getNodeLocationValueId,
getNodeNameValueId,
Expand Down Expand Up @@ -2275,6 +2283,8 @@ protocol version: ${this.protocolVersion}`;

if (command instanceof BasicCC) {
return this.handleBasicCommand(command);
} else if (command instanceof MultilevelSwitchCC) {
return this.handleMultilevelSwitchCommand(command);
} else if (command instanceof CentralSceneCCNotification) {
return this.handleCentralSceneNotification(command);
} else if (command instanceof WakeUpCCWakeUpNotification) {
Expand Down Expand Up @@ -2847,6 +2857,50 @@ protocol version: ${this.protocolVersion}`;
}
}

/** Handles the receipt of a MultilevelCC Set or Report */
private handleMultilevelSwitchCommand(command: MultilevelSwitchCC): void {
if (command instanceof MultilevelSwitchCCSet) {
this.driver.controllerLog.logNode(this.id, {
endpoint: command.endpointIndex,
message: "treating MultiLevelSwitchCCSet::Set as a value event",
});
this._valueDB.setValue(
getMultilevelSwitchCCCompatEventValueId(command.endpointIndex),
command.targetValue,
{
stateful: false,
},
);
} else if (command instanceof MultilevelSwitchCCStartLevelChange) {
this.driver.controllerLog.logNode(this.id, {
endpoint: command.endpointIndex,
message:
"treating MultilevelSwitchCC::StartLevelChange as a notification",
});
this.emit(
"notification",
this,
CommandClasses["Multilevel Switch"],
{
eventType: MultilevelSwitchCommand.StartLevelChange,
direction: command.direction,
},
);
} else if (command instanceof MultilevelSwitchCCStopLevelChange) {
this.driver.controllerLog.logNode(this.id, {
endpoint: command.endpointIndex,
message:
"treating MultilevelSwitchCC::StopLevelChange as a notification",
});
this.emit(
"notification",
this,
CommandClasses["Multilevel Switch"],
{ eventType: MultilevelSwitchCommand.StopLevelChange },
);
}
}

/**
* Allows automatically resetting notification values to idle if the node does not do it itself
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/zwave-js/src/lib/node/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "@zwave-js/core";
import type { FirmwareUpdateStatus } from "../commandclass";
import type { ZWaveNotificationCallbackParams_EntryControlCC } from "../commandclass/EntryControlCC";
import type { ZWaveNotificationCallbackParams_MultilevelSwitchCC } from "../commandclass/MultilevelSwitchCC";
import type { ZWaveNotificationCallbackParams_NotificationCC } from "../commandclass/NotificationCC";
import type {
Powerlevel,
Expand Down Expand Up @@ -88,6 +89,7 @@ export type ZWaveNotificationCallback = (
| ZWaveNotificationCallbackParams_NotificationCC
| ZWaveNotificationCallbackParams_EntryControlCC
| ZWaveNotificationCallbackParams_PowerlevelCC
| ZWaveNotificationCallbackParams_MultilevelSwitchCC
) => void;

export interface ZWaveNodeValueEventCallbacks {
Expand Down