@strato-automation/node-red-contrib-strato-automation
Version:
Strato Automation Official Node Red Library
1,874 lines • 71.7 kB
HTML
<!-- point-out.html -->
<script type="text/javascript">
var buildDeviceDisplayLabel =
window.__bnPointBuildDeviceLabel ||
function (labelMode, deviceName, deviceInstance) {
var cleanDeviceName = String(deviceName || "").trim();
var cleanDeviceInstance = String(deviceInstance || "").trim();
if (labelMode === "device") return cleanDeviceName || cleanDeviceInstance;
if (labelMode === "both") {
var middleDot = " " + String.fromCharCode(183) + " ";
if (cleanDeviceName && cleanDeviceInstance) {
return cleanDeviceName + middleDot + cleanDeviceInstance;
}
return cleanDeviceName || cleanDeviceInstance;
}
return cleanDeviceInstance || cleanDeviceName;
};
var buildObjectDisplayLabel =
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;
};
window.__bnPointBuildObjectLabel = buildObjectDisplayLabel;
RED.nodes.registerType("bacnet-point-out", {
color: "#f3b567",
defaults: {
name: { value: "", label: "Name" },
ocn_server_node: { type: "ocn-server", required: false },
mode: { value: "list", required: true, label: "Mode" },
advancedMode: { value: false },
allow_msg_override: { value: false },
acknowledged_mqtt: { value: true },
output_type: { value: "ip", required: true, label: "Output Type" },
topic: { value: "default", required: true, label: "Topic" },
objectType: { value: "" },
objectInstance: { value: "" },
object_id: { value: "" },
objectName: { value: "" },
description: { value: "" },
units: { value: "" },
deviceInstance: { value: "" },
deviceName: { value: "" },
networkNumber: { value: "" },
priority: { value: "9" },
bypass_priority: { value: false },
send_policy: { value: "Always" },
autoLabel: { value: true },
labelMode: { value: "instance" },
objectLabelMode: { value: "name" },
controller_guid: { value: "" },
},
inputs: 1,
outputs: 2,
align: "right",
inputLabels: function () {
return "Value to write (msg.payload)";
},
icon: "font-awesome/fa-arrow-circle-right",
paletteLabel: function () {
return "BACnet Point Out";
},
label: function () {
if (this.name && String(this.name).trim()) return this.name;
if (this.previewLabel) {
return this.previewLabel;
}
var cache =
(window.__bnPointLabelCache && window.__bnPointLabelCache[this.id]) ||
"";
if (cache) {
return cache;
}
function convertTypeToAbbrLocal(typeValue) {
var TYPE_MAP = {
0: "AI",
1: "AO",
2: "AV",
3: "BI",
4: "BO",
5: "BV",
12: "MI",
13: "MO",
14: "MV",
19: "MSV",
PID: "MI",
};
if (typeof typeValue === "number") {
return TYPE_MAP[typeValue] || String(typeValue);
}
var normalized = String(typeValue || "")
.trim()
.toUpperCase();
if (/^\d+$/.test(normalized)) {
var numeric = Number(normalized);
return TYPE_MAP[numeric] || normalized;
}
return TYPE_MAP[normalized] != null ? TYPE_MAP[normalized] : normalized;
}
var objectName = (this.objectName || "").trim();
var labelMode = this.labelMode || "instance";
var objectLabelMode = this.objectLabelMode || "name";
var deviceLabel = buildDeviceDisplayLabel(
labelMode,
this.deviceName,
this.deviceInstance
);
var hasObjectType =
this.objectType != null && String(this.objectType) !== "";
var hasObjectInstance =
this.objectInstance != null && String(this.objectInstance) !== "";
var objectId =
hasObjectType && hasObjectInstance
? convertTypeToAbbrLocal(this.objectType) + "-" + this.objectInstance
: "";
var objectLabel = buildObjectDisplayLabel(
objectLabelMode,
objectName,
objectId
);
if (this.autoLabel || !this.name) {
var finalLabel = "BACnet Point Out";
if (labelMode === "both") {
var parts = [];
if (objectLabel) parts.push(objectLabel);
if (this.deviceName) parts.push(this.deviceName);
if (this.deviceInstance) parts.push(this.deviceInstance);
if (!parts.length) parts.push(objectId || objectName || "");
var middleDot = " " + String.fromCharCode(183) + " ";
finalLabel = parts.length
? parts.join(middleDot)
: objectId || objectName || deviceLabel || "BACnet Point Out";
} else {
var separator = " " + String.fromCharCode(183) + " ";
if (objectLabel && deviceLabel) finalLabel = objectLabel + separator + deviceLabel;
else if (objectLabel) finalLabel = objectLabel;
else if (deviceLabel) finalLabel = deviceLabel;
else if (objectId) finalLabel = objectId;
}
return finalLabel;
}
return this.name;
},
outputLabels: function (outputIndex) {
return outputIndex === 0 ? "Requested write value" : "Actual present value";
},
oneditprepare: function () {
var nodeId = this.id;
var nodeInstance = RED.nodes.node(nodeId);
window.__bnPointLabelCache = window.__bnPointLabelCache || {};
this._savedState = nodeInstance
? {
labelMode: nodeInstance.labelMode || "instance",
objectLabelMode: nodeInstance.objectLabelMode || "name",
hadPreviewLabel: Object.prototype.hasOwnProperty.call(
nodeInstance,
"previewLabel"
),
previewLabel: nodeInstance.previewLabel,
hadLabelCache: Object.prototype.hasOwnProperty.call(
window.__bnPointLabelCache,
nodeId
),
labelCache: window.__bnPointLabelCache[nodeId],
}
: null;
var lastDiscoveredObjects = [];
var isLoadingObjects = false;
if (RED.tabs && $("#bpo-tabs").length) {
var tabs = RED.tabs.create({
id: "bpo-tabs",
onchange: function (tab) {
$("#bpo-tabs-content").children().hide();
$("#" + tab.id).show();
},
});
tabs.addTab({
id: "bpo-tab-props",
label: "Properties",
iconClass: "fa fa-sliders",
});
tabs.addTab({
id: "bpo-tab-options",
label: "Options",
iconClass: "fa fa-cog",
});
tabs.activateTab("bpo-tab-props");
}
function convertObjectTypeToAbbr(typeValue) {
var TYPE_MAP = {
0: "AI",
1: "AO",
2: "AV",
3: "BI",
4: "BO",
5: "BV",
12: "MI",
13: "MO",
14: "MV",
19: "MSV",
PID: "MI",
};
if (typeof typeValue === "number")
return TYPE_MAP[typeValue] || String(typeValue);
var normalized = String(typeValue || "")
.trim()
.toUpperCase();
if (/^\d+$/.test(normalized)) {
var numericValue = Number(normalized);
return TYPE_MAP[numericValue] || normalized;
}
return TYPE_MAP[normalized] != null ? TYPE_MAP[normalized] : normalized;
}
function convertObjectTypeToNumeric(typeValue) {
var MAP_TO_NUMERIC = {
AI: 0,
AO: 1,
AV: 2,
BI: 3,
BO: 4,
BV: 5,
MI: 12,
MO: 13,
MV: 14,
MSV: 19,
PID: 12,
};
if (typeof typeValue === "number") return typeValue;
var normalized = String(typeValue || "")
.trim()
.toUpperCase();
if (/^\d+$/.test(normalized)) return +normalized;
return MAP_TO_NUMERIC[normalized] != null
? MAP_TO_NUMERIC[normalized]
: normalized;
}
function getFormControllerState() {
return {
di: ($("#node-input-deviceInstance").val() || "").trim(),
dn: ($("#node-input-deviceName").val() || "").trim(),
};
}
function renderSelectedPointPreview() {
var controllerState = getFormControllerState();
var objectTypeValue = $("#node-input-objectType").val() || "";
var objectInstanceValue = $("#node-input-objectInstance").val() || "";
var objectNameValue = ($("#node-input-objectName").val() || "").trim();
var objectId =
objectTypeValue && objectInstanceValue
? convertObjectTypeToAbbr(objectTypeValue) +
"-" +
objectInstanceValue
: "";
var previewParts = [];
if (controllerState.di) previewParts.push(controllerState.di);
if (controllerState.dn) previewParts.push(controllerState.dn);
if (objectId) previewParts.push(objectId);
else if (objectInstanceValue) previewParts.push(objectInstanceValue);
if (objectNameValue) previewParts.push(objectNameValue);
var separator = " " + String.fromCharCode(183) + " ";
var previewSummary = previewParts.join(separator);
$("#nr-point-selected").text(previewSummary || "None selected");
highlightCurrentSelection();
}
function updatePreviewLabelFromFields() {
var currentNodeInstance = RED.nodes.node(nodeId);
if (!currentNodeInstance) return;
var labelMode = $("#node-input-labelMode").val() || "instance";
var objectLabelMode = $("#node-input-objectLabelMode").val() || "name";
var objectNameValue = ($("#node-input-objectName").val() || "").trim();
var controllerState = getFormControllerState();
var objectTypeValue = $("#node-input-objectType").val() || "";
var objectInstanceValue = $("#node-input-objectInstance").val() || "";
var objectId =
objectTypeValue && objectInstanceValue
? convertObjectTypeToAbbr(objectTypeValue) +
"-" +
objectInstanceValue
: "";
var deviceLabel = buildDeviceDisplayLabel(
labelMode,
controllerState.dn,
controllerState.di
);
var objectLabel = buildObjectDisplayLabel(
objectLabelMode,
objectNameValue,
objectId
);
currentNodeInstance.labelMode = labelMode;
currentNodeInstance.objectLabelMode = objectLabelMode;
var previewLabel;
if (labelMode === "both") {
var labelParts = [];
if (objectLabel) labelParts.push(objectLabel);
if (controllerState.dn) labelParts.push(controllerState.dn);
if (controllerState.di) labelParts.push(controllerState.di);
if (!labelParts.length)
labelParts.push(objectId || objectNameValue || "");
var middleDot = " " + String.fromCharCode(183) + " ";
previewLabel = labelParts.length
? labelParts.join(middleDot)
: deviceLabel || objectId || "";
} else {
var separator = " " + String.fromCharCode(183) + " ";
if (objectLabel && deviceLabel) {
previewLabel = objectLabel + separator + deviceLabel;
} else if (objectLabel) {
previewLabel = objectLabel;
} else if (deviceLabel) {
previewLabel = deviceLabel;
} else {
previewLabel = objectId || objectNameValue || "";
}
}
currentNodeInstance.previewLabel = previewLabel;
window.__bnPointLabelCache = window.__bnPointLabelCache || {};
window.__bnPointLabelCache[nodeId] =
currentNodeInstance.previewLabel || "";
RED.view.redraw(true);
highlightCurrentSelection();
}
function parseObjectIdString(objectIdString) {
var normalized = String(objectIdString || "")
.trim()
.toUpperCase();
var match = normalized.match(/^([A-Z]+)\-([0-9]+)$/);
if (!match) return null;
return {
type: convertObjectTypeToNumeric(match[1]),
instance: Number(match[2]),
};
}
function syncAdvancedFromSelection() {
var hiddenDeviceInstance = (
$("#node-input-deviceInstance").val() || ""
).trim();
var hiddenObjectType = ($("#node-input-objectType").val() || "").trim();
var hiddenObjectInstance = (
$("#node-input-objectInstance").val() || ""
).trim();
if ($("#node-input-device_instance").length) {
$("#node-input-device_instance").val(hiddenDeviceInstance);
}
if ($("#node-input-object_id").length) {
if (hiddenObjectType && hiddenObjectInstance) {
var objectId = (
convertObjectTypeToAbbr(hiddenObjectType) +
"-" +
hiddenObjectInstance
).toUpperCase();
$("#node-input-object_id").val(objectId);
if (nodeInstance) nodeInstance.object_id = objectId;
}
}
if (nodeInstance) {
nodeInstance.device_instance = hiddenDeviceInstance;
nodeInstance.deviceInstance = hiddenDeviceInstance;
}
}
function syncHiddenFieldsFromAdvanced(force) {
var isAdvanced = $("#node-input-advancedMode").is(":checked");
if (!force && !isAdvanced) return;
var advancedDeviceInstance = (
$("#node-input-device_instance").val() || ""
).trim();
$("#node-input-deviceInstance")
.val(advancedDeviceInstance)
.trigger("change");
$("#node-input-controllerDeviceInstance").val(advancedDeviceInstance);
var instanceRef = nodeInstance || RED.nodes.node(nodeId);
if (instanceRef) {
instanceRef.deviceInstance = advancedDeviceInstance;
instanceRef.controllerDeviceInstance = advancedDeviceInstance;
instanceRef.device_instance = advancedDeviceInstance;
}
var objectId = ($("#node-input-object_id").val() || "")
.trim()
.toUpperCase();
$("#node-input-object_id").val(objectId);
var parsedObjectId = parseObjectIdString(objectId);
if (parsedObjectId) {
$("#node-input-objectType")
.val(parsedObjectId.type)
.trigger("change");
$("#node-input-objectInstance")
.val(parsedObjectId.instance)
.trigger("change");
if (instanceRef) {
instanceRef.objectType = parsedObjectId.type;
instanceRef.objectInstance = parsedObjectId.instance;
instanceRef.object_id = objectId;
}
}
renderSelectedPointPreview();
highlightCurrentSelection();
}
function initializeAdvancedControls() {
if (
$("#node-input-topic").length &&
typeof $("#node-input-topic").typedInput === "function" &&
!$("#node-input-topic").data("typedInput")
) {
try {
var topicOptions = typeof topics === "function" ? topics() : [];
$("#node-input-topic").typedInput({
types: [{ value: "", options: topicOptions }],
});
} catch (_e) {}
}
syncAdvancedFromSelection();
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_out_obj_id") || ""
);
}
}
syncHiddenFieldsFromAdvanced(true);
}
function hydrateAdvancedFromNodeInstance() {
if (!nodeInstance) return;
var advancedState =
!!nodeInstance.advancedMode || nodeInstance.mode === "advanced";
$("#node-input-advancedMode").prop("checked", advancedState);
$("#node-input-mode").val(advancedState ? "advanced" : "list");
nodeInstance.advancedMode = advancedState;
nodeInstance.mode = advancedState ? "advanced" : "list";
$("#node-input-device_instance").val(
nodeInstance.device_instance || nodeInstance.deviceInstance || ""
);
var baseObjectId = nodeInstance.object_id;
if (
!baseObjectId &&
nodeInstance.objectType != null &&
nodeInstance.objectInstance != null
) {
baseObjectId = (
convertObjectTypeToAbbr(nodeInstance.objectType) +
"-" +
nodeInstance.objectInstance
).toUpperCase();
}
$("#node-input-object_id").val(baseObjectId || "");
if ($("#node-input-priority").data("typedInput")) {
$("#node-input-priority").typedInput(
"value",
nodeInstance.priority || "9"
);
} else {
$("#node-input-priority").val(nodeInstance.priority || "9");
}
if ($("#node-input-send_policy").data("typedInput")) {
$("#node-input-send_policy").typedInput(
"value",
nodeInstance.send_policy || "Always"
);
} else {
$("#node-input-send_policy").val(
nodeInstance.send_policy || "Always"
);
}
$("#node-input-output_type").val(nodeInstance.output_type || "ip");
$("#node-input-acknowledged_mqtt, #node-input-acknowledged_mqtt-simple").prop(
"checked",
!!nodeInstance.acknowledged_mqtt
);
$("#node-input-labelMode").val(nodeInstance.labelMode || "instance");
$("#node-input-objectLabelMode").val(
nodeInstance.objectLabelMode || "name"
);
$("#node-input-bypass_priority").prop(
"checked",
!!nodeInstance.bypass_priority
);
$("#node-input-allow_msg_override").prop(
"checked",
!!nodeInstance.allow_msg_override
);
if (nodeInstance.ocn_server_node) {
$("#node-input-ocn_server_node").val(
typeof nodeInstance.ocn_server_node === "object" &&
nodeInstance.ocn_server_node.id
? nodeInstance.ocn_server_node.id
: nodeInstance.ocn_server_node
);
}
syncAdvancedFromSelection();
syncHiddenFieldsFromAdvanced(true);
updateOcnServerTrace();
}
function getConfigNodeId(configValue) {
if (configValue == null) return "";
if (typeof configValue === "string") return configValue;
if (typeof configValue === "object") {
if (typeof configValue.id === "string") return configValue.id;
if (typeof configValue.value === "string") return configValue.value;
}
return "";
}
function updateOcnServerTrace() {
var ocnConfigId = getConfigNodeId(
$("#node-input-ocn_server_node").val()
);
if (!ocnConfigId && nodeInstance) {
ocnConfigId = getConfigNodeId(nodeInstance.ocn_server_node);
}
$("#bpo-current-ocn-id").text(
ocnConfigId ? ocnConfigId : "None selected"
);
}
function highlightCurrentSelection() {
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(convertObjectTypeToNumeric(objectTypeValue)) &&
rowElement.data("oi") == String(objectInstanceValue)
) {
rowElement.css("outline", "2px solid #4caf50");
} else {
rowElement.css("outline", "none");
}
});
}
function renderFilteredObjectList(objectList, searchQuery) {
var container = $("#nr-point-list");
container.empty();
var objectsArray = Array.isArray(objectList) ? objectList.slice() : [];
var filterText = (searchQuery || "").toLowerCase();
var controllerState = getFormControllerState();
if (filterText) {
objectsArray = objectsArray.filter(function (objectInfo) {
var abbr = convertObjectTypeToAbbr(objectInfo.objectType);
var composedAbbr = abbr + "-" + String(objectInfo.objectInstance);
var composedNumeric =
String(objectInfo.objectType) +
"-" +
String(objectInfo.objectInstance);
var searchableText = [
objectInfo.controllerDeviceInstance || controllerState.di || "",
objectInfo.controllerName || controllerState.dn || "",
composedAbbr,
composedNumeric,
objectInfo.objectName || "",
objectInfo.units || "",
String(objectInfo.objectInstance || ""),
String(objectInfo.objectType || ""),
String(objectInfo.objectTypeName || ""),
]
.join(" ")
.toLowerCase();
return searchableText.includes(filterText);
});
}
if (!objectsArray.length) {
container.append(
'<div style="padding:8px;color:#666">No objects</div>'
);
return;
}
// Sort so AI objects (type 0) appear at the end
objectsArray.sort(function (a, b) {
var aType = convertObjectTypeToNumeric(a.objectType);
var bType = convertObjectTypeToNumeric(b.objectType);
var aIsAI = aType === 0;
var bIsAI = bType === 0;
if (aIsAI && !bIsAI) return 1;
if (!aIsAI && bIsAI) return -1;
return 0;
});
objectsArray.forEach(function (objectInfo) {
var abbr = convertObjectTypeToAbbr(objectInfo.objectType);
var objectId = abbr + "-" + String(objectInfo.objectInstance);
var deviceInstance =
objectInfo.controllerDeviceInstance != null &&
objectInfo.controllerDeviceInstance !== ""
? String(objectInfo.controllerDeviceInstance)
: controllerState.di;
var deviceName =
objectInfo.controllerName != null &&
objectInfo.controllerName !== ""
? String(objectInfo.controllerName)
: controllerState.dn;
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(objectInfo.objectType))
.attr("data-oi", String(objectInfo.objectInstance));
rowElement
.data("ot", String(objectInfo.objectType))
.data("oi", String(objectInfo.objectInstance));
var leftContent = $('<div style="min-width:0"></div>');
var middleDot = " " + String.fromCharCode(183) + " ";
var titleParts = [];
if (objectInfo.objectName) titleParts.push(objectInfo.objectName);
if (deviceName) titleParts.push(deviceName);
var titleElement = $(
'<div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></div>'
).text(titleParts.join(middleDot));
var subtitleParts = [objectId];
if (deviceInstance) subtitleParts.push(deviceInstance);
if (
objectInfo.presentValue != null &&
objectInfo.presentValue !== ""
) {
var pvText =
"PV: " +
objectInfo.presentValue +
(objectInfo.units ? " " + objectInfo.units : "");
subtitleParts.push(pvText);
}
var subtitleElement = $(
'<div style="font-size:12px;color:#666"></div>'
).text(subtitleParts.join(middleDot));
leftContent.append(titleElement).append(subtitleElement);
rowElement.append(leftContent);
rowElement.on("click keypress", function (event) {
if (event.type === "keypress" && event.key !== "Enter") return;
$("#node-input-objectType")
.val(objectInfo.objectType)
.trigger("change");
$("#node-input-objectInstance")
.val(objectInfo.objectInstance)
.trigger("change");
$("#node-input-objectName")
.val(objectInfo.objectName || "")
.trigger("change");
$("#node-input-description")
.val(objectInfo.description || "")
.trigger("change");
$("#node-input-units")
.val(objectInfo.units || "")
.trigger("change");
if (deviceInstance) {
$("#node-input-deviceInstance")
.val(deviceInstance)
.trigger("input");
}
if (deviceName) {
$("#node-input-deviceName").val(deviceName).trigger("input");
}
var selectedNodeInstance = RED.nodes.node(nodeId);
if (selectedNodeInstance) {
var beforeKey =
selectedNodeInstance.objectType +
"|" +
selectedNodeInstance.objectInstance;
selectedNodeInstance.objectType = objectInfo.objectType;
selectedNodeInstance.objectInstance = objectInfo.objectInstance;
selectedNodeInstance.objectName = objectInfo.objectName || "";
selectedNodeInstance.description = objectInfo.description || "";
selectedNodeInstance.units = objectInfo.units || "";
selectedNodeInstance.deviceInstance = deviceInstance || "";
selectedNodeInstance.deviceName = deviceName || "";
selectedNodeInstance.autoLabel = true;
selectedNodeInstance.name = "";
var labelMode = $("#node-input-labelMode").val() || "instance";
var objectLabelMode =
$("#node-input-objectLabelMode").val() || "name";
selectedNodeInstance.labelMode = labelMode;
selectedNodeInstance.objectLabelMode = objectLabelMode;
var deviceLabel = buildDeviceDisplayLabel(
labelMode,
deviceName,
deviceInstance
);
var objectIdForPreview = abbr + "-" + objectInfo.objectInstance;
var objectLabel = buildObjectDisplayLabel(
objectLabelMode,
selectedNodeInstance.objectName,
objectIdForPreview
);
var previewLabel;
if (labelMode === "both") {
var labelParts = [];
if (objectLabel) labelParts.push(objectLabel);
if (deviceName) labelParts.push(deviceName);
if (deviceInstance) labelParts.push(deviceInstance);
if (!labelParts.length) {
labelParts.push(
objectIdForPreview || selectedNodeInstance.objectName || ""
);
}
var middleDotLabel = " " + String.fromCharCode(183) + " ";
previewLabel = labelParts.length
? labelParts.join(middleDotLabel)
: deviceLabel || objectIdForPreview || "";
} else {
var separator = " " + String.fromCharCode(183) + " ";
if (objectLabel && deviceLabel) {
previewLabel = objectLabel + separator + deviceLabel;
} else if (objectLabel) {
previewLabel = objectLabel;
} else if (deviceLabel) {
previewLabel = deviceLabel;
} else {
previewLabel =
objectIdForPreview || selectedNodeInstance.objectName || "";
}
}
selectedNodeInstance.previewLabel = previewLabel;
var afterKey =
selectedNodeInstance.objectType +
"|" +
selectedNodeInstance.objectInstance;
if (beforeKey !== afterKey) {
selectedNodeInstance.changed = true;
RED.nodes.dirty(true);
}
window.__bnPointLabelCache = window.__bnPointLabelCache || {};
window.__bnPointLabelCache[nodeId] =
selectedNodeInstance.previewLabel || "";
RED.view.redraw(true);
}
syncAdvancedFromSelection();
syncHiddenFieldsFromAdvanced(true);
updateOcnServerTrace();
renderSelectedPointPreview();
// 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-deviceName")
.val(controllerName)
.trigger("input");
}
if (controllerDeviceInstance) {
$("#node-input-deviceInstance")
.val(controllerDeviceInstance)
.trigger("input");
}
// Get object info from snapshot if available
if (objectInfo.objectType != null && objectInfo.objectInstance != null && ctrl.objByKey) {
var numericType = Number(objectInfo.objectType);
var lookupKey = String(numericType) + "|" + String(objectInfo.objectInstance);
var objectEntry = ctrl.objByKey.get(lookupKey);
if (objectEntry && objectEntry.objectName) {
$("#node-input-objectName")
.val(objectEntry.objectName)
.trigger("input");
}
}
updatePreviewLabelFromFields();
}
}
});
container.append(rowElement);
});
highlightCurrentSelection();
}
function loadObjectsFromApi() {
if (isLoadingObjects) return;
isLoadingObjects = true;
$("#nr-point-loading").css("display", "flex");
$.ajax({
url: "bacnet-point-out/objects",
method: "GET",
cache: false,
data: { nodeId: nodeId },
dataType: "text",
})
.done(function (_unused, _status, xhr) {
var responseText = xhr.responseText || "[]";
try {
responseText = responseText.replace(/^\)\]\}',?\s*/, "");
} catch (e) {}
var parsedArray = [];
try {
parsedArray = JSON.parse(responseText);
} catch (e) {}
lastDiscoveredObjects = Array.isArray(parsedArray)
? parsedArray
: [];
renderFilteredObjectList(
lastDiscoveredObjects,
$("#nr-point-search").val()
);
$("#nr-point-tip").hide();
})
.fail(function (xhr) {
function parseErrorMessage(httpResponse) {
try {
if (
httpResponse &&
typeof httpResponse.responseText === "string"
) {
var text = httpResponse.responseText.replace(
/^\)\]\}',?\s*/,
""
);
var parsed = JSON.parse(text);
if (parsed && parsed.error) {
return String(parsed.error);
}
}
} catch (e) {}
return (
"HTTP " +
(httpResponse && httpResponse.status ? httpResponse.status : "")
);
}
$("#nr-point-tip").text(parseErrorMessage(xhr)).show();
$("#nr-point-list").empty().html("No objects");
})
.always(function () {
isLoadingObjects = false;
$("#nr-point-loading").css("display", "none");
});
}
function toggleModeUserInterface() {
var isAdvanced = $("#node-input-advancedMode").is(":checked");
$("#node-input-mode").val(isAdvanced ? "advanced" : "list");
if (nodeInstance) {
nodeInstance.advancedMode = isAdvanced;
nodeInstance.mode = isAdvanced ? "advanced" : "list";
}
if (!isAdvanced) {
$("#nr-mode-list").show();
$("#nr-simple-comm").show();
$("#nr-mode-advanced").hide();
syncHiddenFieldsFromAdvanced(true);
} else {
$("#nr-mode-list").hide();
$("#nr-simple-comm").hide();
$("#nr-mode-advanced").show();
initializeAdvancedControls();
}
updateOcnServerTrace();
}
$("#nr-point-search").on("input", function () {
renderFilteredObjectList(lastDiscoveredObjects, this.value);
});
$("#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);
}
});
$("#node-input-advancedMode").on("change", function () {
toggleModeUserInterface();
});
$(
"#node-input-objectName,#node-input-objectType,#node-input-objectInstance,#node-input-deviceInstance,#node-input-deviceName,#node-input-labelMode,#node-input-objectLabelMode"
).on("input change", function () {
updatePreviewLabelFromFields();
renderSelectedPointPreview();
});
$("#node-input-priority").typedInput({
types: [
{
options: [
{ value: "1", label: "Manual Life Safety (1)" },
{ value: "2", label: "Automatic Life Safety (2)" },
{ value: "3", label: "3" },
{ value: "4", label: "4" },
{ value: "5", label: "Critical Equipment Control (5)" },
{ value: "6", label: "Minimum On/Off (6)" },
{ value: "7", label: "OCN Special Event (7)" },
{ value: "8", label: "Manual Operator (8)" },
{ value: "9", label: "OCN Reserved (9)" },
{ value: "10", label: "10" },
{ value: "11", label: "11" },
{ value: "12", label: "12" },
{ value: "13", label: "13" },
{ value: "14", label: "14" },
{ value: "15", label: "15" },
{ value: "16", label: "16" },
],
},
],
});
$("#node-input-send_policy").typedInput({
types: [
{
value: false,
options: [
{ value: "Always", label: "Always" },
{ value: "On value changed", label: "On value changed" },
],
},
],
});
$("#node-input-priority").typedInput(
"value",
(nodeInstance && nodeInstance.priority) || "9"
);
$("#node-input-send_policy").typedInput(
"value",
(nodeInstance && nodeInstance.send_policy) || "Always"
);
(function initSimpleCommLinks() {
function linkSelect(simpleSelector, advancedSelector, options) {
var simpleElement = $(simpleSelector);
var advancedElement = $(advancedSelector);
if (!simpleElement.length || !advancedElement.length) return;
var suppressSync = false;
function readAdvancedValue() {
if (
options &&
options.typed &&
advancedElement.data("typedInput")
) {
return advancedElement.typedInput("value");
}
return advancedElement.val();
}
function writeAdvancedValue(value) {
if (
options &&
options.typed &&
advancedElement.data("typedInput")
) {
advancedElement.typedInput("value", value);
advancedElement.trigger("change");
} else {
advancedElement.val(value).trigger("change");
}
}
function syncFromAdvanced() {
if (suppressSync) return;
suppressSync = true;
var value = readAdvancedValue();
if (value == null) value = "";
simpleElement.val(String(value));
suppressSync = false;
}
simpleElement.on("change", function () {
if (suppressSync) return;
suppressSync = true;
writeAdvancedValue($(this).val());
suppressSync = false;
});
advancedElement.on("change", syncFromAdvanced);
syncFromAdvanced();
}
function linkCheckbox(simpleSelector, advancedSelector) {
var simpleElement = $(simpleSelector);
var advancedElement = $(advancedSelector);
if (!simpleElement.length || !advancedElement.length) return;
var suppressSync = false;
function syncFromAdvanced() {
if (suppressSync) return;
suppressSync = true;
simpleElement.prop("checked", advancedElement.is(":checked"));
suppressSync = false;
}
simpleElement.on("change", function () {
if (suppressSync) return;
suppressSync = true;
advancedElement
.prop("checked", $(this).is(":checked"))
.trigger("change");
suppressSync = false;
});
advancedElement.on("change", syncFromAdvanced);
syncFromAdvanced();
}
linkSelect("#node-input-output_type-simple", "#node-input-output_type");
linkSelect(
"#node-input-send_policy-simple",
"#node-input-send_policy",
{ typed: true }
);
linkSelect("#node-input-priority-simple", "#node-input-priority", {
typed: true,
});
linkCheckbox(
"#node-input-bypass_priority-simple",
"#node-input-bypass_priority"
);
linkCheckbox(
"#node-input-acknowledged_mqtt-simple",
"#node-input-acknowledged_mqtt"
);
})();
function handleAdvancedFieldChange() {
syncHiddenFieldsFromAdvanced();
if (nodeInstance) {
var previousOutputType = nodeInstance.output_type;
var previousOcnServerNode = nodeInstance.ocn_server_node;
nodeInstance.priority =
$("#node-input-priority").typedInput("value") ||
nodeInstance.priority;
nodeInstance.send_policy =
$("#node-input-send_policy").typedInput("value") ||
nodeInstance.send_policy;
var outputTypeValue = $("#node-input-output_type-simple").val() ||
$("#node-input-output_type").val() ||
nodeInstance.output_type;
nodeInstance.output_type = outputTypeValue;
nodeInstance.bypass_priority = $("#node-input-bypass_priority").is(
":checked"
);
nodeInstance.allow_msg_override = $(
"#node-input-allow_msg_override"
).is(":checked");
var ocnValue = $("#node-input-ocn_server_node").val();
nodeInstance.ocn_server_node = ocnValue || "";
var hasChanged = previousOutputType !== nodeInstance.output_type ||
previousOcnServerNode !== nodeInstance.ocn_server_node;
if (hasChanged) {
nodeInstance.changed = true;
try {
RED.nodes.dirty(true);
} catch (error) {}
}
}
updateOcnServerTrace();
}
$(
"#node-input-device_instance, #node-input-object_id, #node-input-output_type, #node-input-ocn_server_node"
).on("input change", handleAdvancedFieldChange);
$("#node-input-bypass_priority, #node-input-allow_msg_override, #node-input-acknowledged_mqtt, #node-input-acknowledged_mqtt-simple").on(
"change",
handleAdvancedFieldChange
);
$("#node-input-priority").on("change", handleAdvancedFieldChange);
$("#node-input-send_policy").on("change", handleAdvancedFieldChange);
$("#node-input-output_type-simple").on("change", handleAdvancedFieldChange);
if (nodeInstance) {
hydrateAdvancedFromNodeInstance();
} else {
$("#node-input-advancedMode").prop("checked", false);
$("#node-input-mode").val("list");
}
toggleModeUserInterface();
renderSelectedPointPreview();
if (!$("#node-input-advancedMode").is(":checked")) {
setTimeout(loadObjectsFromApi, 20);
$("#nr-point-list").html("Loading objects...");
} else {
syncHiddenFieldsFromAdvanced(true);
}
},
oneditcancel: function () {
var editorNodeInstance = RED.nodes.node(this.id);
var saved = this._savedState;
if (editorNodeInstance && saved) {
editorNodeInstance.labelMode = saved.labelMode;
editorNodeInstance.objectLabelMode = saved.objectLabelMode;
if (saved.hadPreviewLabel) {
editorNodeInstance.previewLabel = saved.previewLabel;
} else {
try {
delete editorNodeInstance.previewLabel;
} catch (_e) {}
}
window.__bnPointLabelCache = window.__bnPointLabelCache || {};
if (saved.hadLabelCache) {
window.__bnPointLabelCache[this.id] = saved.labelCache;
} else {
try {
delete window.__bnPointLabelCache[this.id];
} catch (_e) {}
}
}
this._savedState = null;
if (RED.view && RED.view.redraw) {
RED.view.redraw(true);
}
},
oneditsave: function () {
this._savedState = null;
this.acknowledged_mqtt = $("#node-input-acknowledged_mqtt").is(":checked");
try {
if (
$("#node-input-advancedMode").is(":checked") &&
window.localStorage
) {
var deviceInstance = (
$("#node-input-device_instance").val() || ""
).trim();
var objectId = ($("#node-input-object_id").val() || "")
.trim()
.toUpperCase();
if (deviceInstance) {
localStorage.setItem("default_device_instance", deviceInstance);
}
if (objectId) {
localStorage.setItem("default_out_obj_id", objectId);
}
}
} catch (_e) {}
},
});
// Validate nodes immediately when they're added (including copy/paste)
// This ensures HTTP endpoints can find the node without needing to deploy
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);
}
// 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);
}
});
if (RED && RED.palette && RED.palette.hide) {
RED.palette.hide('bacnet-point-out');
}
</script>
<script type="text/html" data-template-name="bacnet-point-out">
<div class="form-row"><ul id="bpo-tabs" class="red-ui-tabs"></ul></div>
<div id="bpo-tabs-content">
<div id="bpo-tab-props" style="display:none">
<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>
<input type="hidden" id="node-input-deviceName" />
<div id="nr-mode-list">
<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:8px"
/>
<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>
</div>
<div id="nr-simple-comm" style="margin-top:20px;">
<div class="form-tips" style="margin-bottom:10px;">
<b>Priority Settings</b>
</div>
<div class="form-row">
<label for="node-input-priority-simple"
><i class="fa fa-sort-numeric-asc"></i> Priority</label
>
<select id="node-input-priority-simple" style="width:70%;">
<option value="1">Manual Life Safety (1)</option>
<option value="2">Automatic Life Safety (2)</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">Critical Equipment Control (5)</option>
<option value="6">Minimum On/Off (6)</option>
<option value="7">OCN Special Event (7)</option>
<option value="8">Manual Operator (8)</option>
<option value="9">OCN Reserved (9)</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14">14</option>
<option value="15">15</option>
<option value="16">16</option>
</select>
</div>
<div class="form-row">
<label for="node-input-bypass_priority-simple"
><i class="fa fa-random"></i> Bypass Priority</label
>
<input
type="checkbox"
id="node-input-bypass_priority-simple"
style="width:auto;margin-top:0;"
/>
</div>
<div class="form-tips" style="margin:20px 0 10px;">
<b>Communication Settings</b>
</div>
<div class="form-row" style="margin-top:10px;">
<label for="node-input-output_type-simple"
><i class="fa fa-exchange"></i> Protocol</label
>
<select id="node-input-output_type-simple" style="width:70%;">
<option value="ip">IP</option>
<option value="mstp">MSTP</option>
</select>
</div>
<div class="form-row" style="margin-top:10px;">
<label for="node-input-send_policy-simple"
><i class="fa fa-paper-plane"></i> Send Policy</label
>
<select id="node-input-send_policy-simple" style="width:70%;">
<option value="Always">Always</option>
<option value="On value changed">On value changed</option>
</select>
</div>
<div class="form-row" style="display:flex;align-items:center;margin-top:10px;">
<label for="node-input-acknowledged_mqtt-simple" style="display:flex;align-items:center;flex-shrink:0;margin:0;cursor:pointer;">
<i class="fa fa-arrow-left" style="margin-right:6px;flex-shrink:0;"></i>
<span>Acknowledged (MQTT)</span>
</label>
<input type="checkbox" id="node-input-acknowledged_mqtt-simple" style="width:auto;margin:0 0 0 12px;flex-shrink:0;" />
</div>
<div style="margin-top:10px;padding:10px 12px;border:1px solid #ddd;background:#fff6df;border-radius:6px;font-size:14px;line-height:1.5;color:#7a5b00;">
<i class="fa fa-info-circle" style="margin-right:6px;"></i><b>Tip:</b> Third-party controllers must have a polling policy configured in OCN<br>for the acknowledge feature to work.
</div>
</div>
<div id="nr-mode-advanced" 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., 213001"
/>
</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 class="form-row">
<label for="node-input-priority"
><i class="fa fa-sort-numeric-asc"></i> Priority</label
>
<input type="text" id="node-input-priority" />
</div>
<div class="form-row">
<label for="node-input-bypass_priority"
><i class="fa fa-random"></i> Bypass Priority</label
>
<input
type="checkbox"
id="node-input-bypass_priority"
style="width:auto;margin-top:0;"
/>
</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" style="margin-top:10px;">
<label for="node-input-output_type"
><i class="fa fa-exchange"></i> Protocol</label
>
<select id="node-input-output_type" style="width:70%;">
<option value="ip">IP</option>
<option value="mstp">MSTP</option>
</select>
</div>
<div class="form-row" style="margin-top:10px;">
<label for="node-input-send_policy"
><i class="fa fa-paper-plane"></i> Send Policy</label
>
<input type="text" id="node-input-send_policy" />
</div>
<div class="form-row">
<label for="node-input-allow_msg_override"
><i class="fa fa-exchange"></i> Allow msg overrides</label
>
<input
type="checkbox"
id="node-input-allow_msg_override"
style="width:auto;margin-top:0;"
/>
</div>
<div class="form-row">
<label for="node-input-acknowledged_mqtt"
><i class="fa fa-arrow-left"></i> Acknowledged (MQTT)</label
>
<input type="checkbox" id="node-input-acknowledged_mqtt" style="width:auto;margin:0 0 0 12px;flex-shrink:0;" />
</div>
<div style="margin-top:10px;padding:10px 12px;border:1px solid #ddd;background:#fff6df;border-radius:6px;font-size:14px;line-height:1.5;color:#7a5b00;">
<i class="fa fa-info-circle" style="margin-right:6px;"></i><b>Tip:</b> Third-party controllers must have a polling policy configured in OCN<br>for the acknowledge feature to work.
</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>
</div>
</div>
<input type="hidden" id="node-input-deviceInstance" />
<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-networkNumber" />
<input type="hidden" id="node-input-controllerDeviceInstance" />
<input type="hidden" id="node-input-controllerName" />
<div id="bpo-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="min-width:160px">
<option value="instance" selected>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="min-width:160px">
<option value="name" selected>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="bpo-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>
<input type="hidden" id="node-input-mode" value="list" />
</div>
</div>
</script>
<script type="text/javascript">
(function () {
function refreshAllPointOutLabels(onComplete) {
try {
var pointNodes = [];
if (RED.nodes && typeof RED.nodes.filterNodes === "function") {
pointNodes = RED.nodes.filterNodes({ type: "bacnet-point-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, deviceName, deviceInstance, 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
// 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-out/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 oldDeviceName = String(pointNode.deviceName || "");
var oldDeviceInstance = String(pointNode.deviceInstance || "");
var oldUnits = String(pointNode.units || "");
var oldDescription = String(pointNode.description || "");
// Update frontend node properties from backend
// Note: The backend returns "deviceName" but the node property might be "deviceName" or "controllerName" depending on usage context
// In bacnet-point-out.js defaults it is deviceName.
// Update the actual node object that Node-RED uses for rendering
// Get fresh reference to ensure we're updating the right object
var actualNode = RED.nodes.node ? RED.nodes.node(pointNode.id) : pointNode;
if (!actualNode) actualNode = pointNode;
if (backendProps.objectName !== oldObjectName) {
actualNode.objectName = backendProps.objectName;
pointNode.objectName = backendProps.objectName; // Also update the loop variable
// Don't set changed = true here - these are backend sync updates, not user changes
updated = true;
}
// Handle device/controller name - try both property sets for robustness
var newDeviceName = backendProps.deviceName || backendProps.controllerName;
if (newDeviceName && newDeviceName !== oldDeviceName) {
actualNode.deviceName = newDeviceName;
pointNode.deviceName = newDeviceName; // Also update the loop variable
// Don't set changed = true here - these are backend sync updates, not user changes
updated = true;
}
// Handle device instance - try both property sets
var newDeviceInstance = backendProps.deviceInstance || backendProps.controllerDeviceInstance;
if (newDeviceInstance && newDeviceInstance !== oldDeviceInstance) {
actualNode.deviceInstance = newDeviceInstance;
pointNode.deviceInstance = newDeviceInstance; // Also update the loop variable
// Don't set changed = true here - these are backend sync updates, not user changes
updated = true;
}
// Always clear cache and previewLabel for the actual node
if (window.__bnPointLabelCache && window.__bnPointLabelCache[actualNode.id]) {
delete window.__bnPointLabelCache[actualNode.id];
}
if (actualNode.previewLabel) {
delete actualNode.previewLabel;
}
if (backendProps.units !== oldUnits) {
pointNode.units = backendProps.units;
updated = true;
}
if (backendProps.description !== oldDescription) {
pointNode.description = backendProps.description;
updated = true;
}
if (updated) {
nodesUpdated++;
}
// Cache clearing moved above to use actualNode
}
});
})
.fail(function() {
// If the batch fetch fails there is no reliable backend data;
// labels are recalculated below from the current editor values.
})
.always(function() {
// Clear all label caches and previewLabels to force label recalculation
if (window.__bnPointLabelCache) {
Object.keys(window.__bnPointLabelCache).forEach(function(nodeId) {
if (RED.nodes.node(nodeId) && RED.nodes.node(nodeId).type === 'bacnet-point-out') {
delete window.__bnPointLabelCache[nodeId];
}
});
}
// Also clear previewLabels for all point-out nodes
pointNodes.forEach(function(pointNode) {
if (pointNode && pointNode.id && pointNode.previewLabel) {
delete pointNode.previewLabel;
}
});
function forceRedraw() {
try {
if (RED.view && typeof RED.view.redraw === "function") {
RED.view.redraw(true);
return true;
}
} catch (e) {
}
return false;
}
// Redraw with a delay to ensure labels update
forceRedraw();
setTimeout(function() {
forceRedraw();
}, 200);
if (typeof onComplete === "function") onComplete();
});
} catch (refreshError) {
if (typeof onComplete === "function") onComplete();
}
}
// Debounced/coalesced entry point for refreshAllPointOutLabels: 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 __bnOutRefreshTimer = null;
var __bnOutRefreshInFlight = false;
var __bnOutRefreshQueued = false;
var __bnOutRefreshCallbacks = [];
var __bnOutFlowsLoaded = false;
function runOutRefreshNow() {
if (__bnOutRefreshInFlight) {
__bnOutRefreshQueued = true;
return;
}
__bnOutRefreshInFlight = true;
var completionCallbacks = __bnOutRefreshCallbacks;
__bnOutRefreshCallbacks = [];
refreshAllPointOutLabels(function () {
__bnOutRefreshInFlight = false;
completionCallbacks.forEach(function (callback) {
try {
callback();
} catch (_ignoredError) {}
});
if (__bnOutRefreshQueued) {
__bnOutRefreshQueued = false;
scheduleRefreshAllPointOutLabels();
}
});
}
// 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 scheduleRefreshAllPointOutLabels(onComplete) {
if (typeof onComplete === "function") __bnOutRefreshCallbacks.push(onComplete);
if (__bnOutRefreshTimer) clearTimeout(__bnOutRefreshTimer);
__bnOutRefreshTimer = setTimeout(function () {
__bnOutRefreshTimer = null;
runOutRefreshNow();
}, 300);
}
function buildPreviewLabel(pointNode) {
var labelMode = pointNode.labelMode || "instance";
var objectLabelMode = pointNode.objectLabelMode || "name";
var deviceLabel = buildDeviceDisplayLabel(
labelMode,
pointNode.deviceName,
pointNode.deviceInstance
);
var objectId = "";
if (pointNode.objectType != null && pointNode.objectInstance != null) {
var TYPE_MAP = {
0: "AI", 1: "AO", 2: "AV", 3: "BI", 4: "BO", 5: "BV",
12: "MI", 13: "MO", 14: "MV", 19: "MSV"
};
var typeAbbr = TYPE_MAP[pointNode.objectType] || String(pointNode.objectType);
objectId = typeAbbr + "-" + pointNode.objectInstance;
}
var objectLabel = buildObjectDisplayLabel(
objectLabelMode,
pointNode.objectName,
objectId
);
if (labelMode === "both") {
var labelParts = [];
if (objectLabel) labelParts.push(objectLabel);
if (pointNode.deviceName) labelParts.push(pointNode.deviceName);
if (pointNode.deviceInstance) labelParts.push(pointNode.deviceInstance);
if (!labelParts.length) labelParts.push(objectId || pointNode.objectName || "");
var middleDot = " " + String.fromCharCode(183) + " ";
return labelParts.length ? labelParts.join(middleDot) : deviceLabel || objectId || "";
} else {
var separator = " " + String.fromCharCode(183) + " ";
if (objectLabel && deviceLabel) {
return objectLabel + separator + deviceLabel;
} else if (objectLabel) {
return objectLabel;
} else if (deviceLabel) {
return deviceLabel;
} else {
return objectId || pointNode.objectName || "";
}
}
}
window.__bnPointOutForceRefresh = scheduleRefreshAllPointOutLabels;
// One batch refresh after the initial flow import completes
RED.events.on("flows:loaded", function() {
__bnOutFlowsLoaded = true;
scheduleRefreshAllPointOutLabels();
});
// Refresh labels on deploy
// Refresh labels on deploy with retries to account for backend startup delay
RED.events.on("deploy", function() {
// Function to run the full refresh sequence
function runRefreshSequence() {
// Add delay to ensure backend refreshMapping() completes before fetching properties
// refreshMapping() runs with 200ms delay, so wait a bit longer to ensure it completes
// Get all point-out 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-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];
// Also force wipe previewLabel, which takes precedence
if (pointNode.previewLabel) {
delete pointNode.previewLabel;
}
}
});
// Redraw immediately to refresh labels (cache clearing is enough to force recalculation)
if (RED.view && typeof RED.view.redraw === "function") {
RED.view.redraw(true);
}
// Also call refresh to sync with backend
try {
scheduleRefreshAllPointOutLabels();
} catch (e) {
// Error calling refreshAllPointOutLabels
}
}
// Wait for backend refreshMapping() to complete (it runs with 200ms delay)
// Then run refresh sequence with additional delay to ensure backend updates are persisted
setTimeout(runRefreshSequence, 500);
// Retry once more 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 (!__bnOutFlowsLoaded) return;
if (addedNode && (addedNode.type === "bacnet-point-out" || addedNode.type === "bacnet-controller")) {
scheduleRefreshAllPointOutLabels();
}
});
RED.events.on("nodes:remove", function(removedNode) {
if (removedNode && (removedNode.type === "bacnet-point-out" || removedNode.type === "bacnet-controller")) {
scheduleRefreshAllPointOutLabels();
}
});
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-out" || node.type === "bacnet-controller")) {
shouldRefresh = true;
break;
}
}
}
if (shouldRefresh) {
scheduleRefreshAllPointOutLabels();
}
});
})();
</script>
<script type="text/html" data-help-name="bacnet-point-out">
<p>
Select a point from a controller in List mode, or enter fields in Advanced
mode. Output 1 emits the requested write. Output 2 emits the actual value.
</p>
</script>