UNPKG

@janart19/node-red-fusebox

Version:

A collection of Fusebox-specific custom nodes for Node-RED

1,067 lines (853 loc) 47 kB
<script type="text/javascript"> RED.nodes.registerType("fusebox-write-static-data-streams", { category: 'fusebox', color: '#f5706c', defaults: { name: { value: "" }, controller: { value: "", type: "fusebox-controller", required: true }, outputMode: { value: "", validate: function (v) { return ["all", "change"].includes(v); } }, payloadType: { value: "dynamic", validate: function (v) { return ["static", "dynamic"].includes(v); } }, mappings: { value: [] } // Store multiple mappings }, inputs: 1, outputs: 1, icon: 'font-awesome/fa-send', label: function () { const mappings = this.mappings; let defaultName = `send data streams`; if (mappings) defaultName = `send data streams: ${mappings.length}` return this.name || defaultName; }, paletteLabel: function () { return "send data streams"; }, oneditprepare: function () { const node = this; let _controller = {}; let _lastControllerId = null; // Load existing mappings or initialize an empty array const mappings = node.mappings || []; // Populate form with node values $('#node-input-name').val(node.name); $('#node-input-controller').val(node.controller); $('#node-input-outputMode').val(node.outputMode); $('#node-input-payloadType').val(node.payloadType); // Load controller data $("#node-input-controller").change(function () { queryControllerConfig(); }); // Define event listeners for form fields $("#mappings-container, #node-input-payloadType, #node-input-outputMode").change(function () { updateTipText(); }); $("#node-input-payloadType").change(function () { const rows = getListElements(); for (const elements of rows) { populatePayload(elements); validatePayload(elements); } }); // Initialize the form fields initializeEditableList(); // Query variables related to controller configuration function queryControllerConfig() { const controllerId = $("#node-input-controller").val(); if (_lastControllerId === controllerId) return; _lastControllerId = controllerId; $.getJSON(`fusebox/controllerNodeConfig?id=${controllerId}`) .done(function (data) { _controller = data; }) .fail(function () { console.error("Failed to query controller configuration!"); RED.notify("Failed to query controller configuration!", { type: "error", timeout: 3000 }); _controller = {}; }) .always(function () { $('#mappings-container').editableList('empty'); $('#mappings-container').editableList('addItems', mappings); updateTipText(); }); } // Initialize the EditableList widget function initializeEditableList() { $('#mappings-container').editableList({ removable: true, sortable: true, header: $("<div>").append($.parseHTML(` <div style="width: 22px"></div> <div class="write-static-data-streams-list" style="text-align: center; white-space: normal; flex: 1"> <div class="node-input-topic">Topic</div> <span class="node-input-arrow"></span> <div class="checkbox-container">Manual entry</div> <div class="node-input-keyNameManual">Data stream name</div> <div class="node-input-index">Member index</div> <div class="node-input-channelType">Channel type</div> <div class="node-input-coefficient">Coefficient</div> <div class="node-input-payload">Payload</div> </div> <div style="width: 28px"></div> `)), buttons: [{ label: "delete all", icon: "fa fa-trash", title: "delete all data stream mappings", click: function (e) { $('#mappings-container').editableList('empty'); updateTipText(); } }, { label: "auto-generate entries", icon: "fa fa-refresh", title: "automatically generate rows for all available controller data streams", click: function (e) { const hasServices = _controller.writableServices && Object.keys(_controller.writableServices).length > 0; if (!hasServices) { RED.notify("No available data streams to auto-generate mappings!", { type: "warning", timeout: 3000 }); return; } autoGenerateMappings(); } }], addItem: function (container, index, row) { addRowElements(container, row); const elements = getRowElements(container); formatRow(elements, row); attachRowEvents(elements); // Initialize autocomplete for the topic input in this row initializeAutocomplete(elements.topicElement, onAutocompleteSelect); updateTipText(); }, removeItem: function (data) { updateTipText(); } }); } // 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 allTopics = _controller?.formattedTopics || [] const writableServices = _controller?.writableServices || {}; const filteredServices = _controller?.filteredServices || {}; const filteredTopics = allTopics.filter(topic => { const key = topic.key; const member = topic.member; // Only include topics that are writable return filteredServices?.[key] && writableServices?.[key] && writableServices?.[key]?.includes(member); }); return filteredTopics; } // Further processing of a row after a topic is selected from the autocomplete function onAutocompleteSelect(element, obj = {}) { const container = element.closest('.red-ui-editableList-item-content'); let elements = getRowElements(container); validateTopic(elements); // Try to set the key and index fields according to the selected topic const key = obj.key; const member = obj.member; const manual = elements.manualElement.is(':checked'); const keyNameSelect = elements.keySelectElement.val(); const index = elements.indexElement.val(); if (manual || !key || !member) return; if (topicRelatedToService(`${key}.${member}`)) { elements.keySelectElement.val(key).trigger('change'); // NB! Because populateIndex() changes the element, find the latest elements = getRowElements(container); elements.indexElement.val(String(member)).trigger("input").trigger('change'); } } // Return true if the topic is related to a service function topicRelatedToService(topic) { const filteredServices = _controller?.filteredServices; if (!filteredServices || !topic || !topic.includes(".")) return false; const key = topic.split(".")[0]; const member = topic.split(".")[1]; return filteredServices?.[key] && Number.isInteger(parseInt(member)) && parseInt(member) >= 1 && parseInt(member) <= 16; } // Append the row to the container function addRowElements(container, row) { const rowHtml = ` <div class="write-static-data-streams-list"> <input type="text" class="node-input-topic" placeholder="Topic" value="${row.topic || ''}"> <span class="node-input-arrow">&#8594;</span> <div class="checkbox-container"> <input type="checkbox" class="node-input-manual" ${row.manual === true ? 'checked' : ''}> <div class="helper-text"></div> </div> <select class="node-input-keyNameSelect"></select> <input type="text" class="node-input-keyNameManual" placeholder="Manual entry, e.g. ABCW" value="${row.keyNameManual || ''}"> <input type="number" class="node-input-index" placeholder="Member" value="${row.index || ''}" min="1" max="16"> <select class="node-input-channelType"> <option value="">Channel type...</option> <option value="ai" ${row.channelType === 'ai' ? 'selected' : ''}>Analogue input (AI)</option> <option value="ao" ${row.channelType === 'ao' ? 'selected' : ''}>Analogue output (AO)</option> <option value="di" ${row.channelType === 'di' ? 'selected' : ''}>Discrete input (DI)</option> <option value="do" ${row.channelType === 'do' ? 'selected' : ''}>Discrete output (DO)</option> </select> <input type="number" class="node-input-coefficient" placeholder="Coef." value="${row.coefficient || ''}" step="0.01"> <input type="number" class="node-input-payload" placeholder="Payload" value="${row.payload || ''}" step="0.01"> </div>`; container.append(rowHtml); } // Get references to the created elements function getRowElements(container) { return { manualElement: container.find('.node-input-manual'), keySelectElement: container.find('.node-input-keyNameSelect'), keyManualElement: container.find('.node-input-keyNameManual'), indexElement: container.find('.node-input-index'), channelTypeElement: container.find('.node-input-channelType'), coefficientElement: container.find('.node-input-coefficient'), payloadElement: container.find('.node-input-payload'), topicElement: container.find('.node-input-topic') }; } // Format the newly created / intialized row's elements function formatRow(elements, row) { const filteredServices = _controller.filteredServices || {}; const writableServices = _controller.writableServices || {}; const finalFilteredServices = Object.keys(filteredServices).reduce((acc, key) => { if (writableServices[key]) { acc[key] = filteredServices[key]; } return acc; }, {}); const keySelectElement = elements.keySelectElement; const selectedKey = row.keyNameSelect; const filteredSvcKeys = sortKeysByServiceNames(finalFilteredServices); clearDropdown(keySelectElement, { text: "Select data stream..." }); // Populate the key selection dropdown with available keys filteredSvcKeys.forEach(key => { const serviceName = filteredServices[key]?.servicename || "???"; const text = `${serviceName} (${key})`; const el = $(`<option value="${key}" ${selectedKey === key ? 'selected' : ''}>${text}</option>`); keySelectElement.append(el); }); updateKeyName(elements); validateKeyName(elements); populateIndex(elements); validateIndex(elements); populateChannelType(elements); validateChannelType(elements); populatePayload(elements); validatePayload(elements); updateCoefficient(elements); validateCoefficient(elements); validateTopic(elements); } // Attach events to the row elements function attachRowEvents(elements = {}) { elements.manualElement.on('change', function () { updateKeyName(elements); validateKeyName(elements); populateIndex(elements); validateIndex(elements); updateCoefficient(elements); validateCoefficient(elements); }); elements.keySelectElement.on('change', function () { validateKeyName(elements); populateIndex(elements); validateIndex(elements); populateChannelType(elements); validateChannelType(elements); updateCoefficient(elements); validateCoefficient(elements); updateTopic(elements); validateTopic(elements); }); elements.keyManualElement.on('change', function () { populateIndex(elements); validateIndex(elements); }); elements.keyManualElement.on('input', function () { validateKeyName(elements); populateChannelType(elements); validateChannelType(elements); updateCoefficient(elements); validateCoefficient(elements); }); elements.channelTypeElement.on('change', function () { populateIndex(elements); validateIndex(elements); populatePayload(elements); validatePayload(elements); updateCoefficient(elements); validateCoefficient(elements); }); elements.indexElement.on('input', function () { validateIndex(elements); }); elements.coefficientElement.on('input', function () { validateCoefficient(elements); }); elements.channelTypeElement.on('change', function () { validateChannelType(elements); }); elements.topicElement.on('input', function () { validateTopic(elements); }); } // Hide the service key selection dropdown if manual is checked function updateKeyName(elements) { const manual = elements.manualElement.is(':checked'); if (manual) { elements.keySelectElement.val(""); elements.keySelectElement.hide(); elements.keyManualElement.show(); } else { elements.keyManualElement.val(""); elements.keySelectElement.show(); elements.keyManualElement.hide(); } } // Update coefficient field based on the selected key function updateCoefficient(elements) { const channelType = elements.channelTypeElement.val() ?? ""; const keyNameSelect = elements.keySelectElement.val(); const keyNameManual = elements.keyManualElement.val(); const filteredServices = _controller.filteredServices || {}; const keyName = keyNameSelect || keyNameManual; if (keyName && filteredServices[keyName]) { const convCoef = filteredServices[keyName]?.conv_coef || 1; elements.coefficientElement.val(convCoef); elements.coefficientElement.prop('disabled', true); } else { elements.coefficientElement.val("1"); elements.coefficientElement.prop('disabled', false); } if (channelType.startsWith("d")) { elements.coefficientElement.val(""); elements.coefficientElement.prop('disabled', true); } } // Update the index element based on the selected key function populateIndex(elements) { const keyNameSelect = elements.keySelectElement.val(); let el = elements.indexElement; const prevIndex = el.val(); // Check if element already has a data-id const existingId = el.attr('data-id'); // Use existing ID if available, otherwise generate a new one const id = existingId || Math.random().toString(36).substr(2, 9); const writableServices = _controller.writableServices || {}; if (keyNameSelect) { const indexes = writableServices[keyNameSelect] || []; const options = indexes.map(idx => { const desc = findValueBySvcNameAndMember(_controller.channels, keyNameSelect, idx, "desc"); return `<option value="${idx}" ${prevIndex == idx ? "selected" : ""}>${idx} ${desc ? `(${desc})` : ``}</option>` }) el.replaceWith(`<select data-id="${id}" class="node-input-index">${options.join('')}</select>`); } else { el.replaceWith(`<input data-id="${id}" type="number" class="node-input-index" placeholder="Member" value="${prevIndex}" min="1" max="16">`); } el = elements.indexElement = $(`.node-input-index[data-id="${id}"]`); // Update the reference using the custom attribute // Reattach the event listener to the new element el.on('change', function () { updateTopic(elements); validateTopic(elements); }); el.on('input', function () { validateIndex(elements); }); } // Populate the data stream type dropdown based on the selected key // Only show the types which are compatible with the selected key and index function populateChannelType(elements) { const channels = _controller.channels || {}; const keyNameSelect = elements.keySelectElement.val(); const keyNameManual = elements.keyManualElement.val(); const index = parseInt(elements.indexElement.val()); const el = elements.channelTypeElement; const prevType = el.val(); let options = [ { value: "ai", text: "Analogue input (AI)" }, { value: "ao", text: "Analogue output (AO)" }, { value: "di", text: "Discrete input (DI)" }, { value: "do", text: "Discrete output (DO)" } ]; // If the key is selected from the dropdown, find the compatible types if (!isNaN(index) && keyNameSelect && channels[keyNameSelect] && channels[keyNameSelect][index]) { const channel = channels[keyNameSelect]; const member = channel[index]; if (["s", "s!"].includes(member.regtype)) { if (member._type === "analogue") { options = [{ value: "ai", text: "Analogue input (AI)" }]; } else if (member._type === "discrete") { options = [{ value: "di", text: "Discrete input (DI)" }]; } else { options = [ { value: "ai", text: "Analogue input (AI)" }, { value: "di", text: "Discrete input (DI)" }, ]; } } else if (["h", "c"].includes(member.regtype) && member._output) { if (member._type === "analogue") { options = [{ value: "ao", text: "Analogue output (AO)" }]; } else if (member._type === "discrete") { options = [{ value: "do", text: "Discrete output (DO)" }]; } else { options = [ { value: "ao", text: "Analogue output (AO)" }, { value: "do", text: "Discrete output (DO)" } ]; } } } const prevTypeValid = options.map(opt => opt.value).includes(prevType); clearDropdown(el, { text: "Select data stream type...", disabled: true, selected: !prevTypeValid }); options.forEach(option => { el.append(`<option value="${option.value}" ${(prevType == option.value || options.length === 1) ? "selected" : ""}>${option.text}</option>`); }); } // Toggle the payload input based on channel type // Discrete data streams only accept 0 or 1 as payload // Analogue data streams accept any float value function populatePayload(elements) { const payloadType = $('#node-input-payloadType').val(); const channelType = elements.channelTypeElement.val() ?? ""; let el = elements.payloadElement; const prevPayload = el.val(); const id = Math.random().toString(36).substr(2, 9); if (channelType.startsWith("d") && payloadType !== "dynamic") { el.replaceWith( `<select data-id="${id}" class="node-input-payload"> <option value="0" ${prevPayload == 0 ? "selected" : ""}>0</option> <option value="1" ${prevPayload == 1 ? "selected" : ""}>1</option> </select>` ); } else { el.replaceWith(`<input data-id="${id}" type="number" class="node-input-payload" placeholder="Payload" value="${prevPayload}" step="0.01">`); } el = elements.payloadElement = $(`.node-input-payload[data-id="${id}"]`); // Update the reference using the custom attribute // Reattach the event listener to the new element el.on('input', function () { validatePayload(elements); }); if (payloadType === "dynamic") { el.val(""); el.prop('disabled', true); } } // Clear the data stream selection function clearDropdown(element, options = {}) { const { text = "none", disabled = false, selected = false } = options; element.empty(); element.append($(`<option value="" ${disabled ? 'disabled' : ''} ${selected ? 'selected' : ''}>${text}</option>`)); } // Update the topic element based on the selected key and member function updateTopic(elements) { const manual = elements.manualElement.is(':checked'); const keyNameSelect = elements.keySelectElement.val(); const index = elements.indexElement.val(); const topic = elements.topicElement.val(); if (manual || !keyNameSelect || !index) return; // Default topic format: "key.member" if (topic === "" || topicRelatedToService(topic)) { elements.topicElement.val(`${keyNameSelect}.${index}`); } } // Update tip text based on current settings function updateTipText() { const rows = getListElements(); const payloadType = $('#node-input-payloadType').val(); const topics = rows.map(row => row.topicElement.val()); const payloads = rows.map(row => row.payloadElement.val()); const topic1 = topics && topics.length > 0 ? topics[0] : "topic1"; const topic2 = topics && topics.length > 1 ? topics[1] : "topic2"; const payload1 = payloads && payloads.length > 0 ? payloads[0] : "payload1"; const payload2 = payloads && payloads.length > 1 ? payloads[1] : "payload2"; const tipText1 = `Routing ${rows.length} values from controller (${_controller.uniqueId || "???"})'s data streams.`; const tipText2 = `The payload will be multiplied by the coefficient (if applicable).`; const tipText3 = payloadType === "dynamic" ? `The required payload(s) will be searched from the incoming 'msg' object.` : `The payload(s) defined in the form fields above will be used when writing data.`; const tipText4 = `Only writable data streams and members are shown in the dropdown lists.`; const tipText5 = payloadType === "dynamic" ? `Incoming message formats: 1. {"topic": topic, "payload": value} to write a single stream 2. {"${topic1}": ${payload1}, "${topic2}": ${payload2}, ...} to write multiple streams` : `Incoming message formats: 1. {"topic": topic} to write a single stream 2. {"${topic1}": true, "${topic2}": false, ...} to write multiple streams 3. none, in which case every stream will be written.`; $("#node-input-tip-text-1").text(tipText1); $("#node-input-tip-text-2").text(tipText2); $("#node-input-tip-text-3").text(tipText3); $("#node-input-tip-text-4").text(tipText4); $("#node-input-tip-text-5").text(tipText5); } // Custom validation functions below function validateKeyName(elements) { console.debug("Validating key name..."); const keyNameSelect = elements.keySelectElement.val(); const keyNameManual = elements.keyManualElement.val(); const invalidValues = ["", undefined, null]; let valid; if (invalidValues.includes(keyNameSelect) && invalidValues.includes(keyNameManual)) { valid = false } else { valid = true } elements.keySelectElement.css('border-color', valid ? '' : 'red'); elements.keyManualElement.css('border-color', valid ? '' : 'red'); } function validateIndex(elements) { const index = elements.indexElement.val(); let valid; valid = isValidIndex(index); elements.indexElement.css('border-color', valid ? '' : 'red'); } function validateCoefficient(elements) { const coefficient = elements.coefficientElement.val(); const channelType = elements.channelTypeElement.val() ?? ""; let valid; valid = channelType.startsWith("d") || isValidFloat(coefficient); elements.coefficientElement.css('border-color', valid ? '' : 'red'); } function validateChannelType(elements) { const channelType = elements.channelTypeElement.val(); let valid; valid = isValidChannelType(channelType); elements.channelTypeElement.css('border-color', valid ? '' : 'red'); } function validatePayload(elements) { const payloadType = $('#node-input-payloadType').val(); const channelType = elements.channelTypeElement.val() ?? ""; const payload = elements.payloadElement.val(); let valid; if (channelType.startsWith("d")) { valid = ["0", "1"].includes(payload); } else { valid = isValidFloat(payload); } if (payloadType === "dynamic") { valid = true; } elements.payloadElement.css('border-color', valid ? '' : 'red'); } function validateTopic(elements) { const topic = elements.topicElement.val(); const invalidValues = ["", undefined, null]; let duplicateTopics = []; const rows = getListElements(); for (const elements of rows) { const t = elements.topicElement.val(); if (t === topic) duplicateTopics.push(t); } let valid; valid = !invalidValues.includes(topic) && duplicateTopics.length <= 1; elements.topicElement.css('border-color', valid ? '' : 'red'); } function isValidIndex(index) { const indexInt = parseInt(index); return Number.isInteger(indexInt) && indexInt >= 1 && indexInt <= 16; } function isValidFloat(value) { return !isNaN(parseFloat(value)); } function isValidChannelType(type) { return ["ai", "ao", "di", "do"].includes(type); } // Data stream helper functions below // Sort alphabetically, all keys starting with "_" are to be in the end function sortKeysByServiceNames(obj) { const keyNames = Object.keys(obj); const normalKeys = keyNames.filter(key => !obj[key]?.servicename.startsWith('_')); const underscoreKeys = keyNames.filter(key => obj[key]?.servicename.startsWith('_')); // Sort both arrays in ascending alphabetical order based on servicename normalKeys.sort((a, b) => obj[a]?.servicename?.localeCompare(obj[b]?.servicename)); underscoreKeys.sort((a, b) => obj[a]?.servicename?.localeCompare(obj[b]?.servicename)); return normalKeys.concat(underscoreKeys); } function findValueBySvcNameAndMember(channels = {}, name, member, key) { return channels?.[name]?.[member]?.[key] ?? null; } // Auto-generate mappings for all available services function autoGenerateMappings() { const writableServices = _controller.writableServices || {}; const filteredServices = _controller.filteredServices || {}; const keyNames = sortKeysByServiceNames(filteredServices); const newMappings = []; // Clear existing mappings first $('#mappings-container').editableList('empty'); // Create a mapping for each key and its members keyNames.forEach(keyName => { const service = filteredServices[keyName]; if (!service) return; // Get all members for this service key const members = _controller?.channels?.[keyName] || {}; // Add an entry for each member Object.values(members).forEach(member => { const idx = member.member; if (!writableServices?.[keyName]?.includes(parseInt(idx))) return; // Skip if not writable newMappings.push({ manual: false, keyNameSelect: keyName, keyNameManual: "", channelType: "", index: idx, coefficient: service?.conv_coef || 1, payload: "", topic: `${keyName}.${idx}` }); }); }); // Add all mappings to the list $('#mappings-container').editableList('addItems', newMappings); // Update ports and tip text updateTipText(); RED.notify("Generated " + newMappings.length + " data stream mappings", { type: "success", timeout: 3000 }); } // Iterate over each mapping row and store the elements function getListElements() { const result = []; $('#mappings-container').editableList('items').each(function () { const container = $(this); const elements = getRowElements(container); result.push(elements); }); return result; } }, oneditsave: function () { const node = this; node.name = $('#node-input-name').val(); node.controller = $("#node-input-controller").val(); node.outputMode = $('#node-input-outputMode').val(); node.payloadType = $('#node-input-payloadType').val(); node.mappings = getMappings(); // Iterate over each mapping row and store the values function getMappings() { const mappings = []; $('#mappings-container').editableList('items').each(function () { const container = $(this); const manual = container.find('.node-input-manual').is(':checked'); const keyNameSelect = container.find('.node-input-keyNameSelect').val(); const keyNameManual = container.find('.node-input-keyNameManual').val(); const channelType = container.find('.node-input-channelType').val(); const index = parseInt(container.find('.node-input-index').val()); const coefficient = parseFloat(container.find('.node-input-coefficient').val()); const payload = parseFloat(container.find('.node-input-payload').val()); const topic = container.find('.node-input-topic').val(); mappings.push({ manual, keyNameSelect, keyNameManual, channelType, index, coefficient, payload, topic }); }); return mappings; } } }); </script> <!-- Define style for the form fields --> <style type="text/css"> .write-static-data-streams-div .form-row { margin-bottom: 10px; } .red-ui-editableList-header { background-color: #80808014; font-weight: bold; display: flex; } .red-ui-editableList-container { min-height: 50px; } .write-static-data-streams-div .form-row label { width: 33% !important; vertical-align: middle; } .write-static-data-streams-div .form-row div, .write-static-data-streams-div .form-row input, .write-static-data-streams-div .form-row select { max-width: 66% !important; } .write-static-data-streams-div .form-row select { width: 66% !important; } .write-static-data-streams-div .form-tips { max-width: 100% !important; text-align: center; } /* Editable list style */ .write-static-data-streams-div .red-ui-editableList { margin-bottom: 10px; min-width: 700px; } .write-static-data-streams-list { overflow: hidden; white-space: nowrap; display: flex; align-items: center; } .write-static-data-streams-list .checkbox-container { text-align: center; min-width: 35px; width: 7%; } .write-static-data-streams-list .helper-text { font-size: 10px; line-height: 12px; text-wrap: wrap; color: #666; } .write-static-data-streams-list .node-input-manual { min-width: 16px; min-height: 16px; } .write-static-data-streams-list .node-input-keyNameManual { font-size: 12px !important; min-width: 150px; width: 30%; } .write-static-data-streams-list .node-input-keyNameSelect { font-size: 12px; min-width: 150px; width: 30%; } .write-static-data-streams-list .node-input-channelType { font-size: 12px; min-width: 100px; width: 15%; } .write-static-data-streams-list .node-input-index { font-size: 12px !important; min-width: 85px; width: 15%; } .write-static-data-streams-list .node-input-coefficient { font-size: 12px !important; min-width: 65px; width: 7%; } .write-static-data-streams-list .node-input-payload { font-size: 12px !important; min-width: 75px; width: 7%; } .write-static-data-streams-list .node-input-arrow { font-size: 18px; margin-left: 4px; margin-right: 4px; min-width: 20px; width: 4%; } .write-static-data-streams-list .node-input-topic { font-size: 12px !important; min-width: 100px; width: 15%; } </style> <!-- Form fields for the Data Stream Router node --> <script type="text/html" data-template-name="fusebox-write-static-data-streams"> <div class="write-static-data-streams-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-row"> <label for="node-input-controller"><i class="fa fa-search"></i> Controller</label> <select id="node-input-controller"> <option value="" disabled selected>Select controller...</option> </select> </div> <div class="form-row"> <label for="node-input-outputMode"><i class="fa fa-sign-out"></i> Write mode</label> <select id="node-input-outputMode"> <option value="" disabled selected>Select write mode...</option> <option value="all">Write data every time</option> <option value="change">Write data on change</option> </select> </div> <div class="form-row"> <label for="node-input-payloadType"><i class="fa fa-sign-out"></i> Payload type</label> <select id="node-input-payloadType"> <option value="" disabled>Select payload type...</option> <option value="static">Use form values</option> <option value="dynamic" selected>Use incoming 'msg' payload</option> </select> </div> <ol id="mappings-container"></ol> <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> <div class="form-tips" id="node-input-tip-text-4"></div> <div class="form-tips" id="node-input-tip-text-5" style="white-space: pre-line;"></div> <br> </div> </script> <!-- Define node description --> <script type="text/html" data-help-name="fusebox-write-static-data-streams"> <p> Write (save) the formatted values of one or more data streams back to the devices. Widely customizable node that can format data in various formats. </p> <p>Often used at the end of some flow to route a value back to the data stream.</p> <h3>Parameters</h3> <dl class="message-properties"> <dt class="optional">name <span class="property-type">string</span></dt> <dd>User-friendly name for the node</dd> <dt>controller <span class="property-type">controller</span></dt> <dd>The source of the data streams on the local network. Localhost (127.0.0.1) for most cases.</dd> <dt>write mode <span class="property-type">string</span></dt> <dd>Specifies the conditions when data will be saved. Selecting <code>change</code> will write data only when the data stream differs from the previous value.</dd> <dt>payload type <span class="property-type">string</span></dt> <dd>Specifies the source of payload values, either from the mapping fields or from the incoming <code>msg</code> object.</dd> <dt>mappings <span class="property-type">object</span></dt> <dd>Each row specifies a data stream member, channel type (analogue or discrete), and a topic to use when writing the message. Manual data stream entries are also allowed.</dd> </dl> <h3>Output</h3> <dl class="message-properties"> <dt>controller <span class="property-type">object</span></dt> <dd>Each output <code>msg</code> includes info about the source of the data, e.g. <code>uniqueId</code>, <code>host</code>, <code>protocol</code>.</dd> <dt>result <span class="property-type">boolean</span></dt> <dd><code>true</code> if the data was successfully written to the device, <code>false</code> otherwise.</dd> <dt>parameters <span class="property-type">object</span></dt> <dd>Contains info about the data stream which was written to the device, e.g. <code>data stream name</code>, <code>channel type</code>, <code>final payload</code>.</dd> </dl> <h3>Additional details</h3> <p>This node gets its data from the controller configuration and the global context.</p> <p>In order to preserve the unit of the defined data stream, the payload is multiplied by the coefficient (if applicable).</p> <p>The exact payload format depends on the message type. Example incoming messages:</p> <p> In case of <code>separate</code> message types: <code style="white-space: normal;">{"topic": string, "payload": float}</code>. <br> In case of <code>comprehensive</code> message type: <code style="white-space: normal;">{"topic1": float, "topic2": float, ...}</code>. </p> <p>Any other incoming <code>msg</code> properties will be preserved.</p> </script>