From 0961a5181b07cefd943b6ede904cb63df4efee61 Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Thu, 4 Jun 2026 16:20:19 +0200 Subject: [PATCH] chore: fixes ReadPropertyMultiple ignoring unknown objects/properties (closes #33) --- src/objects/device/device.ts | 11 +++--- src/objects/generic/object.ts | 17 ++++----- src/tests/bacnet-stack-client.ts | 20 ++++++++++- src/tests/read-multiple.test.ts | 62 ++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 src/tests/read-multiple.test.ts diff --git a/src/objects/device/device.ts b/src/objects/device/device.ts index e987d3a..ce8d591 100644 --- a/src/objects/device/device.ts +++ b/src/objects/device/device.ts @@ -696,13 +696,10 @@ export class BDDevice extends BDObject implements AsyncEventEmitter { const { header, invokeId, payload: { properties } } = req; if (!header) return; - const values: BACNetReadAccess[] = []; - for (const { objectId, properties: objProperties } of properties) { - const object = this.#objects.get(getObjectUID(objectId)); - if (object) { - values.push(await object.___readPropertyMultiple(objProperties)); - } - } + const values: BACNetReadAccess[] = await Promise.all(properties.map(({ objectId, properties: objProperties }) => { + const object = this.#getObjectByIdOrThrow(objectId); + return object.___readPropertyMultiple(objProperties); + })); this.#client.readPropertyMultipleResponse(header.sender, invokeId!, values); }); }; diff --git a/src/objects/generic/object.ts b/src/objects/generic/object.ts index 1d14ef2..d405d00 100644 --- a/src/objects/generic/object.ts +++ b/src/objects/generic/object.ts @@ -320,16 +320,13 @@ export class BDObject extends AsyncEventEmitter { return this.___readPropertyMultipleAll(); } const ctx: BDPropertyAccessContext = { date: new Date() }; - const values: BACNetReadAccess['values'] = []; - for (const identifier of identifiers) { - const property = this.#properties.get(identifier.id); - if (property) { - values.push({ - property: identifier, - value: ensureArray(property.___readData(identifier.index, ctx)) - }); - } - } + const values: BACNetReadAccess['values'] = await Promise.all(identifiers.map((identifier) => { + const property = this.___getPropertyOrThrow(identifier.id); + return { + property: identifier, + value: ensureArray(property.___readData(identifier.index, ctx)) + }; + })); return { objectId: this.identifier.value, values }; } diff --git a/src/tests/bacnet-stack-client.ts b/src/tests/bacnet-stack-client.ts index 57cab0a..0be4a17 100644 --- a/src/tests/bacnet-stack-client.ts +++ b/src/tests/bacnet-stack-client.ts @@ -21,6 +21,24 @@ export const bsReadProperty = async (devIn: number, objType: ObjectType, objIn: return await bsExec('bacrp', args); }; +export const bsReadMultiple = async (devIn: number, tuples: [objType: ObjectType, objIn: number, propId: PropertyIdentifier, index?: number][]) => { + // bacrpm 123 analog-input 77 85 analog-input 78 85 + const args = tuples.flatMap((tuple) => { + const ser = [`${tuple[0]}`, `${tuple[1]}`]; + if (tuple[3] !== undefined) { + ser.push(`${tuple[2]}[${tuple[3]}]`); + } else { + ser.push(`${tuple[2]}`); + } + return ser; + }); + args.unshift(`${devIn}`); + const res = await bsExec('bacrpm', args) + return res.replaceAll(/\r?\n/g, ' ') + .replaceAll(/\s+/g, ' ') + .trim(); +}; + export const bsWriteProperty = async ( devIn: number, objType: ObjectType, @@ -41,4 +59,4 @@ export const bsWriteProperty = async ( `${tag}`, `${value}`, ]); -}; \ No newline at end of file +}; diff --git a/src/tests/read-multiple.test.ts b/src/tests/read-multiple.test.ts new file mode 100644 index 0000000..8ec2c2e --- /dev/null +++ b/src/tests/read-multiple.test.ts @@ -0,0 +1,62 @@ +import { it, describe, beforeEach, afterEach } from 'node:test'; +import { deepStrictEqual } from 'node:assert'; +import { BDDevice } from '../objects/device/device.js'; +import { bsReadMultiple, bsReadProperty } from './bacnet-stack-client.js'; +import { BDDateTimeValue } from '../objects/temporal/datetimevalue.js'; +import { EngineeringUnits, ObjectType, PropertyIdentifier } from '@bacnet-js/client'; +import { BDAnalogValue } from '../objects/numeric/analogvalue.js'; + +describe('ReadMultiple', () => { + + let device: BDDevice; + let av_1: BDAnalogValue; + let av_2: BDAnalogValue; + + beforeEach(async () => { + device = new BDDevice(1, { + name: 'Test Device', + }); + device.on('error', console.error); + av_1 = device.addObject(new BDAnalogValue({ + name: 'Test AV 1', + description: 'A test analog value', + presentValue: 10, + unit: EngineeringUnits.PERCENT, + })); + av_2 = device.addObject(new BDAnalogValue({ + name: 'Test AV 2', + description: 'A test analog value', + presentValue: 90, + unit: EngineeringUnits.PERCENT, + })); + }); + + afterEach(async () => { + device.destroy(); + }); + + it('should read multiple Present_Value properties across different objects', async () => { + const res = await bsReadMultiple(device.identifier.value.instance, [ + [ObjectType.ANALOG_VALUE, av_1.identifier.value.instance, PropertyIdentifier.PRESENT_VALUE], + [ObjectType.ANALOG_VALUE, av_2.identifier.value.instance, PropertyIdentifier.PRESENT_VALUE], + ]); + deepStrictEqual(res, `analog-value #1 { present-value: 10.000000 } analog-value #2 { present-value: 90.000000 }`); + }); + + it('should fail to read multiple Present_Value properties across a mix of existent and non-existent properties', async () => { + const res = await bsReadMultiple(device.identifier.value.instance, [ + [ObjectType.ANALOG_VALUE, av_1.identifier.value.instance, PropertyIdentifier.PRESENT_VALUE], + [ObjectType.ANALOG_VALUE, 42, PropertyIdentifier.PRESENT_VALUE], + ]); + deepStrictEqual(res, ``); + }); + + it('should fail to read a mix of existent and non-existent properties on the same object', async () => { + const res = await bsReadMultiple(device.identifier.value.instance, [ + [ObjectType.ANALOG_VALUE, av_1.identifier.value.instance, PropertyIdentifier.PRESENT_VALUE], + [ObjectType.ANALOG_VALUE, av_1.identifier.value.instance, PropertyIdentifier.ACCESS_EVENT_TAG], + ]); + deepStrictEqual(res, ``); + }); + +});