Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
75 changes: 73 additions & 2 deletions bacnet_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1657,7 +1658,7 @@ class BacnetClient extends EventEmitter {
},
values: {
property: {
id: 85,
id: resolvedPropertyId,
index: point.meta.arrayIndex,
},
value: [
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -2386,6 +2387,76 @@ 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();

// 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.
// 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)
Expand Down
60 changes: 59 additions & 1 deletion bacnet_write.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
name: { value: "" },
applicationTag: { value: "-1" },
priority: { value: "16" },
propertyId: { value: "" },
pointsToWrite: { value: [] },
writeDevices: { value: [] },
hiddenDeployToggle: { value: false },
Expand Down Expand Up @@ -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 || "";

var menu = document.querySelector(".context-menu");
window.addEventListener("click", (event) => {
Expand Down Expand Up @@ -985,6 +987,57 @@
</select>
</div>

<div class="form-row bp-row">
<label for="node-input-propertyId"
><i class="icon-tag"></i><span data-i18n="bitpool-bacnet.label.propertyId"></span> Property</label
>
<input
type="text"
id="node-input-propertyId"
list="bacnet-property-suggestions"
placeholder="PRESENT_VALUE"
autocomplete="off" />
<datalist id="bacnet-property-suggestions">
<option value="PRESENT_VALUE">85 — most common write target</option>
<option value="RELINQUISH_DEFAULT">104</option>
<option value="OUT_OF_SERVICE">81</option>
<option value="POLARITY">84</option>
<option value="COV_INCREMENT">22</option>
<option value="HIGH_LIMIT">45</option>
<option value="LOW_LIMIT">59</option>
<option value="DEADBAND">25</option>
<option value="DESCRIPTION">28</option>
<option value="OBJECT_NAME">77</option>
<option value="ACTIVE_TEXT">4</option>
<option value="INACTIVE_TEXT">46</option>
<option value="MAX_PRES_VALUE">65</option>
<option value="MIN_PRES_VALUE">69</option>
<option value="RESOLUTION">106</option>
<option value="UNITS">117</option>
<option value="TIME_DELAY">113</option>
<option value="LIMIT_ENABLE">52</option>
<option value="EVENT_ENABLE">35</option>
<option value="NOTIFY_TYPE">72</option>
<option value="NOTIFICATION_CLASS">17</option>
<option value="ELAPSED_ACTIVE_TIME">33</option>
<option value="TIME_OF_ACTIVE_TIME_RESET">114</option>
<option value="TIME_OF_STATE_COUNT_RESET">115</option>
<option value="STATE_TEXT">110</option>
<option value="NUMBER_OF_STATES">74</option>
<option value="EVENT_TIME_STAMPS">130</option>
<option value="EVENT_DETECTION_ENABLE">353</option>
<option value="ALARM_VALUES">7</option>
<option value="FAULT_VALUES">39</option>
<option value="OBJECT_PROPERTY_REFERENCE">78</option>
<option value="SETPOINT">108</option>
<option value="SETPOINT_REFERENCE">109</option>
<option value="ACTION">2</option>
<option value="ACTION_TEXT">3</option>
<option value="LOCATION">58</option>
<option value="PROFILE_NAME">168</option>
</datalist>
</div>

<div class="form-row bp-row">
<label for="node-input-priority"
><i class="icon-tag"></i><span data-i18n="bitpool-bacnet.label.priority"></span> Priority</label
Expand Down Expand Up @@ -1039,7 +1092,12 @@ <h3><strong>Write List</strong></h3>
<h3><strong>Properties</strong></h3>
<ol class="node-ports">
<p>
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 field accepts any BACnet property — by name (e.g., <code>PRESENT_VALUE</code>,
<code>RELINQUISH_DEFAULT</code>, <code>OUT_OF_SERVICE</code>, also <code>presentValue</code> or
<code>relinquish_default</code>) or by numeric identifier (e.g., <code>85</code>). Common picks are offered as
autocomplete suggestions; leaving the field blank defaults to <code>PRESENT_VALUE</code>. To override per-point,
set <code>propertyId</code> on individual entries in <code>msg.options.pointsToWrite</code>. Please make sure
the device / controller is configured correctly to accept the write command.
</p>
</ol>
Expand Down
9 changes: 8 additions & 1 deletion bacnet_write.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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: {
Expand Down