Skip to content

Commit

Permalink
feat(cc): add value event and notifications for Multilevel Switch CC …
Browse files Browse the repository at this point in the history
…Set and Start/StopLevelChange (#4282)

Co-authored-by: Z-Wave JS Bot <bot@zwave-js.io>
Co-authored-by: Dominic Griesel <d.griesel@gmx.net>
  • Loading branch information
3 people committed Mar 14, 2022
1 parent 0134fa7 commit e789714
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 13 deletions.
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 @@ -348,7 +348,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 @@ -106,6 +106,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 @@ -2274,6 +2282,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 @@ -2846,6 +2856,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

0 comments on commit e789714

Please sign in to comment.