From 9a8063e2dcf22a89c5325d959738e7e724c2b0ab Mon Sep 17 00:00:00 2001 From: Christopher Piggott Date: Sat, 25 Apr 2026 22:23:59 -0400 Subject: [PATCH 1/3] feat: allow writes to BACnet properties other than Present_Value --- bacnet_client.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/bacnet_client.js b/bacnet_client.js index fd43309..21689ab 100644 --- a/bacnet_client.js +++ b/bacnet_client.js @@ -1648,6 +1648,7 @@ class BacnetClient extends EventEmitter { let objectType = point.meta.objectId.type; let resolvedAppTag = that._resolveAppTag(objectType, options.appTag); let resolvedValue = that._coerceWriteValue(value, resolvedAppTag); + let resolvedPropertyId = that._resolveWritePropertyId(point.propertyId); let writeObject = { address: addressObject, @@ -1657,7 +1658,7 @@ class BacnetClient extends EventEmitter { }, values: { property: { - id: 85, + id: resolvedPropertyId, index: point.meta.arrayIndex, }, value: [ @@ -1691,7 +1692,7 @@ class BacnetClient extends EventEmitter { that.client.writeProperty( point.address, point.objectId, - baEnum.PropertyIdentifier.PRESENT_VALUE, + point.values.property.id, point.values.value, point.options, (err, value) => { @@ -2386,6 +2387,62 @@ class BacnetClient extends EventEmitter { } } + _resolveWritePropertyId(propertyId) { + + // Default to Present_Value to preserve existing behavior when no property is supplied. + if (propertyId === null || typeof propertyId === "undefined") { + return baEnum.PropertyIdentifier.PRESENT_VALUE; + } + + // Allow raw numeric BACnet property identifiers. + if (typeof propertyId === "number" && Number.isInteger(propertyId)) { + return propertyId; + } + + if (typeof propertyId === "string") { + const trimmed = propertyId.trim(); + const upper = trimmed.toUpperCase(); + + // First try a direct enum lookup using the uppercase string exactly as given. + // Example: "present_value" -> "PRESENT_VALUE" + const exactResolvedPropertyId = baEnum.PropertyIdentifier[upper]; + if (typeof exactResolvedPropertyId === "number") { + return exactResolvedPropertyId; + } + + // Final fallback: compare without separators so camelCase/PascalCase can still match + // BACnet enum names that use underscores. + // Example: + // user input "presentValue" -> "PRESENTVALUE" + // enum key "PRESENT_VALUE" -> "PRESENTVALUE" + // + const underscored = upper.replace(/[\s-]+/g, "_"); + const underscoredResolvedPropertyId = baEnum.PropertyIdentifier[underscored]; + if (typeof underscoredResolvedPropertyId === "number") { + return underscoredResolvedPropertyId; + } + + // Separator-insensitiver fallback: PresentValue -> PRESENT_VALUE + // This strips off separators in both the user input and the enum key + // to try to find a match + const compact = upper.replace(/[\s_-]+/g, ""); + + // The key should already be in uppercase but just in case, let's force it. + // This protects against somebody doing something different in the future. + const matchedKey = Object.keys(baEnum.PropertyIdentifier).find( + (key) => key.toUpperCase().replace(/_/g, "") === compact + ); + + if (matchedKey) { + return baEnum.PropertyIdentifier[matchedKey]; + } + + throw new Error(`Invalid BACnet property name: ${propertyId}`); + } + + throw new Error(`Invalid BACnet property id type: ${typeof propertyId}`); + } + _coerceWriteValue(value, appTag) { switch (appTag) { case 9: // ENUMERATED - binary objects expect 0 (inactive) or 1 (active) From 3bb026d4c7a81e4e2cbbf8adf160637071c50efd Mon Sep 17 00:00:00 2001 From: Rav Panchalingam Date: Thu, 30 Apr 2026 10:12:48 +0800 Subject: [PATCH 2/3] feat(write): add BACnet property selector to Write node UI Wires the new propertyId resolver to the Write node by adding a node-level Property dropdown alongside the existing Application Tag and Priority selectors, defaulting to PRESENT_VALUE for backward compatibility. The configured property is stamped onto each point in pointsToWrite on input, so doWrite() resolves it per-point as point.propertyId. Per-point overrides supplied via msg.options.pointsToWrite are preserved. Resolves the UI gap raised in PR review: without this, the resolver always received undefined and silently fell back to PRESENT_VALUE. --- bacnet_write.html | 26 +++++++++++++++++++++++++- bacnet_write.js | 9 ++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/bacnet_write.html b/bacnet_write.html index 945331b..dbac8df 100644 --- a/bacnet_write.html +++ b/bacnet_write.html @@ -10,6 +10,7 @@ name: { value: "" }, applicationTag: { value: "-1" }, priority: { value: "16" }, + propertyId: { value: "85" }, pointsToWrite: { value: [] }, writeDevices: { value: [] }, hiddenDeployToggle: { value: false }, @@ -515,6 +516,7 @@ document.getElementById("node-input-applicationTag").value = node.applicationTag; document.getElementById("node-input-priority").value = node.priority; + document.getElementById("node-input-propertyId").value = node.propertyId || "85"; var menu = document.querySelector(".context-menu"); window.addEventListener("click", (event) => { @@ -985,6 +987,24 @@ +
+ + +
+
Write List

Properties

    - This tab allows the user to choose the Application Tag and Priority to be used in a Write function. Please make sure + This tab allows the user to choose the Application Tag, BACnet Property, and Priority to be used in a Write function. + The Property defaults to PRESENT_VALUE; select a different one to write to a non-default property + (e.g., RELINQUISH_DEFAULT, OUT_OF_SERVICE). To override per-point, set + propertyId on individual entries in msg.options.pointsToWrite (accepts a numeric + identifier or a name like "PRESENT_VALUE" / "presentValue"). Please make sure the device / controller is configured correctly to accept the write command.

diff --git a/bacnet_write.js b/bacnet_write.js index 83e71ec..e7adb9e 100644 --- a/bacnet_write.js +++ b/bacnet_write.js @@ -8,6 +8,7 @@ module.exports = function (RED) { var node = this; node.priority = config.priority; node.appTag = config.applicationTag; + node.propertyId = config.propertyId; node.pointsToWrite = config.pointsToWrite; node.writeDevices = config.writeDevices; @@ -18,13 +19,19 @@ module.exports = function (RED) { let value = msg.payload == "null" ? null : msg.payload; let priority = node.priority == "null" ? null : parseInt(node.priority); + // Stamp the configured BACnet property onto each point so doWrite() resolves it per-point. + // Per-point propertyId already on the point (e.g., supplied via msg) wins. + let pointsToWrite = Array.isArray(node.pointsToWrite) + ? node.pointsToWrite.map((p) => ({ ...p, propertyId: p.propertyId ?? node.propertyId })) + : node.pointsToWrite; + let output = { type: "Write", id: node.id, options: { priority: priority, appTag: parseInt(node.appTag), - pointsToWrite: node.pointsToWrite + pointsToWrite: pointsToWrite }, value: value, outputType: { From 4ddf5eadaecf320a08a5daa2ea02b445cf3ebaca Mon Sep 17 00:00:00 2001 From: Rav Panchalingam Date: Thu, 30 Apr 2026 10:29:47 +0800 Subject: [PATCH 3/3] feat(write): accept any BACnet property in the Property field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the closed Property dropdown with a free-text input plus a datalist of common writable properties for autocomplete. Users can now target any of the ~370 BACnet property identifiers — by name (any case or separator style) or numeric ID — without us having to enumerate them in the UI. Resolver tweaks (bacnet_client.js): - Empty / whitespace-only string defaults to PRESENT_VALUE. - Numeric strings ("85") are accepted as raw property identifiers. UI tweaks (bacnet_write.html): - Property field is now backed by a datalist; placeholder shows "PRESENT_VALUE" for the default. - Default value is now empty (was "85") so the placeholder is visible. Addresses review feedback on PR #42 — the closed list was unnecessarily limiting given the resolver already accepts any name or number. --- bacnet_client.js | 14 ++++++++++ bacnet_write.html | 70 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/bacnet_client.js b/bacnet_client.js index 21689ab..2f9122a 100644 --- a/bacnet_client.js +++ b/bacnet_client.js @@ -2401,6 +2401,20 @@ class BacnetClient extends EventEmitter { if (typeof propertyId === "string") { const trimmed = propertyId.trim(); + + // Empty / whitespace-only string also defaults to Present_Value, matching the + // null/undefined branch above. The Write node UI uses an empty default so the + // placeholder ("PRESENT_VALUE") shows. + if (trimmed === "") { + return baEnum.PropertyIdentifier.PRESENT_VALUE; + } + + // Numeric strings ("85") are treated as raw numeric BACnet property identifiers, + // since users see numeric IDs alongside names in the UI suggestions. + if (/^\d+$/.test(trimmed)) { + return parseInt(trimmed, 10); + } + const upper = trimmed.toUpperCase(); // First try a direct enum lookup using the uppercase string exactly as given. diff --git a/bacnet_write.html b/bacnet_write.html index dbac8df..531da84 100644 --- a/bacnet_write.html +++ b/bacnet_write.html @@ -10,7 +10,7 @@ name: { value: "" }, applicationTag: { value: "-1" }, priority: { value: "16" }, - propertyId: { value: "85" }, + propertyId: { value: "" }, pointsToWrite: { value: [] }, writeDevices: { value: [] }, hiddenDeployToggle: { value: false }, @@ -516,7 +516,7 @@ document.getElementById("node-input-applicationTag").value = node.applicationTag; document.getElementById("node-input-priority").value = node.priority; - document.getElementById("node-input-propertyId").value = node.propertyId || "85"; + document.getElementById("node-input-propertyId").value = node.propertyId || ""; var menu = document.querySelector(".context-menu"); window.addEventListener("click", (event) => { @@ -991,18 +991,51 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -1060,10 +1093,11 @@

Properties

    This tab allows the user to choose the Application Tag, BACnet Property, and Priority to be used in a Write function. - The Property defaults to PRESENT_VALUE; select a different one to write to a non-default property - (e.g., RELINQUISH_DEFAULT, OUT_OF_SERVICE). To override per-point, set - propertyId on individual entries in msg.options.pointsToWrite (accepts a numeric - identifier or a name like "PRESENT_VALUE" / "presentValue"). Please make sure + The Property field accepts any BACnet property — by name (e.g., PRESENT_VALUE, + RELINQUISH_DEFAULT, OUT_OF_SERVICE, also presentValue or + relinquish_default) or by numeric identifier (e.g., 85). Common picks are offered as + autocomplete suggestions; leaving the field blank defaults to PRESENT_VALUE. To override per-point, + set propertyId on individual entries in msg.options.pointsToWrite. Please make sure the device / controller is configured correctly to accept the write command.