diff --git a/packages/cc/src/cc/BinarySensorCC.ts b/packages/cc/src/cc/BinarySensorCC.ts index 5938766165a2..125cbb762865 100644 --- a/packages/cc/src/cc/BinarySensorCC.ts +++ b/packages/cc/src/cc/BinarySensorCC.ts @@ -4,7 +4,9 @@ import { type MaybeNotKnown, type MessageOrCCLogEntry, MessagePriority, + type SupervisionResult, ValueMetadata, + encodeBitMask, parseBitMask, validatePayload, } from "@zwave-js/core/safe"; @@ -71,8 +73,10 @@ export class BinarySensorCCAPI extends PhysicalCCAPI { public supportsCommand(cmd: BinarySensorCommand): MaybeNotKnown { 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); @@ -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 { + 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, @@ -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 { + 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"]) @@ -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( + 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); @@ -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 ); } @@ -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 { diff --git a/packages/cc/src/cc/index.ts b/packages/cc/src/cc/index.ts index 4ca2320d059f..84eab606ef60 100644 --- a/packages/cc/src/cc/index.ts +++ b/packages/cc/src/cc/index.ts @@ -85,6 +85,7 @@ export { export type { BinarySensorCCGetOptions, BinarySensorCCReportOptions, + BinarySensorCCSupportedReportOptions, } from "./BinarySensorCC"; export { BinarySensorCC, diff --git a/packages/testing/src/CCSpecificCapabilities.ts b/packages/testing/src/CCSpecificCapabilities.ts index 789891a26925..0aa275283445 100644 --- a/packages/testing/src/CCSpecificCapabilities.ts +++ b/packages/testing/src/CCSpecificCapabilities.ts @@ -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; @@ -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; diff --git a/packages/zwave-js/src/lib/node/MockNodeBehaviors.ts b/packages/zwave-js/src/lib/node/MockNodeBehaviors.ts index 4efa04e44e99..9e47d226ca60 100644 --- a/packages/zwave-js/src/lib/node/MockNodeBehaviors.ts +++ b/packages/zwave-js/src/lib/node/MockNodeBehaviors.ts @@ -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"; @@ -291,6 +292,7 @@ export function createDefaultBehaviors(): MockNodeBehavior[] { respondToS2ZWavePlusCCGet, ...BasicCCBehaviors, + ...BinarySensorCCBehaviors, ...ConfigurationCCBehaviors, ...EnergyProductionCCBehaviors, ...ManufacturerSpecificCCBehaviors, diff --git a/packages/zwave-js/src/lib/node/mockCCBehaviors/BinarySensor.ts b/packages/zwave-js/src/lib/node/mockCCBehaviors/BinarySensor.ts new file mode 100644 index 000000000000..0788c94e14ba --- /dev/null +++ b/packages/zwave-js/src/lib/node/mockCCBehaviors/BinarySensor.ts @@ -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, +]; diff --git a/packages/zwave-js/src/lib/test/compat/binarySensorReportAnyUseFirstSupported.test.ts b/packages/zwave-js/src/lib/test/compat/binarySensorReportAnyUseFirstSupported.test.ts new file mode 100644 index 000000000000..0a3db216b072 --- /dev/null +++ b/packages/zwave-js/src/lib/test/compat/binarySensorReportAnyUseFirstSupported.test.ts @@ -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); + }, + }, +);