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

fix: replace type Any in Binary Sensor Report with first supported sensor type #6933

Merged
merged 3 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 86 additions & 11 deletions packages/cc/src/cc/BinarySensorCC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
type MaybeNotKnown,
type MessageOrCCLogEntry,
MessagePriority,
type SupervisionResult,
ValueMetadata,
encodeBitMask,
parseBitMask,
validatePayload,
} from "@zwave-js/core/safe";
Expand Down Expand Up @@ -71,8 +73,10 @@ export class BinarySensorCCAPI extends PhysicalCCAPI {
public supportsCommand(cmd: BinarySensorCommand): MaybeNotKnown<boolean> {
switch (cmd) {
case BinarySensorCommand.Get:
case BinarySensorCommand.Report:
return true; // This is mandatory
case BinarySensorCommand.SupportedGet:
case BinarySensorCommand.SupportedReport:
return this.version >= 2;
}
return super.supportsCommand(cmd);
Expand Down Expand Up @@ -116,8 +120,28 @@ export class BinarySensorCCAPI extends PhysicalCCAPI {
return response?.value;
}

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public async getSupportedSensorTypes() {
@validateArgs()
public async sendReport(
value: boolean,
sensorType?: BinarySensorType,
): Promise<SupervisionResult | undefined> {
this.assertSupportsCommand(
BinarySensorCommand,
BinarySensorCommand.Report,
);

const cc = new BinarySensorCCReport(this.applHost, {
nodeId: this.endpoint.nodeId,
endpoint: this.endpoint.index,
value,
type: sensorType,
});
return this.applHost.sendCommand(cc, this.commandOptions);
}

public async getSupportedSensorTypes(): Promise<
readonly BinarySensorType[] | undefined
> {
this.assertSupportsCommand(
BinarySensorCommand,
BinarySensorCommand.SupportedGet,
Expand All @@ -136,6 +160,23 @@ export class BinarySensorCCAPI extends PhysicalCCAPI {
// We don't want to repeat the sensor type
return response?.supportedSensorTypes;
}

@validateArgs()
public async reportSupportedSensorTypes(
supported: BinarySensorType[],
): Promise<SupervisionResult | undefined> {
this.assertSupportsCommand(
BinarySensorCommand,
BinarySensorCommand.SupportedReport,
);

const cc = new BinarySensorCCSupportedReport(this.applHost, {
nodeId: this.endpoint.nodeId,
endpoint: this.endpoint.index,
supportedSensorTypes: supported,
});
return this.applHost.sendCommand(cc, this.commandOptions);
}
}

@commandClass(CommandClasses["Binary Sensor"])
Expand Down Expand Up @@ -319,7 +360,19 @@ export class BinarySensorCCReport extends BinarySensorCC {
public persistValues(applHost: ZWaveApplicationHost): boolean {
if (!super.persistValues(applHost)) return false;

const binarySensorValue = BinarySensorCCValues.state(this.type);
// Workaround for devices reporting with sensor type Any -> find first supported sensor type and use that
let sensorType = this.type;
if (sensorType === BinarySensorType.Any) {
const supportedSensorTypes = this.getValue<BinarySensorType[]>(
applHost,
BinarySensorCCValues.supportedSensorTypes,
);
if (supportedSensorTypes?.length) {
sensorType = supportedSensorTypes[0];
}
}

const binarySensorValue = BinarySensorCCValues.state(sensorType);
this.setMetadata(applHost, binarySensorValue, binarySensorValue.meta);
this.setValue(applHost, binarySensorValue, this.value);

Expand Down Expand Up @@ -354,6 +407,8 @@ function testResponseForBinarySensorGet(
sent.sensorType == undefined
|| sent.sensorType === BinarySensorType.Any
|| received.type === sent.sensorType
// This is technically not correct, but some devices do this anyways
|| received.type === BinarySensorType.Any
);
}

Expand Down Expand Up @@ -399,24 +454,44 @@ export class BinarySensorCCGet extends BinarySensorCC {
}
}

// @publicAPI
export interface BinarySensorCCSupportedReportOptions {
supportedSensorTypes: BinarySensorType[];
}

@CCCommand(BinarySensorCommand.SupportedReport)
export class BinarySensorCCSupportedReport extends BinarySensorCC {
public constructor(
host: ZWaveHost,
options: CommandClassDeserializationOptions,
options:
| CommandClassDeserializationOptions
| (BinarySensorCCSupportedReportOptions & CCCommandOptions),
) {
super(host, options);

validatePayload(this.payload.length >= 1);
// The enumeration starts at 1, but the first (reserved) bit is included
// in the report
this.supportedSensorTypes = parseBitMask(this.payload, 0).filter(
(t) => t !== 0,
);
if (gotDeserializationOptions(options)) {
validatePayload(this.payload.length >= 1);
// The enumeration starts at 1, but the first (reserved) bit is included
// in the report
this.supportedSensorTypes = parseBitMask(this.payload, 0).filter(
(t) => t !== 0,
);
} else {
this.supportedSensorTypes = options.supportedSensorTypes;
}
}

@ccValue(BinarySensorCCValues.supportedSensorTypes)
public readonly supportedSensorTypes: readonly BinarySensorType[];
public supportedSensorTypes: BinarySensorType[];

public serialize(): Buffer {
this.payload = encodeBitMask(
this.supportedSensorTypes.filter((t) => t !== BinarySensorType.Any),
undefined,
0,
);
return super.serialize();
}

public toLogEntry(applHost: ZWaveApplicationHost): MessageOrCCLogEntry {
return {
Expand Down
6 changes: 6 additions & 0 deletions packages/testing/src/CCSpecificCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import {
type ConfigValueFormat,
} from "@zwave-js/core";

export interface BinarySensorCCCapabilities {
supportedSensorTypes: number[];
getValue?: (sensorType: number | undefined) => boolean | undefined;
}

export interface ConfigurationCCCapabilities {
// We don't have bulk support implemented in the mocks
bulkSupport?: false;
Expand Down Expand Up @@ -115,6 +120,7 @@ export interface ScheduleEntryLockCCCapabilities {
export type CCSpecificCapabilities = {
[CommandClasses.Configuration]: ConfigurationCCCapabilities;
[CommandClasses.Notification]: NotificationCCCapabilities;
[48 /* Binary Sensor */]: BinarySensorCCCapabilities;
[49 /* Multilevel Sensor */]: MultilevelSensorCCCapabilities;
[121 /* Sound Switch */]: SoundSwitchCCCapabilities;
[106 /* Window Covering */]: WindowCoveringCCCapabilities;
Expand Down
2 changes: 2 additions & 0 deletions packages/zwave-js/src/lib/node/MockNodeBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {

import { CommandClasses } from "@zwave-js/core";
import { BasicCCBehaviors } from "./mockCCBehaviors/Basic";
import { BinarySensorCCBehaviors } from "./mockCCBehaviors/BinarySensor";
import { ConfigurationCCBehaviors } from "./mockCCBehaviors/Configuration";
import { EnergyProductionCCBehaviors } from "./mockCCBehaviors/EnergyProduction";
import { ManufacturerSpecificCCBehaviors } from "./mockCCBehaviors/ManufacturerSpecific";
Expand Down Expand Up @@ -291,6 +292,7 @@ export function createDefaultBehaviors(): MockNodeBehavior[] {
respondToS2ZWavePlusCCGet,

...BasicCCBehaviors,
...BinarySensorCCBehaviors,
...ConfigurationCCBehaviors,
...EnergyProductionCCBehaviors,
...ManufacturerSpecificCCBehaviors,
Expand Down
95 changes: 95 additions & 0 deletions packages/zwave-js/src/lib/node/mockCCBehaviors/BinarySensor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
BinarySensorCCGet,
BinarySensorCCReport,
BinarySensorCCSupportedGet,
BinarySensorCCSupportedReport,
BinarySensorType,
} from "@zwave-js/cc";
import { CommandClasses } from "@zwave-js/core";
import type { BinarySensorCCCapabilities } from "@zwave-js/testing";
import {
type MockNodeBehavior,
MockZWaveFrameType,
createMockZWaveRequestFrame,
} from "@zwave-js/testing";

const defaultCapabilities: BinarySensorCCCapabilities = {
supportedSensorTypes: [],
};

const respondToBinarySensorSupportedGet: MockNodeBehavior = {
async onControllerFrame(controller, self, frame) {
if (
frame.type === MockZWaveFrameType.Request
&& frame.payload instanceof BinarySensorCCSupportedGet
) {
const capabilities = {
...defaultCapabilities,
...self.getCCCapabilities(
CommandClasses["Binary Sensor"],
frame.payload.endpointIndex,
),
};
const cc = new BinarySensorCCSupportedReport(self.host, {
nodeId: controller.host.ownNodeId,
supportedSensorTypes: capabilities.supportedSensorTypes,
});
await self.sendToController(
createMockZWaveRequestFrame(cc, {
ackRequested: false,
}),
);
return true;
}
return false;
},
};

const respondToBinarySensorGet: MockNodeBehavior = {
async onControllerFrame(controller, self, frame) {
if (
frame.type === MockZWaveFrameType.Request
&& frame.payload instanceof BinarySensorCCGet
) {
const capabilities = {
...defaultCapabilities,
...self.getCCCapabilities(
CommandClasses["Binary Sensor"],
frame.payload.endpointIndex,
),
};

let sensorType: BinarySensorType | undefined;
if (
frame.payload.sensorType == undefined
|| frame.payload.sensorType === BinarySensorType.Any
) {
// If the sensor type is not specified, use the first supported one
sensorType = capabilities.supportedSensorTypes[0];
} else {
sensorType = frame.payload.sensorType;
}

if (sensorType != undefined) {
const value = capabilities.getValue?.(sensorType) ?? false;
const cc = new BinarySensorCCReport(self.host, {
nodeId: controller.host.ownNodeId,
type: sensorType,
value,
});
await self.sendToController(
createMockZWaveRequestFrame(cc, {
ackRequested: false,
}),
);
}
return true;
}
return false;
},
};

export const BinarySensorCCBehaviors = [
respondToBinarySensorSupportedGet,
respondToBinarySensorGet,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
BinarySensorCCGet,
BinarySensorCCReport,
BinarySensorCCValues,
BinarySensorType,
} from "@zwave-js/cc";
import { CommandClasses } from "@zwave-js/core";
import {
type MockNodeBehavior,
MockZWaveFrameType,
ccCaps,
createMockZWaveRequestFrame,
} from "@zwave-js/testing";
import { defaultCapabilities } from "../../node/mockCCBehaviors/UserCode";
import { integrationTest } from "../integrationTestSuite";

integrationTest(
"When a node sends a Binary Sensor Report with type 0xFF (Any), use the first supported sensor instead",
{
// debug: true,

nodeCapabilities: {
commandClasses: [
CommandClasses.Version,
ccCaps({
ccId: CommandClasses["Binary Sensor"],
isSupported: true,
version: 2,
supportedSensorTypes: [BinarySensorType.Motion],
}),
],
},

customSetup: async (driver, mockController, mockNode) => {
const respondToBinarySensorGet: MockNodeBehavior = {
async onControllerFrame(controller, self, frame) {
if (
frame.type === MockZWaveFrameType.Request
&& frame.payload instanceof BinarySensorCCGet
) {
const capabilities = {
...defaultCapabilities,
...self.getCCCapabilities(
CommandClasses["Binary Sensor"],
frame.payload.endpointIndex,
),
};

// Incorrectly respond with 0xFF as the sensor type
const cc = new BinarySensorCCReport(self.host, {
nodeId: controller.host.ownNodeId,
type: BinarySensorType.Any,
value: true,
});
await self.sendToController(
createMockZWaveRequestFrame(cc, {
ackRequested: false,
}),
);

return true;
}
return false;
},
};
mockNode.defineBehavior(respondToBinarySensorGet);
},

async testBody(t, driver, node, mockController, mockNode) {
// Even though the node reports a sensor type of 0xFF (Any),
// there should be no value for the type Any, and only one for type Motion
const anyValue = node.getValue(
BinarySensorCCValues.state(BinarySensorType.Any).id,
);
t.is(anyValue, undefined);

const motionValue = node.getValue(
BinarySensorCCValues.state(BinarySensorType.Motion).id,
);
t.is(motionValue, true);
},
},
);
Loading