@janart19/node-red-fusebox
Version:
A comprehensive collection of custom nodes for interfacing with Fusebox automation controllers - data streams, energy management, and utilities
275 lines (237 loc) • 11.2 kB
HTML
<script type="text/javascript">
RED.nodes.registerType("fusebox-measure-delay", {
category: "fusebox utils",
color: "#e6e0f8",
defaults: {
name: { value: "" },
trigger: { value: "" },
partner: { value: "" },
timeout: { value: 1000, validate: (v) => !isNaN(v) && v > 0 },
responseType: { value: "exact" },
tolerance: { value: 0, validate: (v) => !isNaN(v) && v >= 0 }
},
inputs: 1,
outputs: 1,
icon: "timer.svg",
label: function () {
return this.name || "measure delay";
},
paletteLabel: function () {
return "measure delay";
},
oneditprepare: function () {
const node = this;
let _controllers = {};
// Populate form with node values
$("#node-input-name").val(node.name);
$("#node-input-trigger").val(node.trigger);
$("#node-input-partner").val(node.partner);
$("#node-input-timeout").val(node.timeout);
$("#node-input-responseType").val(node.responseType || "exact");
$("#node-input-tolerance").val(node.tolerance ?? 0);
// Update tip text when relevant fields change
$("#node-input-trigger, #node-input-timeout").on("change keyup", updateTipText);
$("#node-input-responseType").on("change", updateResponseTypeVisibility);
// Initialize topic dropdown and tip text
queryControllerConfig();
updateResponseTypeVisibility();
updateTipText();
// Show/hide tolerance input depending on responseType
function updateResponseTypeVisibility() {
const v = $("#node-input-responseType").val();
if (v === "tolerance") {
$("#node-input-tolerance").closest(".form-row").show();
} else {
$("#node-input-tolerance").closest(".form-row").hide();
}
}
// Query topics defined in read and write nodes
function queryControllerConfig() {
$.getJSON(`fusebox/controller-config`, function (data) {
_controllers = data;
initializeAutocomplete($("#node-input-trigger"));
initializeAutocomplete($("#node-input-partner"));
}).fail(function () {
console.error("Failed to get topics!");
_controllers = {};
});
}
// Initialize autocomplete for topic inputs in the editable list
function initializeAutocomplete(element, onSelect = null) {
element
.autocomplete({
minLength: 0,
source: function (request, response) {
const term = request.term.toLowerCase();
const selection = getTopics();
const matches = selection?.filter((obj) => {
return obj.label.toLowerCase().indexOf(term) > -1;
});
response(matches);
},
focus: function (event, ui) {
// Don't change the input value on hover/focus
event.preventDefault();
},
select: function (event, ui) {
event.preventDefault();
element.val(ui.item.topic);
// Call the onSelect callback if provided
if (onSelect) onSelect(element, ui.item);
}
})
.on("focus", function () {
element.autocomplete("search", element.val() || "");
})
.autocomplete("instance")._renderItem = function (ul, item) {
const term = this.term.trim();
const label = item.label;
let highlightedLabel = label;
if (term) {
const regex = new RegExp("(" + term.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") + ")", "ig");
highlightedLabel = label.replace(regex, '<strong style="color:#e65; font-weight:bold">$1</strong>');
}
return $("<li></li>").data("ui-autocomplete-item", item).append(`<div>${highlightedLabel}</div>`).appendTo(ul);
};
}
// Return a list of topics from the controller configuration
// Structure: [ { key: "ABC", member: 1, topic: test/1, label: ... } ]
function getTopics() {
const result = [];
const controllers = _controllers?.controllers || [];
for (const controller of controllers) {
result.push(...controller.formattedTopics);
}
return result;
}
// Update tip text based on current settings
function updateTipText() {
const tipText1 = `Starts timer on message with topic "${$("#node-input-trigger").val() || "<trigger>"}".`;
const tipText2 = `Waits up to ${$("#node-input-timeout").val() || "<timeout>"} ms for a matching response.`;
const tipText3 = `If no response arrives in time, or the value differs, an error is raised.`;
$("#node-input-tip-text-1").text(tipText1);
$("#node-input-tip-text-2").text(tipText2);
$("#node-input-tip-text-3").text(tipText3);
}
},
// Save the values from the UI to the node configuration
oneditsave: function () {
const node = this;
node.name = $("#node-input-name").val();
node.trigger = $("#node-input-trigger").val();
node.partner = $("#node-input-partner").val();
node.timeout = parseInt($("#node-input-timeout").val());
node.responseType = $("#node-input-responseType").val();
node.tolerance = parseFloat($("#node-input-tolerance").val()) || 0;
}
});
</script>
<!-- Define style for the form fields -->
<style type="text/css">
.measure-delay-div .form-row {
margin-bottom: 10px;
}
.measure-delay-div .form-row label {
width: 33% ;
vertical-align: middle;
}
.measure-delay-div .form-row div,
.measure-delay-div .form-row input,
.measure-delay-div .form-row textarea {
max-width: 66% ;
}
.measure-delay-div .form-row select {
width: 66% ;
}
.measure-delay-div .form-tips {
max-width: 100% ;
text-align: center;
}
.measure-delay-div .help-text {
font-size: 0.8em;
color: #666;
margin-top: 1px;
margin-bottom: 6px;
}
.measure-delay-div .form-divider {
border-top: 1px solid #ccc;
margin: 5px 0;
}
/* Autocomplete widget styling */
.measure-delay-div .ui-autocomplete {
max-height: 250px;
overflow-y: auto;
overflow-x: hidden;
z-index: 2000;
background: #fff;
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.measure-delay-div .ui-menu-item {
font-size: 12px;
padding: 5px;
cursor: pointer;
position: relative;
}
.measure-delay-div .ui-menu-item:hover {
background-color: #f5f5f5;
}
</style>
<!-- Form fields for the node -->
<script type="text/html" data-template-name="fusebox-measure-delay">
<div class="measure-delay-div">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name" />
</div>
<div class="form-divider"></div>
<div class="form-row">
<label for="node-input-trigger"><i class="fa fa-bolt"></i> Trigger topic</label>
<input type="text" id="node-input-trigger" />
<div class="help-text">Topic that arms the timer when a message arrives with this topic and payload.</div>
</div>
<div class="form-row">
<label for="node-input-partner"><i class="fa fa-link"></i> Response topic</label>
<input type="text" id="node-input-partner" />
<div class="help-text">Topic that must provide the matching value (leave blank for any topic).</div>
</div>
<div class="form-row">
<label for="node-input-responseType"><i class="fa fa-check-circle"></i> Response type</label>
<select id="node-input-responseType">
<option value="exact">Only accept exact value</option>
<option value="tolerance">Accept difference within X</option>
</select>
<div class="help-text">Choose whether response must equal trigger exactly or accept numeric difference within tolerance.</div>
</div>
<div class="form-row">
<label for="node-input-tolerance"><i class="fa fa-sliders"></i> Response tolerance</label>
<input type="number" id="node-input-tolerance" step="any" />
<div class="help-text">Maximum absolute difference allowed to be considered a valid response.</div>
</div>
<div class="form-row">
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout (ms)</label>
<input type="number" id="node-input-timeout" />
<div class="help-text">Wait before raising a timeout error.</div>
</div>
<div class="form-tips" id="node-input-tip-text-1"></div>
<div class="form-tips" id="node-input-tip-text-2"></div>
<div class="form-tips" id="node-input-tip-text-3"></div>
<br />
</div>
</script>
<!-- Define node description -->
<script type="text/html" data-help-name="fusebox-measure-delay">
<p><b>measure-delay</b> waits for a second message that repeats the trigger-value.</p>
<ul>
<li><b>Trigger topic</b> - first message with this topic arms the timer.</li>
<li><b>Response topic</b> - must provide the matching value (blank = any topic).</li>
<li><b>Timeout</b> - window to wait before raising a timeout error.</li>
</ul>
<p>Outputs:</p>
<ul>
<li><code>{ok:true}</code> - response received in time and value matches.</li>
<li><code>{error:"mismatch"}</code> - response arrived but value differed.</li>
<li><code>{error:"timeout"}</code> - response never arrived.</li>
</ul>
</script>