@strato-automation/node-red-contrib-strato-automation
Version:
Strato Automation Official Node Red Library
2,163 lines • 84 kB
HTML
<script type="text/javascript">
(function () {
if (window.__bnPointLabelCacheInit) return;
window.__bnPointLabelCacheInit = true;
window.__bnPointLabelCache = window.__bnPointLabelCache || {};
function toAbbr(typeValue) {
var typeAbbreviationMap = {
0: "AI",
1: "AO",
2: "AV",
3: "BI",
4: "BO",
5: "BV",
12: "PID",
19: "MSV",
};
if (typeof typeValue === "number") {
return typeAbbreviationMap[typeValue] || String(typeValue);
}
var normalizedType = String(typeValue || "")
.trim()
.toUpperCase();
if (/^\d+$/.test(normalizedType)) {
return typeAbbreviationMap[+normalizedType] || normalizedType;
}
return typeAbbreviationMap[normalizedType] != null
? typeAbbreviationMap[normalizedType]
: normalizedType;
}
function buildDeviceLabel(mode, name, instance) {
var cleanName = String(name || "").trim();
var cleanInstance = String(instance || "").trim();
if (mode === "device") return cleanName || cleanInstance;
if (mode === "both") {
var middleDotSeparator = " " + String.fromCharCode(183) + " ";
if (cleanName && cleanInstance)
return cleanName + middleDotSeparator + cleanInstance;
return cleanName || cleanInstance;
}
return cleanInstance || cleanName;
}
function buildObjectLabel(mode, objectName, objectId) {
var cleanName = String(objectName || "").trim();
var cleanId = String(objectId || "").trim();
if (mode === "id") return cleanId || cleanName;
if (mode === "both") {
if (cleanName && cleanId) return cleanName + " (" + cleanId + ")";
return cleanName || cleanId;
}
return cleanName || cleanId;
}
function computePreviewLabel(pointNode, controllerInfo) {
var controllerName =
(controllerInfo && controllerInfo.controllerName) ||
pointNode.controllerName ||
"";
var controllerDeviceInstance =
(controllerInfo && controllerInfo.controllerDeviceInstance) ||
pointNode.controllerDeviceInstance ||
"";
var labelMode = pointNode.labelMode || "instance";
var objectLabelMode = pointNode.objectLabelMode || "name";
var deviceLabel = buildDeviceLabel(
labelMode,
controllerName,
controllerDeviceInstance
);
var objectId =
pointNode.objectType != null && pointNode.objectInstance != null
? toAbbr(pointNode.objectType) + "-" + pointNode.objectInstance
: "";
var objectName = (pointNode.objectName || "").trim();
var objectLabel = buildObjectLabel(objectLabelMode, objectName, objectId);
if (labelMode === "both") {
var labelParts = [];
if (objectLabel) labelParts.push(objectLabel);
if (controllerName) labelParts.push(controllerName);
if (controllerDeviceInstance) labelParts.push(controllerDeviceInstance);
if (!labelParts.length)
labelParts.push(objectId || objectName || "BACnet Point");
var middleDotSeparator = " " + String.fromCharCode(183) + " ";
return labelParts.length
? labelParts.join(middleDotSeparator)
: "BACnet Point";
}
var separator = " " + String.fromCharCode(183) + " ";
return objectLabel && deviceLabel
? objectLabel + separator + deviceLabel
: objectLabel || deviceLabel || objectId || "BACnet Point";
}
function setLabelCache(pointNode, controllerInfo, objectInfo) {
var computedLabel = computePreviewLabel(
pointNode,
objectInfo || controllerInfo || {}
);
var nodeChanged = false;
if (pointNode) {
if (controllerInfo) {
if (controllerInfo.controllerName != null) {
if (
Object.prototype.hasOwnProperty.call(pointNode, "controllerName")
) {
if (pointNode.controllerName !== controllerInfo.controllerName) {
pointNode.controllerName = controllerInfo.controllerName;
nodeChanged = true;
}
}
if (
Object.prototype.hasOwnProperty.call(pointNode, "deviceName") &&
!controllerInfo.deviceName
) {
if (pointNode.deviceName !== controllerInfo.controllerName) {
pointNode.deviceName = controllerInfo.controllerName;
nodeChanged = true;
}
}
}
if (controllerInfo.controllerDeviceInstance != null) {
if (
Object.prototype.hasOwnProperty.call(
pointNode,
"controllerDeviceInstance"
)
) {
if (pointNode.controllerDeviceInstance !== controllerInfo.controllerDeviceInstance) {
pointNode.controllerDeviceInstance =
controllerInfo.controllerDeviceInstance;
nodeChanged = true;
}
}
if (
Object.prototype.hasOwnProperty.call(pointNode, "deviceInstance")
) {
if (pointNode.deviceInstance !== controllerInfo.controllerDeviceInstance) {
pointNode.deviceInstance =
controllerInfo.controllerDeviceInstance;
nodeChanged = true;
}
}
}
}
if (objectInfo) {
if (
objectInfo.objectName != null &&
Object.prototype.hasOwnProperty.call(pointNode, "objectName")
) {
if (pointNode.objectName !== objectInfo.objectName) {
pointNode.objectName = objectInfo.objectName;
nodeChanged = true;
}
}
if (
objectInfo.units != null &&
Object.prototype.hasOwnProperty.call(pointNode, "units")
) {
if (pointNode.units !== objectInfo.units) {
pointNode.units = objectInfo.units;
nodeChanged = true;
}
}
if (
objectInfo.description != null &&
Object.prototype.hasOwnProperty.call(pointNode, "description")
) {
if (pointNode.description !== objectInfo.description) {
pointNode.description = objectInfo.description;
nodeChanged = true;
}
}
}
if (pointNode.previewLabel !== computedLabel) {
pointNode.previewLabel = computedLabel;
nodeChanged = true;
}
}
if (window.__bnPointLabelCache[pointNode.id] !== computedLabel) {
window.__bnPointLabelCache[pointNode.id] = computedLabel;
}
// Mark node as changed so Node-RED updates the label (redraw will be called after all nodes are updated)
if (nodeChanged) {
pointNode.changed = true;
}
}
function refreshAllPointLabels(onComplete) {
try {
var pointNodes = [];
if (RED.nodes && typeof RED.nodes.filterNodes === "function") {
pointNodes = pointNodes.concat(
RED.nodes.filterNodes({ type: "bacnet-point-in" }) || []
);
pointNodes = pointNodes.concat(
RED.nodes.filterNodes({ type: "bacnet-point-in-out" }) || []
);
var seenNodeIds = {};
pointNodes = pointNodes.filter(function (pointNode) {
if (!pointNode || !pointNode.id) return false;
if (seenNodeIds[pointNode.id]) return false;
seenNodeIds[pointNode.id] = true;
return true;
});
}
// Backend refreshMapping() has already updated node properties (objectName, controllerName, etc.)
// Since labels can only be updated from HTML, we need to:
// 1. Fetch latest node properties from RED.nodes.node() to get backend updates
// 2. Update frontend node properties to match backend
// 3. Clear label cache to force recalculation
// 4. Trigger redraw so label() function runs with updated properties
// First, fetch latest properties from backend nodes via HTTP endpoint
// This ensures we get the actual backend values updated by refreshMapping()
// RED.nodes.node() may return cached frontend values, so we need to fetch from backend
// A single batch request returns every point node's properties at once
// (one request per refresh instead of one request per node).
var nodesUpdated = 0;
$.ajax({
url: "bacnet-point-in/nodePropertiesBatch",
method: "GET",
dataType: "json"
})
.done(function(backendPropsById) {
if (!backendPropsById || typeof backendPropsById !== "object") return;
pointNodes.forEach(function(pointNode) {
if (!pointNode || !pointNode.id || pointNode._closed) return;
var backendProps = backendPropsById[pointNode.id];
if (backendProps && backendProps.nodeId === pointNode.id) {
var updated = false;
var oldObjectName = String(pointNode.objectName || "");
var oldControllerName = String(pointNode.controllerName || "");
var oldControllerDeviceInstance = String(pointNode.controllerDeviceInstance || "");
var oldUnits = String(pointNode.units || "");
var oldDescription = String(pointNode.description || "");
// Update frontend node properties from backend
if (backendProps.objectName !== oldObjectName) {
pointNode.objectName = backendProps.objectName;
updated = true;
}
if (backendProps.controllerName !== oldControllerName) {
pointNode.controllerName = backendProps.controllerName;
updated = true;
}
if (backendProps.controllerDeviceInstance !== oldControllerDeviceInstance) {
pointNode.controllerDeviceInstance = backendProps.controllerDeviceInstance;
updated = true;
}
if (backendProps.units !== oldUnits) {
pointNode.units = backendProps.units;
updated = true;
}
if (backendProps.description !== oldDescription) {
pointNode.description = backendProps.description;
updated = true;
}
if (updated) {
nodesUpdated++;
}
// Always clear cache for this node to force label recalculation
if (window.__bnPointLabelCache && window.__bnPointLabelCache[pointNode.id]) {
delete window.__bnPointLabelCache[pointNode.id];
}
}
});
})
.fail(function() {
// If the batch fetch fails there is no reliable backend data;
// labels are recalculated below from the current editor values
// (the .always() handler clears every label cache and redraws).
})
.always(function() {
// Clear all label caches to force label recalculation
var cacheClearedCount = 0;
if (window.__bnPointLabelCache) {
Object.keys(window.__bnPointLabelCache).forEach(function(nodeId) {
delete window.__bnPointLabelCache[nodeId];
cacheClearedCount++;
});
}
// Multiple redraw attempts to ensure labels update
function forceRedraw() {
try {
if (RED.view && typeof RED.view.redraw === "function") {
RED.view.redraw(true);
return true;
}
} catch (e) {
}
return false;
}
// Immediate redraw
forceRedraw();
// Redraw after short delay
setTimeout(function() {
forceRedraw();
}, 100);
// Redraw after longer delay to catch any late updates
setTimeout(function() {
forceRedraw();
}, 500);
if (typeof onComplete === "function") onComplete();
});
} catch (refreshError) {
if (typeof onComplete === "function") onComplete();
}
}
// Debounced/coalesced entry point for refreshAllPointLabels: bursts of
// events (e.g. hundreds of nodes:add during flow import) collapse into a
// single trailing refresh, and refreshes never overlap - a request made
// while one is in flight queues exactly one follow-up run.
var __bnRefreshTimer = null;
var __bnRefreshInFlight = false;
var __bnRefreshQueued = false;
var __bnRefreshCallbacks = [];
var __bnFlowsLoaded = false;
function runRefreshNow() {
if (__bnRefreshInFlight) {
__bnRefreshQueued = true;
return;
}
__bnRefreshInFlight = true;
var completionCallbacks = __bnRefreshCallbacks;
__bnRefreshCallbacks = [];
refreshAllPointLabels(function () {
__bnRefreshInFlight = false;
completionCallbacks.forEach(function (callback) {
try {
callback();
} catch (_ignoredError) {}
});
if (__bnRefreshQueued) {
__bnRefreshQueued = false;
scheduleRefreshAllPointLabels();
}
});
}
// Optional onComplete callback fires once the refresh it was queued for
// (or the follow-up run, if one was already in flight) has completed.
function scheduleRefreshAllPointLabels(onComplete) {
if (typeof onComplete === "function") __bnRefreshCallbacks.push(onComplete);
if (__bnRefreshTimer) clearTimeout(__bnRefreshTimer);
__bnRefreshTimer = setTimeout(function () {
__bnRefreshTimer = null;
runRefreshNow();
}, 300);
}
function migrateOldBacnetPointNodes() {
try {
// Check for old bacnet-point nodes and migrate them to bacnet-point-in
var allNodes = RED.nodes.filterNodes ? RED.nodes.filterNodes({ type: "bacnet-point" }) : [];
if (allNodes && allNodes.length > 0) {
var migrationCount = 0;
allNodes.forEach(function(node) {
if (node && node.type === "bacnet-point") {
// Migrate to bacnet-point-in
node.type = "bacnet-point-in";
// Set default values for new properties if missing
if (node.source === undefined || node.source === null) {
node.source = "mqtt";
}
if (node.advancedMode === undefined) {
node.advancedMode = false;
}
if (node.output_type === undefined) {
node.output_type = "ip";
}
if (node.bypass_priority === undefined) {
node.bypass_priority = false;
}
if (node.initialized === undefined) {
node.initialized = false;
}
// Mark node as changed
node.changed = true;
migrationCount++;
}
});
if (migrationCount > 0) {
// Mark flow as dirty so user can deploy
// Only mark as dirty if nodes were actually migrated (they will have changed = true)
try {
// Check if any migrated nodes have changed = true before setting dirty
var hasChangedNodes = false;
for (var i = 0; i < allNodes.length; i++) {
var node = allNodes[i];
if (node && node.type === "bacnet-point-in" && node.changed === true) {
hasChangedNodes = true;
break;
}
}
if (hasChangedNodes && RED.nodes && typeof RED.nodes.dirty === "function") {
RED.nodes.dirty(true);
}
RED.view.redraw(true);
} catch (error) {
console.error("Error marking flow as dirty after migration:", error);
}
// Show notification to user
if (typeof RED.notify !== "undefined" && RED.notify.info) {
RED.notify.info(
"Migrated " + migrationCount + " bacnet-point node(s) to bacnet-point-in. Please deploy to apply changes.",
{ timeout: 8000 }
);
}
}
}
} catch (error) {
console.error("Error during bacnet-point migration:", error);
}
}
RED.events.on("flows:loaded", function() {
__bnFlowsLoaded = true;
migrateOldBacnetPointNodes();
scheduleRefreshAllPointLabels();
});
// Validate nodes immediately when they're added (including copy/paste)
RED.events.on("nodes:add", function(addedNode) {
try {
if (!addedNode) return;
if (addedNode.type === "bacnet-point-in" || addedNode.type === "bacnet-point-out") {
// Validate the node immediately so it's recognized by HTTP endpoints
if (typeof RED.editor !== "undefined" && RED.editor.validateNode) {
RED.editor.validateNode(addedNode);
}
// Also trigger a redraw to ensure the node is properly registered
// (debounced via a window-shared timer: one redraw per burst of
// added nodes, even across the point-in and point-out handlers)
if (window.__bnValidateRedrawTimer) clearTimeout(window.__bnValidateRedrawTimer);
window.__bnValidateRedrawTimer = setTimeout(function() {
window.__bnValidateRedrawTimer = null;
if (RED.view && RED.view.redraw) {
RED.view.redraw(true);
}
}, 50);
}
} catch (error) {
console.error("Error validating node on add:", error);
}
});
RED.events.on("deploy", function() {
// Function to run the full refresh sequence
function runRefreshSequence() {
// Get all point nodes and update their label cache directly (like setLabelCacheFromFields does)
var pointNodes = [];
if (RED.nodes && typeof RED.nodes.filterNodes === "function") {
pointNodes = RED.nodes.filterNodes({ type: "bacnet-point-in" }) || [];
pointNodes = pointNodes.concat(RED.nodes.filterNodes({ type: "bacnet-point-in-out" }) || []);
}
// Update label cache for each node (this is what makes labels update)
window.__bnPointLabelCache = window.__bnPointLabelCache || {};
pointNodes.forEach(function(pointNode) {
if (pointNode && pointNode.id) {
// Force cache update by deleting it - label() will recalculate
delete window.__bnPointLabelCache[pointNode.id];
}
});
// Redraw immediately (like setLabelCacheFromFields does)
if (RED.view && typeof RED.view.redraw === "function") {
RED.view.redraw(true);
}
// Also call refresh to sync with backend
try {
scheduleRefreshAllPointLabels();
} catch (e) {
}
}
// Run immediately
runRefreshSequence();
// And retry once to account for backend startup delay
setTimeout(runRefreshSequence, 1500);
});
// Refresh labels when nodes are added, removed, or changed (not on a timer)
RED.events.on("nodes:add", function(addedNode) {
// Skip during initial flow import: nodes:add fires once per imported
// node and flows:loaded already triggers a single refresh afterwards.
if (!__bnFlowsLoaded) return;
if (addedNode && (addedNode.type === "bacnet-point-in" || addedNode.type === "bacnet-controller")) {
scheduleRefreshAllPointLabels();
}
});
RED.events.on("nodes:remove", function(removedNode) {
if (removedNode && (removedNode.type === "bacnet-point-in" || removedNode.type === "bacnet-controller")) {
scheduleRefreshAllPointLabels();
}
});
RED.events.on("nodes:change", function(changedNodes) {
// Refresh labels when controllers or points are modified
var shouldRefresh = false;
if (changedNodes && changedNodes.changed) {
for (var i = 0; i < changedNodes.changed.length; i++) {
var node = changedNodes.changed[i];
if (node && (node.type === "bacnet-point-in" || node.type === "bacnet-controller")) {
shouldRefresh = true;
break;
}
}
}
if (shouldRefresh) {
scheduleRefreshAllPointLabels();
}
});
// Removed automatic polling - labels now only refresh on events
// This prevents constant HTTP requests and 504 errors
window.__bnPointForceRefresh = scheduleRefreshAllPointLabels;
window.__bnPointBuildDeviceLabel = buildDeviceLabel;
window.__bnPointBuildObjectLabel = buildObjectLabel;
})();
</script>
<script type="text/javascript">
(function () {
if (window.__bnPointEditorV9) return;
window.__bnPointEditorV9 = true;
var buildDeviceLabel =
window.__bnPointBuildDeviceLabel ||
function (mode, name, instance) {
var cleanName = String(name || "").trim();
var cleanInstance = String(instance || "").trim();
if (mode === "device") return cleanName || cleanInstance;
if (mode === "both") {
if (cleanName && cleanInstance)
return cleanName + " (" + cleanInstance + ")";
return cleanName || cleanInstance;
}
return cleanInstance || cleanName;
};
var buildObjectLabel =
window.__bnPointBuildObjectLabel ||
function (mode, objectName, objectId) {
var cleanName = String(objectName || "").trim();
var cleanId = String(objectId || "").trim();
if (mode === "id") return cleanId || cleanName;
if (mode === "both") {
if (cleanName && cleanId) return cleanName + " (" + cleanId + ")";
return cleanName || cleanId;
}
return cleanName || cleanId;
};
function allowInitial() {
return true;
}
// Register bacnet-point as an alias/migration target for bacnet-point-in
// This ensures Node-RED recognizes old bacnet-point nodes in the editor
// The JavaScript migration handler will convert them to bacnet-point-in
RED.nodes.registerType("bacnet-point", {
color: "#f3b567",
defaults: {
name: { value: "", label: "Name" },
ocn_server_node: { type: "ocn-server", required: false },
source: {
value: "mqtt",
required: true,
label: "Source",
validate: allowInitial,
},
topic: {
value: "default",
required: true,
label: "Topic",
validate: allowInitial,
},
objectType: { value: "", validate: allowInitial },
objectInstance: { value: "", validate: allowInitial },
objectName: { value: "" },
description: { value: "" },
units: { value: "" },
autoLabel: { value: true },
controller_guid: { value: "" },
controllerName: { value: "" },
controllerDeviceInstance: { value: "" },
labelMode: { value: "instance" },
objectLabelMode: { value: "name" },
advancedMode: { value: false },
device_instance: { value: "" },
object_id: { value: "" },
priority: { value: "9" },
send_policy: { value: "On value changed" },
output_type: { value: "ip" },
bypass_priority: { value: false },
initialized: { value: false },
},
inputs: 1,
outputs: 2,
icon: "node-red/arrow-in.svg",
paletteLabel: function () {
return this.advancedMode ? "Output" : "BACnet Point";
},
label: function () {
// Use the same label logic as bacnet-point-in
if (this.name && String(this.name).trim()) return this.name;
if (this.advancedMode) {
return (
(
(this.device_instance || "Output") +
" " +
(this.object_id || "")
).trim() || "Output"
);
}
var labelCache =
(window.__bnPointLabelCache && window.__bnPointLabelCache[this.id]) ||
"";
if (labelCache) return labelCache;
var deviceLabel = buildDeviceLabel(
this.labelMode || "instance",
this.controllerName || "",
this.controllerDeviceInstance || ""
);
var hasObjectType = this.objectType != null && String(this.objectType) !== "";
var hasObjectInstance = this.objectInstance != null && String(this.objectInstance) !== "";
var objectId = hasObjectType && hasObjectInstance
? (function () {
var typeAbbr = "";
var typeMap = {
0: "AI", 1: "AO", 2: "AV", 3: "BI", 4: "BO", 5: "BV",
12: "MI", 13: "MO", 14: "MV", 19: "MSV"
};
if (typeof this.objectType === "number") {
typeAbbr = typeMap[this.objectType] || String(this.objectType);
} else {
var normalized = String(this.objectType || "").trim().toUpperCase();
if (/^\d+$/.test(normalized)) {
typeAbbr = typeMap[parseInt(normalized, 10)] || normalized;
} else {
typeAbbr = typeMap[normalized] || normalized;
}
}
return typeAbbr + "-" + String(this.objectInstance || "");
}.call(this))
: "";
var objectLabel = buildObjectLabel(
this.objectLabelMode || "name",
this.objectName || "",
objectId
);
var separator = " " + String.fromCharCode(183) + " ";
if (this.labelMode === "both") {
var dot = " " + String.fromCharCode(183) + " ";
var labelParts = [];
if (objectLabel) labelParts.push(objectLabel);
if (this.controllerName) labelParts.push(this.controllerName);
if (this.controllerDeviceInstance) labelParts.push(this.controllerDeviceInstance);
if (!labelParts.length) labelParts.push(objectId || this.objectName || "");
return labelParts.length
? labelParts.join(dot)
: objectLabel ||
deviceLabel ||
objectId ||
"BACnet Point";
} else {
if (objectLabel && deviceLabel) {
return objectLabel + separator + deviceLabel;
} else if (objectLabel) {
return objectLabel;
} else if (deviceLabel) {
return deviceLabel;
} else {
return objectId || this.objectName || "BACnet Point";
}
}
},
inputLabels: function () {
return "Trigger";
},
outputLabels: function (outputIndex) {
return outputIndex === 0 ? "Value (msg.payload)" : "Status flags";
},
});
RED.nodes.registerType("bacnet-point-in", {
color: "#f3b567",
defaults: {
name: { value: "", label: "Name" },
ocn_server_node: { type: "ocn-server", required: false },
source: {
value: "mqtt",
required: true,
label: "Source",
validate: allowInitial,
},
topic: {
value: "default",
required: true,
label: "Topic",
validate: allowInitial,
},
objectType: { value: "", validate: allowInitial },
objectInstance: { value: "", validate: allowInitial },
objectName: { value: "" },
description: { value: "" },
units: { value: "" },
autoLabel: { value: true },
controller_guid: { value: "" },
controllerName: { value: "" },
controllerDeviceInstance: { value: "" },
labelMode: { value: "instance" },
objectLabelMode: { value: "name" },
advancedMode: { value: false },
device_instance: { value: "" },
object_id: { value: "" },
priority: { value: "9" },
send_policy: { value: "On value changed" },
output_type: { value: "ip" },
bypass_priority: { value: false },
initialized: { value: false },
},
inputs: 1,
outputs: 2,
icon: "node-red/arrow-in.svg",
paletteLabel: function () {
return this.advancedMode ? "Output" : "BACnet Point";
},
label: function () {
// Log label calculation for debugging (only first few calls to avoid spam)
if (!window.__bnLabelCallCount) window.__bnLabelCallCount = 0;
window.__bnLabelCallCount++;
var shouldLog = window.__bnLabelCallCount <= 5 || (window.__bnLabelCallCount % 50 === 0);
if (this.name && String(this.name).trim()) {
return this.name;
}
if (this.advancedMode) {
var advLabel = (
(
(this.device_instance || "Output") +
" " +
(this.object_id || "")
).trim() || "Output"
);
return advLabel;
}
// TEST: Always bypass cache to verify labels can update
// var labelCache =
// (window.__bnPointLabelCache && window.__bnPointLabelCache[this.id]) ||
// "";
// if (labelCache) {
// if (shouldLog) console.log("[bacnet-point-in] label() returning cached label:", labelCache, "for node:", this.id);
// return labelCache;
// }
var typeAbbreviationMap = {
0: "AI",
1: "AO",
2: "AV",
3: "BI",
4: "BO",
5: "BV",
12: "PID",
19: "MSV",
};
function toAbbrLocal(typeValue) {
if (typeof typeValue === "number") {
return typeAbbreviationMap[typeValue] || String(typeValue);
}
var normalizedType = String(typeValue || "")
.trim()
.toUpperCase();
if (/^\d+$/.test(normalizedType))
return typeAbbreviationMap[+normalizedType] || normalizedType;
return typeAbbreviationMap[normalizedType] != null
? typeAbbreviationMap[normalizedType]
: normalizedType;
}
// TEST: Append deploy counter to objectName to verify labels can update
var deployCounter = window.__bnPointDeployCounter || 0;
var objectName = this.objectName || "";
var controllerName = (this.controllerName || "").trim();
var controllerDeviceInstance = (
this.controllerDeviceInstance || ""
).trim();
var objectType = this.objectType;
var objectInstance = this.objectInstance;
var hasObjectId =
objectType !== undefined &&
objectType !== null &&
String(objectType) !== "" &&
objectInstance !== undefined &&
objectInstance !== null &&
String(objectInstance) !== "";
var objectId = hasObjectId
? toAbbrLocal(objectType) + "-" + objectInstance
: "";
if (this.autoLabel || !this.name) {
var labelMode = this.labelMode || "instance";
var objectLabelMode = this.objectLabelMode || "name";
var objectLabel = buildObjectLabel(
objectLabelMode,
objectName,
objectId
);
var deviceLabel = buildDeviceLabel(
labelMode,
controllerName,
controllerDeviceInstance
);
if (labelMode === "both") {
var labelParts = [];
if (objectLabel) labelParts.push(objectLabel);
if (controllerName) labelParts.push(controllerName);
if (controllerDeviceInstance)
labelParts.push(controllerDeviceInstance);
if (!labelParts.length)
labelParts.push(objectId || objectName || "BACnet Point");
var middleDotSeparator = " " + String.fromCharCode(183) + " ";
return labelParts.length
? labelParts.join(middleDotSeparator)
: "BACnet Point";
}
var separator = " " + String.fromCharCode(183) + " ";
// TEST: Append deploy counter to verify labels are recalculated
var deployCounter = window.__bnPointDeployCounter || 0;
var counterSuffix = deployCounter > 0 ? " [" + deployCounter + "]" : "";
if (objectLabel && deviceLabel)
return objectLabel + counterSuffix + separator + deviceLabel;
if (objectLabel) return objectLabel + counterSuffix;
if (deviceLabel) return deviceLabel;
return (objectId || objectName || "BACnet Point") + counterSuffix;
}
// Always return a string to prevent "replace" errors
// TEST: Append deploy counter to verify labels are recalculated
var deployCounter = window.__bnPointDeployCounter || 0;
var counterSuffix = deployCounter > 0 ? " [" + deployCounter + "]" : "";
return String(this.name || objectId || objectName || "BACnet Point") + counterSuffix;
},
inputLabels: function () {
return "Trigger read";
},
outputLabels: function (outputIndex) {
return outputIndex === 0 ? "Value (msg.payload)" : "Status flags";
},
oneditcancel: function () {
function abortPending(requestHandle) {
if (requestHandle && typeof requestHandle.abort === "function") {
try {
requestHandle.abort();
} catch (_ignoredError) {}
}
}
abortPending(this.__pendingLabelForRequest);
abortPending(this.__pendingObjectsRequest);
abortPending(this.__pendingResolveRequest);
var editorNodeInstance = RED.nodes.node(this.id);
var originalSnapshot = this.__instOriginal || {};
if (editorNodeInstance) {
editorNodeInstance.name = originalSnapshot.name || "";
editorNodeInstance.autoLabel = !!originalSnapshot.autoLabel;
editorNodeInstance.objectType = originalSnapshot.objectType;
editorNodeInstance.objectInstance = originalSnapshot.objectInstance;
editorNodeInstance.objectName = originalSnapshot.objectName;
editorNodeInstance.description = originalSnapshot.description;
editorNodeInstance.units = originalSnapshot.units;
editorNodeInstance.controllerName =
originalSnapshot.controllerName || "";
editorNodeInstance.controllerDeviceInstance =
originalSnapshot.controllerDeviceInstance || "";
editorNodeInstance.labelMode =
originalSnapshot.labelMode || "instance";
editorNodeInstance.objectLabelMode =
originalSnapshot.objectLabelMode || "name";
editorNodeInstance.advancedMode = !!originalSnapshot.advancedMode;
editorNodeInstance.device_instance =
originalSnapshot.device_instance || "";
editorNodeInstance.object_id = originalSnapshot.object_id || "";
editorNodeInstance.priority = originalSnapshot.priority || "9";
editorNodeInstance.send_policy =
originalSnapshot.send_policy || "On value changed";
editorNodeInstance.output_type = originalSnapshot.output_type || "ip";
editorNodeInstance.bypass_priority =
!!originalSnapshot.bypass_priority;
editorNodeInstance.changed = !!originalSnapshot.changed;
RED.view.redraw(true);
}
},
oneditprepare: function () {
var nodeId = this.id;
var editorNodeInstance = RED.nodes.node(nodeId);
var editContext = this;
editContext.__pendingLabelForRequest = null;
editContext.__pendingObjectsRequest = null;
editContext.__pendingResolveRequest = null;
this.__instOriginal = editorNodeInstance
? {
name: editorNodeInstance.name,
autoLabel: editorNodeInstance.autoLabel,
objectType: editorNodeInstance.objectType,
objectInstance: editorNodeInstance.objectInstance,
objectName: editorNodeInstance.objectName,
description: editorNodeInstance.description,
units: editorNodeInstance.units,
controllerName: editorNodeInstance.controllerName,
controllerDeviceInstance:
editorNodeInstance.controllerDeviceInstance,
labelMode: editorNodeInstance.labelMode,
objectLabelMode:
editorNodeInstance.objectLabelMode || "name",
advancedMode: !!editorNodeInstance.advancedMode,
device_instance: editorNodeInstance.device_instance,
object_id: editorNodeInstance.object_id,
priority: editorNodeInstance.priority,
send_policy: editorNodeInstance.send_policy,
output_type: editorNodeInstance.output_type,
bypass_priority: !!editorNodeInstance.bypass_priority,
changed: !!editorNodeInstance.changed,
}
: null;
if (RED.tabs && $("#bp-tabs").length) {
var propertyTabs = RED.tabs.create({
id: "bp-tabs",
onchange: function (selectedTab) {
$("#bp-tabs-content").children().hide();
$("#" + selectedTab.id).show();
},
});
propertyTabs.addTab({
id: "bp-tab-props",
label: "Properties",
iconClass: "fa fa-sliders",
});
propertyTabs.addTab({
id: "bp-tab-options",
label: "Options",
iconClass: "fa fa-cog",
});
propertyTabs.activateTab("bp-tab-props");
}
var abbreviationToNumericMap = {
AI: 0,
AO: 1,
AV: 2,
BI: 3,
BO: 4,
BV: 5,
MI: 12,
MO: 13,
MV: 14,
MSV: 19,
};
function fillAdvancedFromSelection() {
var hiddenDeviceInstance = (
$("#node-input-controllerDeviceInstance").val() || ""
).trim();
var hiddenObjectType = (
$("#node-input-objectType").val() || ""
).trim();
var hiddenObjectInstance = (
$("#node-input-objectInstance").val() || ""
).trim();
var currentNodeInstance = RED.nodes.node(nodeId);
if (hiddenDeviceInstance) {
$("#node-input-device_instance").val(hiddenDeviceInstance);
if (currentNodeInstance) {
currentNodeInstance.device_instance = hiddenDeviceInstance;
}
}
if (hiddenObjectType && hiddenObjectInstance) {
var composedObjectId =
toAbbrLocal(hiddenObjectType) + "-" + hiddenObjectInstance;
$("#node-input-object_id").val(composedObjectId.toUpperCase());
if (currentNodeInstance) {
currentNodeInstance.object_id = composedObjectId.toUpperCase();
}
}
}
function parseObjectIdLocal(objectIdString) {
var trimmedId = String(objectIdString || "")
.trim()
.toUpperCase();
var match = trimmedId.match(/^([A-Z]+)\-([0-9]+)$/);
if (!match) return null;
var abbreviation = match[1];
var instanceString = match[2];
var mappedType =
abbreviationToNumericMap &&
Object.prototype.hasOwnProperty.call(
abbreviationToNumericMap,
abbreviation
)
? abbreviationToNumericMap[abbreviation]
: abbreviation;
return { type: mappedType, instance: Number(instanceString) };
}
function applyAdvancedToHidden(forceApply) {
if (!forceApply && !$("#node-input-advancedMode").is(":checked"))
return;
var deviceInstanceValue = (
$("#node-input-device_instance").val() || ""
).trim();
if (deviceInstanceValue) {
$("#node-input-controllerDeviceInstance")
.val(deviceInstanceValue)
.trigger("change");
var currentNodeInstance = RED.nodes.node(nodeId);
if (currentNodeInstance) {
currentNodeInstance.controllerDeviceInstance =
deviceInstanceValue;
currentNodeInstance.device_instance = deviceInstanceValue;
}
}
var objectIdValue = ($("#node-input-object_id").val() || "").trim();
var parsedId = parseObjectIdLocal(objectIdValue);
if (parsedId) {
$("#node-input-objectType").val(parsedId.type).trigger("change");
$("#node-input-objectInstance")
.val(parsedId.instance)
.trigger("change");
var currentNodeInstance2 = RED.nodes.node(nodeId);
if (currentNodeInstance2) {
currentNodeInstance2.objectType = parsedId.type;
currentNodeInstance2.objectInstance = parsedId.instance;
currentNodeInstance2.object_id = (
toAbbrLocal(parsedId.type) +
"-" +
parsedId.instance
).toUpperCase();
}
}
renderSelectedSummary();
highlightCurrent();
}
function initAdvancedControls() {
var $topicInput = $("#node-input-topic");
if (
$topicInput.length &&
typeof $topicInput.typedInput === "function" &&
!$topicInput.data("typedInput")
) {
try {
var topicOptions = typeof topics === "function" ? topics() : [];
$topicInput.typedInput({
types: [{ value: "", options: topicOptions }],
});
} catch (topicInitError) {}
}
fillAdvancedFromSelection();
if (
$("#node-input-advancedMode").is(":checked") &&
window.localStorage
) {
if (($("#node-input-device_instance").val() || "").trim() === "") {
$("#node-input-device_instance").val(
localStorage.getItem("default_device_instance") || ""
);
}
if (($("#node-input-object_id").val() || "").trim() === "") {
$("#node-input-object_id").val(
localStorage.getItem("default_in_obj_id") || ""
);
}
}
applyAdvancedToHidden(true);
}
function toggleModeUI() {
var isAdvancedMode = $("#node-input-advancedMode").is(":checked");
if (isAdvancedMode) {
$("#bp-props-modern").hide();
$("#bp-props-legacy").show();
initAdvancedControls();
} else {
$("#bp-props-modern").show();
$("#bp-props-legacy").hide();
}
}
$("#node-input-advancedMode").on("change", function () {
toggleModeUI();
});
toggleModeUI();
function updateOcnTrace() {
var ocnId = "";
var $ocnField = $("#node-input-ocn_server_node");
if ($ocnField.length) {
var rawOcnValue = $ocnField.val();
if (typeof rawOcnValue === "string") {
ocnId = rawOcnValue;
} else if (rawOcnValue && typeof rawOcnValue === "object") {
ocnId = rawOcnValue.id || rawOcnValue.value || "";
}
}
if (!ocnId && editorNodeInstance) {
var storedOcnValue = editorNodeInstance.ocn_server_node;
if (storedOcnValue) {
if (typeof storedOcnValue === "string") {
ocnId = storedOcnValue;
} else if (typeof storedOcnValue === "object") {
ocnId = storedOcnValue.id || storedOcnValue.value || "";
}
}
}
$("#bp-current-ocn-id").text(ocnId ? ocnId : "None selected");
}
$("#node-input-ocn_server_node").on("change", function() {
var currentNodeInstance = RED.nodes.node(nodeId);
if (currentNodeInstance) {
var previousOcnServerNode = currentNodeInstance.ocn_server_node;
var newOcnServerNode = $(this).val() || "";
if (previousOcnServerNode !== newOcnServerNode) {
currentNodeInstance.ocn_server_node = newOcnServerNode;
markDeployNeeded(currentNodeInstance);
}
}
updateOcnTrace();
});
updateOcnTrace();
$("#node-input-device_instance").on("input change", function () {
applyAdvancedToHidden();
});
$("#node-input-labelMode").on("change", setLabelCacheFromFields);
$("#node-input-objectLabelMode").on("change", setLabelCacheFromFields);
$("#node-input-objectName").on("input change", setLabelCacheFromFields);
$("#node-input-objectType").on("change", setLabelCacheFromFields);
$("#node-input-objectInstance").on("change", setLabelCacheFromFields);
$("#node-input-object_id").on("input change", function () {
applyAdvancedToHidden();
});
var lastLoadedObjects = [];
var isLoadingObjects = false;
(function syncSourceSelectors() {
var $legacySourceSelect = $("#node-input-source");
var $simpleSourceSelect = $("#node-input-source-simple");
if (!$legacySourceSelect.length || !$simpleSourceSelect.length)
return;
var suppressChangeSync = false;
var initialSource = $legacySourceSelect.val() || "mqtt";
$simpleSourceSelect.val(initialSource);
$simpleSourceSelect.on("change", function () {
if (suppressChangeSync) return;
suppressChangeSync = true;
$legacySourceSelect.val($(this).val()).trigger("change");
suppressChangeSync = false;
});
$legacySourceSelect.on("change", function () {
if (suppressChangeSync) return;
suppressChangeSync = true;
$simpleSourceSelect.val($(this).val());
suppressChangeSync = false;
});
})();
function toAbbrLocal(typeValue) {
var typeAbbreviationMap = {
0: "AI",
1: "AO",
2: "AV",
3: "BI",
4: "BO",
5: "BV",
12: "MI",
13: "MO",
14: "MV",
19: "MSV",
};
if (typeof typeValue === "number") {
return typeAbbreviationMap[typeValue] != null
? typeAbbreviationMap[typeValue]
: String(typeValue);
}
var normalizedType = String(typeValue || "")
.trim()
.toUpperCase();
if (/^\d+$/.test(normalizedType)) {
var numericType = Number(normalizedType);
return typeAbbreviationMap[numericType] != null
? typeAbbreviationMap[numericType]
: normalizedType;
}
return typeAbbreviationMap[normalizedType] != null
? typeAbbreviationMap[normalizedType]
: normalizedType;
}
function renderSelectedSummary() {
var controllerDeviceInstance = (
$("#node-input-controllerDeviceInstance").val() || ""
).trim();
var controllerName = (
$("#node-input-controllerName").val() || ""
).trim();
var objectType = ($("#node-input-objectType").val() || "").trim();
var objectInstance = (
$("#node-input-objectInstance").val() || ""
).trim();
var objectName = ($("#node-input-objectName").val() || "").trim();
var objectId =
objectType && objectInstance
? toAbbrLocal(objectType) + "-" + objectInstance
: "";
var summaryParts = [];
var middleDotSeparator = " " + String.fromCharCode(183) + " ";
if (controllerDeviceInstance)
summaryParts.push(controllerDeviceInstance);
if (controllerName) summaryParts.push(controllerName);
if (objectId) summaryParts.push(objectId);
if (objectName) summaryParts.push(objectName);
var summaryText = summaryParts.join(middleDotSeparator);
$("#nr-point-selected").text(summaryText || "None selected");
}
function setLabelCacheFromFields() {
var controllerDeviceInstance = (
$("#node-input-controllerDeviceInstance").val() || ""
).trim();
var controllerName = (
$("#node-input-controllerName").val() || ""
).trim();
var objectName = ($("#node-input-objectName").val() || "").trim();
var objectTypeValue = $("#node-input-objectType").val() || "";
var objectInstanceValue = $("#node-input-objectInstance").val() || "";
var objectId =
objectTypeValue && objectInstanceValue
? toAbbrLocal(objectTypeValue) + "-" + objectInstanceValue
: "";
var labelMode = $("#node-input-labelMode").val() || "instance";
var objectLabelMode =
$("#node-input-objectLabelMode").val() || "name";
var deviceLabel = buildDeviceLabel(
labelMode,
controllerName,
controllerDeviceInstance
);
var objectLabel = buildObjectLabel(
objectLabelMode,
objectName,
objectId
);
var computedLabel = "";
if (labelMode === "both") {
var labelParts = [];
if (objectLabel) labelParts.push(objectLabel);
if (controllerName) labelParts.push(controllerName);
if (controllerDeviceInstance)
labelParts.push(controllerDeviceInstance);
if (!labelParts.length)
labelParts.push(objectId || objectName || "");
var middleDotSeparator = " " + String.fromCharCode(183) + " ";
computedLabel = labelParts.join(middleDotSeparator);
} else {
var separator = " " + String.fromCharCode(183) + " ";
if (objectLabel && deviceLabel)
computedLabel = objectLabel + separator + deviceLabel;
else if (objectLabel) computedLabel = objectLabel;
else if (deviceLabel) computedLabel = deviceLabel;
else computedLabel = objectId || objectName || "";
}
window.__bnPointLabelCache = window.__bnPointLabelCache || {};
window.__bnPointLabelCache[nodeId] = computedLabel || "";
RED.view.redraw(true);
renderSelectedSummary();
}
function highlightCurrent() {
var objectTypeValue = $("#node-input-objectType").val();
var objectInstanceValue = $("#node-input-objectInstance").val();
$(".nr-point-row").each(function () {
var $rowElement = $(this);
if (
$rowElement.data("ot") == String(objectTypeValue) &&
$rowElement.data("oi") == String(objectInstanceValue)
) {
$rowElement.css("outline", "2px solid #4caf50");
} else {
$rowElement.css("outline", "none");
}
});
}
function markDeployNeeded(nodeInstanceToMark) {
var nodeInstance = nodeInstanceToMark || RED.nodes.node(nodeId);
if (!nodeInstance) return;
nodeInstance.changed = true;
try {
RED.nodes.dirty(true);
} catch (dirtyError) {}
RED.view.redraw(true);
}
function renderFiltered(objectItems, queryText) {
var $listContainer = $("#nr-point-list");
$listContainer.empty();
var objectArray = Array.isArray(objectItems) ? objectItems : [];
var loweredQuery = (queryText || "").toLowerCase();
if (loweredQuery) {
objectArray = objectArray.filter(function (objectDescriptor) {
var typeAbbreviation = toAbbrLocal(objectDescriptor.objectType);
var composedAbbreviation =
typeAbbreviation +
"-" +
String(objectDescriptor.objectInstance);
var composedNumeric =
String(objectDescriptor.objectType) +
"-" +
String(objectDescriptor.objectInstance);
var searchBlob = [
objectDescriptor.objectName || "",
objectDescriptor.units || "",
String(objectDescriptor.objectInstance || ""),
String(objectDescriptor.objectType || ""),
String(objectDescriptor.objectTypeName || ""),
composedAbbreviation,
composedNumeric,
objectDescriptor.controllerName || "",
objectDescriptor.controllerDeviceInstance || "",
]
.join(" ")
.toLowerCase();
return searchBlob.includes(loweredQuery);
});
}
if (!objectArray.length) {
$listContainer.append(
'<div style="padding:8px;color:#666">No objects</div>'
);
return;
}
objectArray.forEach(function (objectDescriptor) {
var typeAbbreviation = toAbbrLocal(objectDescriptor.objectType);
var composedObjectId =
typeAbbreviation + "-" + String(objectDescriptor.objectInstance);
var $rowElement = $(
'<div class="nr-point-row" tabindex="0" ' +
'style="display:flex;align-items:center;justify-content:space-between;' +
"padding:8px 10px;border:1px solid #ddd;border-radius:8px;" +
'margin-bottom:8px;cursor:pointer"></div>'
);
$rowElement
.attr("data-ot", String(objectDescriptor.objectType))
.attr("data-oi", String(objectDescriptor.objectInstance));
$rowElement
.data("ot", String(objectDescriptor.objectType))
.data("oi", String(objectDescriptor.objectInstance));
var $leftContainer = $('<div style="min-width:0"></div>');
var titleText =
(objectDescriptor.objectName || "") +
(objectDescriptor.controllerName
? " " +
String.fromCharCode(183) +
" " +
objectDescriptor.controllerName
: "");
var $titleElement = $(
'<div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></div>'
).text(titleText);
var presentValuePart =
objectDescriptor.presentValue != null &&
objectDescriptor.presentValue !== ""
? " " +
String.fromCharCode(183) +
" PV: " +
objectDescriptor.presentValue +
(objectDescriptor.units ? " " + objectDescriptor.units : "")
: "";
var $subtitleElement = $(
'<div style="font-size:12px;color:#666"></div>'
).text(composedObjectId + presentValuePart);
$leftContainer.append($titleElement).append($subtitleElement);
$rowElement.append($leftContainer);
$rowElement.on("click keypress", function (event) {
if (event.type === "keypress" && event.key !== "Enter") return;
$("#node-input-objectType")
.val(objectDescriptor.objectType)
.trigger("change");
$("#node-input-objectInstance")
.val(objectDescriptor.objectInstance)
.trigger("change");
$("#node-input-objectName")
.val(objectDescriptor.objectName || "")
.trigger("change");
$("#node-input-description")
.val(objectDescriptor.description || "")
.trigger("change");
$("#node-input-units")
.val(objectDescriptor.units || "")
.trigger("change");
var currentNodeInstance = RED.nodes.node(nodeId);
if (currentNodeInstance) {
var beforeKey =
currentNodeInstance.objectType +
"|" +
currentNodeInstance.objectInstance;
currentNodeInstance.objectType = objectDescriptor.objectType;
currentNodeInstance.objectInstance =
objectDescriptor.objectInstance;
currentNodeInstance.objectName =
objectDescriptor.objectName || "";
currentNodeInstance.description =
objectDescriptor.description || "";
currentNodeInstance.units = objectDescriptor.units || "";
currentNodeInstance.autoLabel = true;
currentNodeInstance.name = "";
$("#node-input-controllerDeviceInstance")
.val(objectDescriptor.controllerDeviceInstance || "")
.trigger("change");
currentNodeInstance.controllerDeviceInstance =
objectDescriptor.controllerDeviceInstance || "";
currentNodeInstance.controllerName =
objectDescriptor.controllerName || "";
var controllerNameCurrent = (
$("#node-input-controllerName").val() || ""
).trim();
var controllerDeviceInstanceCurrent = (
$("#node-input-controllerDeviceInstance").val() || ""
).trim();
var labelMode = $("#node-input-labelMode").val() || "instance";
var objectLabelMode =
$("#node-input-objectLabelMode").val() || "name";
var deviceLabel = buildDeviceLabel(
labelMode,
controllerNameCurrent,
controllerDeviceInstanceCurrent
);
var objectIdLocal =
toAbbrLocal(objectDescriptor.objectType) +
"-" +
String(objectDescriptor.objectInstance);
var objectLabel = buildObjectLabel(
objectLabelMode,
currentNodeInstance.objectName,
objectIdLocal
);
var computedLabel;
if (labelMode === "both") {
var labelParts = [];
if (objectLabel) labelParts.push(objectLabel);
if (controllerNameCurrent)
labelParts.push(controllerNameCurrent);
if (controllerDeviceInstanceCurrent)
labelParts.push(controllerDeviceInstanceCurrent);
if (!labelParts.length)
labelParts.push(
objectIdLocal || currentNodeInstance.objectName || ""
);
var middleDotSeparator = " " + String.fromCharCode(183) + " ";
computedLabel = labelParts.join(middleDotSeparator);
} else {
var separator = " " + String.fromCharCode(183) + " ";
if (objectLabel && deviceLabel) {
computedLabel = objectLabel + separator + deviceLabel;
} else if (objectLabel) {
computedLabel = objectLabel;
} else if (deviceLabel) {
computedLabel = deviceLabel;
} else {
computedLabel =
objectIdLocal || currentNodeInstance.objectName || "";
}
}
window.__bnPointLabelCache = window.__bnPointLabelCache || {};
window.__bnPointLabelCache[nodeId] = computedLabel || "";
var afterKey =
currentNodeInstance.objectType +
"|" +
currentNodeInstance.objectInstance;
if (beforeKey !== afterKey) {
markDeployNeeded(currentNodeInstance);
} else {
RED.view.redraw(true);
}
}
fillAdvancedFromSelection();
renderSelectedSummary();
highlightCurrent();
setTimeout(function () {
// Use bacnetHub service directly instead of making API calls
var bacnetHubService = window.__bacnetHubService;
var currentNode = RED.nodes.node ? RED.nodes.node(nodeId) : null;
if (bacnetHubService && currentNode) {
var controllerGuid = String(currentNode.controller_guid || "");
var controllerSnapshot = controllerGuid
? bacnetHubService.lookupControllerByGuid(controllerGuid)
: null;
if (controllerSnapshot && controllerSnapshot.controller) {
var ctrl = controllerSnapshot.controller;
var controllerName = ctrl.deviceName || ctrl.modelName || "";
var controllerDeviceInstance = ctrl.deviceInstance != null
? String(ctrl.deviceInstance)
: "";
// Update form fields with latest controller info from snapshot
if (controllerName) {
$("#node-input-controllerName")
.val(controllerName)
.trigger("input");
}
if (controllerDeviceInstance) {
$("#node-input-controllerDeviceInstance")
.val(controllerDeviceInstance)
.trigger("input");
// Also sync device_instance to match controllerDeviceInstance
$("#node-input-device_instance")
.val(controllerDeviceInstance)
.trigger("input");
}
// Get object info from snapshot if available
if (objectDescriptor.objectType != null && objectDescriptor.objectInstance != null && ctrl.objByKey) {
var numericType = Number(objectDescriptor.objectType);
var lookupKey = String(numericType) + "|" + String(objectDescriptor.objectInstance);
var objectEntry = ctrl.objByKey.get(lookupKey);
if (objectEntry && objectEntry.objectName) {
$("#node-input-objectName")
.val(objectEntry.objectName)
.trigger("input");
}
}
setLabelCacheFromFields();
}
}
}, 10);
});
$listContainer.append($rowElement);
});
highlightCurrent();
}
function selectionExistsIn(objectArray) {
var objectTypeValue = $("#node-input-objectType").val() || "";
var objectInstanceValue = $("#node-input-objectInstance").val() || "";
if (!objectTypeValue || !objectInstanceValue) return true;
var mapAbbreviationToNumber = {
AI: 0,
AO: 1,
AV: 2,
BI: 3,
BO: 4,
BV: 5,
PID: 12,
MSV: 19,
};
function convertTypeToNumber(typeValue) {
if (typeof typeValue === "number") return typeValue;
var normalized = String(typeValue)
.trim()
.toUpperCase();
if (/^\d+$/.test(normalized)) return +normalized;
return mapAbbreviationToNumber[normalized] != null
? mapAbbreviationToNumber[normalized]
: normalized;
}
var numericObjectType = convertTypeToNumber(objectTypeValue);
for (var index = 0; index < objectArray.length; index++) {
var descriptor = objectArray[index];
if (
String(descriptor.objectType) === String(numericObjectType) &&
String(descriptor.objectInstance) === String(objectInstanceValue)
) {
return true;
}
}
return false;
}
function clearSelectionWithNotice() {
$("#node-input-objectType").val("");
$("#node-input-objectInstance").val("");
$("#node-input-objectName").val("");
$("#node-input-description").val("");
$("#node-input-units").val("");
$("#nr-point-selected").text(
"This point no longer exists on the linked controller"
);
}
function setLoading(isLoading) {
isLoadingObjects = !!isLoading;
$("#nr-point-loading").css(
"display",
isLoadingObjects ? "flex" : "none"
);
}
function primeControllerMeta(nextCallback) {
// Use bacnetHub service directly instead of making API calls
var bacnetHubService = window.__bacnetHubService;
var currentNode = RED.nodes.node ? RED.nodes.node(nodeId) : null;
if (bacnetHubService && currentNode) {
var controllerGuid = String(currentNode.controller_guid || "");
var controllerSnapshot = controllerGuid
? bacnetHubService.lookupControllerByGuid(controllerGuid)
: null;
if (controllerSnapshot && controllerSnapshot.controller) {
var ctrl = controllerSnapshot.controller;
var controllerName = ctrl.deviceName || ctrl.modelName || "";
var controllerDeviceInstance = ctrl.deviceInstance != null
? String(ctrl.deviceInstance)
: "";
if (controllerName) {
$("#node-input-controllerName").val(controllerName);
}
if (controllerDeviceInstance) {
$("#node-input-controllerDeviceInstance").val(controllerDeviceInstance);
// Also sync device_instance to match controllerDeviceInstance
$("#node-input-device_instance").val(controllerDeviceInstance);
}
}
}
renderSelectedSummary();
updateOcnTrace();
nextCallback();
}
function loadFromApi() {
if (isLoadingObjects) return;
setLoading(true);
var requestedDeviceInstance = (
$("#node-input-controllerDeviceInstance").val() || ""
).trim();
if (
editContext.__pendingObjectsRequest &&
typeof editContext.__pendingObjectsRequest.abort === "function"
) {
try {
editContext.__pendingObjectsRequest.abort();
} catch (_ignoredError) {}
}
editContext.__pendingObjectsRequest = $.ajax({
url: "bacnet-point-in/objects",
method: "GET",
cache: false,
data: { nodeId: nodeId, deviceInstance: requestedDeviceInstance },
dataType: "text",
timeout: 10000,
})
.done(function (_unusedUrl, _unusedStatus, xhrResponse) {
var rawText =
xhrResponse && typeof xhrResponse.responseText === "string"
? xhrResponse.responseText
: "[]";
try {
rawText = rawText.replace(/^\)\]\}',?\s*/, "");
} catch (stripError) {}
var parsedArray = [];
try {
parsedArray = JSON.parse(rawText);
} catch (parseError) {}
lastLoadedObjects = Array.isArray(parsedArray) ? parsedArray : [];
renderFiltered(lastLoadedObjects, $("#nr-point-search").val());
if (
lastLoadedObjects.length > 0 &&
!selectionExistsIn(lastLoadedObjects)
) {
clearSelectionWithNotice();
}
})
.fail(function (xhrError) {
function parseErrorMessage(x) {
try {
if (x && typeof x.responseText === "string") {
var text = x.responseText.replace(/^\)\]\}',?\s*/, "");
var json = JSON.parse(text);
if (json && json.error) return String(json.error);
}
} catch (parseError) {}
return "HTTP " + (x && x.status ? x.status : "");
}
$("#nr-point-tip").text(parseErrorMessage(xhrError)).show();
$("#nr-point-list").empty().html("No controller info received.");
})
.always(function () {
setLoading(false);
editContext.__pendingObjectsRequest = null;
});
}
function debounce(fn, delayMs) {
var timeoutId;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeoutId);
timeoutId = setTimeout(function () {
fn.apply(context, args);
}, delayMs);
};
}
$("#nr-point-search").on(
"input",
debounce(function () {
renderFiltered(lastLoadedObjects, this.value);
}, 120)
);
$("#node-input-name").on("input", function () {
var currentNodeInstance = RED.nodes.node(nodeId);
if (currentNodeInstance) {
currentNodeInstance.name = this.value;
currentNodeInstance.autoLabel = this.value.trim() === "";
RED.view.redraw(true);
}
});
(function hydrateFromInstance() {
if (!editorNodeInstance) return;
$("#node-input-controllerName").val(
editorNodeInstance.controllerName || ""
);
$("#node-input-controllerDeviceInstance").val(
editorNodeInstance.controllerDeviceInstance || ""
);
$("#node-input-objectType").val(
editorNodeInstance.objectType != null
? editorNodeInstance.objectType
: ""
);
$("#node-input-objectInstance").val(
editorNodeInstance.objectInstance != null
? editorNodeInstance.objectInstance
: ""
);
$("#node-input-objectName").val(editorNodeInstance.objectName || "");
$("#node-input-description").val(
editorNodeInstance.description || ""
);
$("#node-input-units").val(editorNodeInstance.units || "");
$("#node-input-labelMode").val(
editorNodeInstance.labelMode || "instance"
);
$("#node-input-objectLabelMode").val(
editorNodeInstance.objectLabelMode || "name"
);
$("#node-input-advancedMode").prop(
"checked",
!!editorNodeInstance.advancedMode
);
$("#node-input-device_instance").val(
editorNodeInstance.device_instance || ""
);
$("#node-input-object_id").val(editorNodeInstance.object_id || "");
$("#node-input-priority").val(editorNodeInstance.priority || "9");
$("#node-input-send_policy").val(
editorNodeInstance.send_policy || "On value changed"
);
$("#node-input-output_type").val(
editorNodeInstance.output_type || "ip"
);
$("#node-input-bypass_priority").val(
editorNodeInstance.bypass_priority ? "true" : ""
);
renderSelectedSummary();
toggleModeUI();
updateOcnTrace();
initAdvancedControls();
})();
primeControllerMeta(function () {
setTimeout(loadFromApi, 50);
setTimeout(function () {
if (
editorNodeInstance &&
editorNodeInstance.objectType != null &&
editorNodeInstance.objectInstance != null
) {
// Use bacnetHub service directly instead of making API calls
var bacnetHubService = window.__bacnetHubService;
var currentNode = RED.nodes.node ? RED.nodes.node(nodeId) : null;
if (bacnetHubService && currentNode) {
var controllerGuid = String(currentNode.controller_guid || "");
var controllerSnapshot = controllerGuid
? bacnetHubService.lookupControllerByGuid(controllerGuid)
: null;
if (controllerSnapshot && controllerSnapshot.controller) {
var ctrl = controllerSnapshot.controller;
var controllerName = ctrl.deviceName || ctrl.modelName || "";
var controllerDeviceInstance = ctrl.deviceInstance != null
? String(ctrl.deviceInstance)
: "";
// Update form fields with latest controller info from snapshot
if (controllerName) {
$("#node-input-controllerName").val(controllerName);
}
if (controllerDeviceInstance) {
$("#node-input-controllerDeviceInstance").val(controllerDeviceInstance);
// Also sync device_instance to match controllerDeviceInstance
$("#node-input-device_instance").val(controllerDeviceInstance);
}
// Get object info from snapshot if available
if (editorNodeInstance.objectType != null && editorNodeInstance.objectInstance != null && ctrl.objByKey) {
var numericType = Number(editorNodeInstance.objectType);
var lookupKey = String(numericType) + "|" + String(editorNodeInstance.objectInstance);
var objectEntry = ctrl.objByKey.get(lookupKey);
if (objectEntry && objectEntry.objectName) {
$("#node-input-objectName").val(objectEntry.objectName);
}
}
renderSelectedSummary();
}
}
}
}, 120);
});
},
oneditsave: function () {
try {
// Sync device_instance with controllerDeviceInstance before save
// This ensures they stay in sync when controller changes
var controllerDeviceInstanceValue = (
$("#node-input-controllerDeviceInstance").val() || ""
).trim();
if (controllerDeviceInstanceValue) {
var currentDeviceInstance = (
$("#node-input-device_instance").val() || ""
).trim();
// If controllerDeviceInstance is set and differs from device_instance, sync them
if (currentDeviceInstance !== controllerDeviceInstanceValue) {
$("#node-input-device_instance").val(controllerDeviceInstanceValue);
}
}
if (
$("#node-input-advancedMode").is(":checked") &&
window.localStorage
) {
var deviceInstanceValue = (
$("#node-input-device_instance").val() || ""
).trim();
var objectIdValue = ($("#node-input-object_id").val() || "").trim();
if (deviceInstanceValue) {
localStorage.setItem(
"default_device_instance",
deviceInstanceValue
);
}
if (objectIdValue) {
localStorage.setItem("default_in_obj_id", objectIdValue);
}
}
} catch (storageError) {}
},
});
})();
if (RED && RED.palette && RED.palette.hide) {
RED.palette.hide('bacnet-point');
RED.palette.hide('bacnet-point-in');
}
</script>
<script type="text/html" data-template-name="bacnet-point-in">
<div class="form-row"><ul id="bp-tabs" class="red-ui-tabs"></ul></div>
<div id="bp-tabs-content">
<div id="bp-tab-props" style="display:none">
<div id="bp-props-modern">
<div class="form-row">
<label for="node-input-name"
><i class="fa fa-bookmark"></i> Name</label
>
<input type="text" id="node-input-name" placeholder="Optional name" />
</div>
<div class="form-row" style="margin-top:10px;">
<span
id="nr-point-selected"
style="display:inline-block;padding:6px 8px;background:#f5f5f5;border-radius:6px"
>None selected</span
>
</div>
<div class="form-row">
<div
style="display:flex;align-items:center;justify-content:space-between;width:100%;height:32px;"
>
<label style="margin:0;line-height:32px;">Objects</label>
</div>
<div
id="nr-point-tip"
style="display:none;margin:6px 0 0 0;padding:6px 8px;border:1px solid #ffd37a;background:#fff6df;color:#7a5b00;border-radius:6px;font-size:12px;"
></div>
<input
type="text"
id="nr-point-search"
placeholder="Search name/type/instance"
style="width:100%;margin:6px 0 8px 0;padding:6px 8px;border:1px solid #ddd;border-radius:6px"
/>
<div id="nr-point-list-wrap" style="position:relative;">
<div
id="nr-point-list"
style="height:240px;overflow:auto;border:1px solid #ddd;border-radius:8px;padding:6px"
></div>
<div
id="nr-point-loading"
style="display:none;position:absolute;left:0;top:0;right:0;bottom:0;background:rgba(255,255,255,0.7);align-items:center;justify-content:center;border:1px solid #ddd;border-radius:8px;"
>
<i
class="fa fa-spinner fa-spin"
style="font-size:18px;margin-right:8px;"
></i>
Loading...
</div>
</div>
</div>
<input type="hidden" id="node-input-objectType" />
<input type="hidden" id="node-input-objectInstance" />
<input type="hidden" id="node-input-objectName" />
<input type="hidden" id="node-input-description" />
<input type="hidden" id="node-input-units" />
<input type="hidden" id="node-input-controllerName" />
<input type="hidden" id="node-input-controllerDeviceInstance" />
<div class="form-row" style="margin-top:20px;">
<label for="node-input-source-simple"
><i class="fa fa-database"></i> Data Source</label
>
<select id="node-input-source-simple" style="width:70%;">
<option value="mqtt">MQTT (Recommended)</option>
<option value="api">API (HTTPS)</option>
</select>
</div>
</div>
<div id="bp-props-legacy" style="display:none">
<div class="form-tips" style="margin-bottom:20px;">
<b>Connection Settings</b>
</div>
<div class="form-row">
<label for="node-input-ocn_server_node"
><i class="fa fa-server"></i> OCN Server</label
>
<input type="text" id="node-input-ocn_server_node" />
</div>
<div class="form-tips" style="margin:20px 0;">
<b>Device Settings</b>
</div>
<div style="padding-left:10px;border-left:3px solid #ddd;">
<div class="form-row">
<label for="node-input-device_instance"
><i class="fa fa-microchip"></i> Device Instance</label
>
<input
type="text"
id="node-input-device_instance"
placeholder="e.g., 1234"
/>
</div>
<div class="form-row">
<label for="node-input-object_id"
><i class="fa fa-cube"></i> Object ID</label
>
<input
type="text"
id="node-input-object_id"
placeholder="e.g., AV-1"
/>
</div>
</div>
<div class="form-tips" style="margin:20px 0;">
<b>Communication Settings</b>
</div>
<div style="padding-left:10px;border-left:3px solid #ddd;">
<div class="form-row">
<label for="node-input-source"
><i class="fa fa-database"></i> Data Source</label
>
<select id="node-input-source" style="width:70%;">
<option value="mqtt">MQTT (Recommended)</option>
<option value="api">API (HTTPS)</option>
</select>
</div>
<div class="form-row">
<label for="node-input-topic"
><i class="fa fa-tags"></i> Topic</label
>
<input type="text" id="node-input-topic" />
</div>
</div>
<div class="form-tips" style="margin-top:20px;">
<p>
<i class="fa fa-info-circle"></i> Tip: Device instance and object ID
settings are saved as defaults for future nodes.
</p>
</div>
<input type="hidden" id="node-input-priority" />
<input type="hidden" id="node-input-send_policy" />
<input type="hidden" id="node-input-output_type" />
<input type="hidden" id="node-input-bypass_priority" />
</div>
</div>
<div id="bp-tab-options" style="display:none">
<div class="form-row">
<label for="node-input-labelMode"
><i class="fa fa-tag"></i> Device Label</label
>
<select id="node-input-labelMode" style="width:70%">
<option value="instance">Use Device Instance</option>
<option value="device">Use Device Name</option>
<option value="both">Use Device Name + Instance</option>
</select>
</div>
<div class="form-row">
<label for="node-input-objectLabelMode"
><i class="fa fa-tag"></i> Object Label</label
>
<select id="node-input-objectLabelMode" style="width:70%">
<option value="name">Use Object Name</option>
<option value="id">Use Object ID</option>
<option value="both">Use Object Name + ID</option>
</select>
</div>
<div class="form-row">
<label><i class="fa fa-server"></i> OCN ID</label>
<span
id="bp-current-ocn-id"
style="display:inline-block;width:70%;padding:6px 0;color:#555;word-break:break-all;"
>None selected</span
>
</div>
<div class="form-row">
<label for="node-input-advancedMode"
><i class="fa fa-cogs"></i> Advanced mode</label
>
<input
type="checkbox"
id="node-input-advancedMode"
style="width:auto;margin-top:0"
/>
</div>
</div>
</div>
</script>
<script type="text/html" data-help-name="bacnet-point-in">
<p>BACnet point node.</p>
</script>