T3MP3ST public launch build

This commit is contained in:
t3mp3st contributors committed 2026-07-03 10:57:20 -04:00
1 parent 972247b881
commit 184c7d4e59
1589 files changed
+57150 -506

No files matched your search

@@ -0,0 +1,47 @@
--- server/api/command/index.js
@@ -78,7 +78,7 @@ module.exports = {
if (res.statusCode === 403) {
runtime.logger.error("api get getTagValue: Tocken Expired");
} else if (!authJwt.haveAdminPermission(permission) && !runtime.scriptsMgr.isAuthorisedByScriptName(req.query.sourceScriptName, permission)) {
- res.status(400).json({error:"unauthorized_error", message: "Unauthorized!"});
+ res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
runtime.logger.error("api get getTagValue: Unauthorized");
} else {
try {
--- server/integrations/node-red/index.js
@@ -152,12 +152,11 @@ async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger,
});
};
- // Allow public dashboard UI and socket.io; require JWT or API key for admin/editor/flows when security is enabled
+ // Allow only dashboard routes as public; require JWT or API key for admin/editor/flows when security is enabled
const allowDashboard = (req, res, next) => {
- const url = req.originalUrl || req.url || req.path;
-
- // Public dashboard UI and its HTTP APIs (served from httpNodeRoot/ui.path)
- if (url.includes('/dashboard') || url.includes('/socket.io')) return next();
+ // Public dashboard UI and its HTTP APIs (served from httpNodeRoot/ui.path).
+ // baseUrl comes from Express mount point and is not affected by query/path tricks.
+ if (req.baseUrl === '/dashboard') return next();
if (!settings.secureEnabled || settings.nodeRedAuthMode === 'legacy-open') {
return next();
--- server/runtime/scripts/index.js
@@ -106,7 +106,7 @@ function ScriptsManager(_runtime) {
this.isAuthorisedByScriptName = function (scriptName, permission) {
const script = scriptModule.getScriptByName(scriptName);
if (!script) {
- return true;
+ return false;
}
return this.isAuthorised(script, permission);
}
@@ -366,4 +366,4 @@ const ScriptSchedulingMode = {
const SchedulerType = {
weekly: 0,
date: 1,
-}
\ No newline at end of file
+}
@@ -0,0 +1 @@
{"ghsa": "GHSA-rg3m-cfq7-g6h6", "cve": "CVE-2026-43947", "repo": "frangoteam/FUXA", "severity": "high", "cwes": ["CWE-863"], "summary": "FUXA Vulnerable to Unauthenticated Remote Code Execution via Script Test Mode Authorization Bypass", "fix_commit": "78534da61a91613712b44bb63c8d7da8c5df5ca4", "parent_commit": "61b5cf41af63a60749ac39a285be66b693d19b94", "vuln_files": ["server/api/command/index.js", "server/integrations/node-red/index.js"], "vuln_lines": {"server/api/command/index.js": [81], "server/integrations/node-red/index.js": [155, 157, 158, 159, 160]}, "published": "2026-05-26T23:44:52Z", "source_files": 40, "scope_dirs": ["server/api/command", "server/integrations/node-red", "server/runtime/scripts"]}
@@ -0,0 +1,164 @@
/**
* 'api/command': Command API to process command like build Report
*/
var express = require("express");
const authJwt = require('../jwt-helper');
const fs = require('fs');
const path = require('path');
const { resolveWithin } = require('../path-helper');
var runtime;
var secureFnc;
var checkGroupsFnc;
module.exports = {
init: function (_runtime, _secureFnc, _checkGroupsFnc) {
runtime = _runtime;
secureFnc = _secureFnc;
checkGroupsFnc = _checkGroupsFnc;
},
app: function () {
var commandApp = express();
commandApp.use(function (req, res, next) {
if (!runtime.project) {
res.status(404).end();
} else {
next();
}
});
/**
* GET download
*/
commandApp.get('/api/download', secureFnc, function(req, res){
const permission = checkGroupsFnc(req);
if (res.statusCode === 403) {
runtime.logger.error("api post command: Tocken Expired");
} else if (!authJwt.haveAdminPermission(permission)) {
res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
runtime.logger.error("api post command: Unauthorized");
} else {
if (req.query.cmd === CommanTypeEnum.reportDownload) {
try {
var reportBase = runtime.settings.reportsDir;
if (!fs.existsSync(reportBase)) {
reportBase = path.join(process.cwd(), runtime.settings.reportsDir);
}
const resolvedReport = resolveWithin(reportBase, req.query.name);
if (!resolvedReport) {
res.status(400).json({ error: "invalid_path", message: "Invalid report file." });
return;
}
const reportPath = resolvedReport.resolvedTarget;
if (fs.existsSync(reportPath)) {
res.sendFile(reportPath, (err) => {
if (err) {
runtime.logger.error("api get download: " + err);
}
});
} else {
res.status(400).json({ error: "not_found", message: 'report not found!'});
runtime.logger.error("api get download: " + 'report not found!');
}
} catch (error) {
res.status(400).json({ error: 'error', message: error});
runtime.logger.error("api get download: " + error);
}
} else {
res.status(400).json({ error: "not_found", message: 'command not found!'});
}
}
});
/**
* GET get tags values
*/
commandApp.get("/api/getTagValue", secureFnc, async function (req, res, next) {
const permission = checkGroupsFnc(req);
if (res.statusCode === 403) {
runtime.logger.error("api get getTagValue: Tocken Expired");
} else if (!authJwt.haveAdminPermission(permission) && !runtime.scriptsMgr.isAuthorisedByScriptName(req.query.sourceScriptName, permission)) {
res.status(400).json({error:"unauthorized_error", message: "Unauthorized!"});
runtime.logger.error("api get getTagValue: Unauthorized");
} else {
try {
var tagsIds = JSON.parse(req.query.ids);
if (tagsIds) {
var errors = '';
var result = [];
for (const tagId of tagsIds) {
try {
const value = runtime.devices.getTagValue(tagId, true);
if (value) {
result.push(value);
} else {
result.push({id: tagId, value: null});
errors += `${tagId}; `;
}
} catch (err) {
errors += `${tagId}: ${err}`;
}
}
if (errors) {
runtime.logger.error("api get getTagValue: " + 'id not found!' + errors);
}
res.json(result);
} else {
res.status(400).json({ error: "not_found", message: 'tag id not found!'});
runtime.logger.error("api get getTagValue: " + 'id not found!');
}
} catch (error) {
res.status(400).json({ error: "error", message: error});
runtime.logger.error("api get getTagValue: " + error);
}
}
});
/**
* POST set tags values
*/
commandApp.post("/api/setTagValue", secureFnc, async function (req, res, next) {
const permission = checkGroupsFnc(req);
if (res.statusCode === 403) {
runtime.logger.error("api post setTagValue: Tocken Expired");
} else if (!authJwt.haveAdminPermission(permission)) {
res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
runtime.logger.error("api post setTagValue: Unauthorized");
} else {
try {
if (req.body.tags) {
var errors = '';
for (const tag of req.body.tags) {
try {
if (!await runtime.devices.setTagValue(tag.id, tag.value)) {
errors += `${tag.id} not found; `
}
} catch (err) {
errors += `${tag.id}: ${err}`;
}
}
if (errors) {
res.status(400).json({ error: "not_found", message: 'setTagValue Failed: ' + errors});
runtime.logger.error("api post setTagValue Failed:" + errors);
} else {
res.end();
}
} else {
res.status(400).json({ error: "not_found", message: 'no tags to set!'});
runtime.logger.error("api post setTagValue: " + 'id not found!');
}
} catch (error) {
res.status(400).json({ error: "error", message: error});
runtime.logger.error("api post setTagValue: " + error);
}
}
});
return commandApp;
}
}
const CommanTypeEnum = {
reportDownload: 'REPORT-DOWNLOAD'
};
@@ -0,0 +1,286 @@
// server/integrations/node-red/index.js
// Optional Node-RED integration: mount only if the package is installed.
const fs = require('fs');
const path = require('path');
function tryRequireNodeRed() {
try {
// Fast check whether the module is resolvable
require.resolve('node-red');
// Load it only after resolve succeeds
// (keeps main process clean when not installed)
// eslint-disable-next-line global-require
return require('node-red');
} catch {
return null;
}
}
async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger, authJwt, events }) {
const RED = tryRequireNodeRed();
if (!RED) {
logger.info('[Node-RED] Package not installed. Skipping Node-RED initialization.');
// Do not register any /nodered routes; default 404 is fine.
return;
}
// Prepare Node-RED userDir for flows and settings
const userDir = path.join(settings.workDir, 'node-red');
if (!fs.existsSync(userDir)) {
fs.mkdirSync(userDir, { recursive: true });
}
// Minimal Node-RED settings; extend only what is really needed
const redSettings = {
httpAdminRoot: '/nodered/',
// Serve Node-RED HTTP nodes under /dashboard to avoid intercepting FUXA routes
httpNodeRoot: '/dashboard',
userDir,
nodesDir: [path.join(__dirname, 'node-red-contrib-fuxa')],
flowFile: 'flows.json',
editorTheme: {
notifications: { enabled: false },
tours: { enabled: false },
},
// Dashboard will be exposed at /dashboard/...
ui: { path: '/' },
// Values used by FlowFuse dashboard's ui_base.js for layout saves
// These mirror the FUXA HTTP bind address so Node-RED can call its own /nodered/flows API
uiHost: settings.uiHost,
uiPort: settings.uiPort,
functionGlobalContext: {
// Expose essential FUXA runtime helpers
fuxa: {
runtime,
getTag: require(path.join(settings.appDir, 'runtime/devices')).getTagValue,
setTag: require(path.join(settings.appDir, 'runtime/devices')).setTagValue,
getDaq: require(path.join(settings.appDir, 'runtime/storage/daqstorage')).getNodeValues,
getTagId: require(path.join(settings.appDir, 'runtime/devices')).getTagId,
getHistoricalTags: require(path.join(settings.appDir, 'runtime/devices')).getHistoricalTags,
emit: events.emit.bind(events),
on: events.on.bind(events),
removeListener: events.removeListener.bind(events),
sendMessage: async (address, subject, message) =>
await runtime.notificatorMgr.sendMailMessage(null, address, subject, message, null, null),
getAlarms: async () => await runtime.alarmsMgr.getAlarmsValues(null, -1),
getHistoryAlarms: async (start, end) => {
const query = { start, end };
return runtime.alarmsMgr.getAlarmsHistory(query, -1);
},
ackAlarm: async (alarmName, types) => {
const utils = require(path.join(settings.appDir, 'runtime/utils'));
const sep = runtime.alarmsMgr.getIdSeparator();
if (alarmName.indexOf(sep) === -1 && !utils.isNullOrUndefined(types)) {
const results = [];
for (const t of types) {
const alarmId = `${alarmName}${sep}${t}`;
results.push(await runtime.alarmsMgr.setAlarmAck(alarmId, null, -1));
}
return results;
}
return runtime.alarmsMgr.setAlarmAck(alarmName, null, -1);
},
getScripts: async () => {
const scripts = await runtime.project.getScripts();
return scripts ? scripts.map(s => ({ id: s.id, name: s.name })) : [];
},
runScript: async (scriptName, params) => {
const scripts = await runtime.project.getScripts();
const script = scripts?.find(s => s.name === scriptName);
if (!script) throw new Error(`Script '${scriptName}' not found`);
return runtime.scriptsMgr.runScript(script, null, params);
},
},
// Common Node.js stdlib modules handy in flows
fs: require('fs').promises,
fsSync: require('fs'),
path: require('path'),
util: require('util'),
os: require('os'),
http: require('http'),
https: require('https'),
dgram: require('dgram'),
dns: require('dns'),
url: require('url'),
querystring: require('querystring'),
crypto: require('crypto'),
zlib: require('zlib'),
stream: require('stream'),
events: require('events'),
buffer: require('buffer'),
sqlite3: require('sqlite3'),
serialport: require('serialport'),
// Dangerous modules are opt-in only (see settings.nodeRedUnsafeModules)
...(settings.nodeRedUnsafeModules ? {
child_process: require('child_process'),
net: require('net'),
} : {}),
},
};
// Initialize Node-RED on the existing HTTP server (must be done before server.listen)
RED.init(server, redSettings);
const getCookieValue = (req, name) => {
const cookieHeader = req.headers.cookie;
if (!cookieHeader) return null;
const cookies = cookieHeader.split(';');
for (const cookie of cookies) {
const [key, ...rest] = cookie.trim().split('=');
if (key === name) {
return rest.join('=');
}
}
return null;
};
const verifyApiKey = (runtimeRef, apiKey) => {
return runtimeRef.apiKeys.getApiKeys().then(stored => {
const now = Date.now();
return stored.find(k => {
if (!k || k.key !== apiKey || k.enabled === false) {
return false;
}
if (!k.expires) {
return true;
}
const expiresAt = new Date(k.expires).getTime();
return !isNaN(expiresAt) && expiresAt > now;
});
});
};
// Allow public dashboard UI and socket.io; require JWT or API key for admin/editor/flows when security is enabled
const allowDashboard = (req, res, next) => {
const url = req.originalUrl || req.url || req.path;
// Public dashboard UI and its HTTP APIs (served from httpNodeRoot/ui.path)
if (url.includes('/dashboard') || url.includes('/socket.io')) return next();
if (!settings.secureEnabled || settings.nodeRedAuthMode === 'legacy-open') {
return next();
}
const apiKey = req.headers['x-api-key'];
if (apiKey) {
return verifyApiKey(runtime, apiKey)
.then(validKey => {
if (!validKey) {
return res.status(401).json({ error: "unauthorized_error", message: "Invalid API Key" });
}
return next();
})
.catch(err => {
logger.error(`api-key validation failed: ${err}`);
return res.status(500).json({ error: "unexpected_error", message: "ApiKey validation failed" });
});
}
const headerToken = req.headers['x-access-token'];
const queryToken = req.query?.token;
const cookieToken = getCookieValue(req, 'nodered_auth');
// Prefer explicit tokens over cookie to avoid stale cookie blocking valid logins
const token = headerToken || queryToken || cookieToken;
if (!token) {
return res.status(401).json({ error: "unauthorized_error", message: "Authentication required!" });
}
return authJwt.verify(token)
.then(() => {
if (queryToken) {
res.cookie('nodered_auth', token, {
httpOnly: true,
sameSite: 'lax',
secure: !!settings.https,
});
if (req.method === 'GET') {
const cleanUrl = new URL(req.originalUrl, `http://${req.headers.host}`);
cleanUrl.searchParams.delete('token');
return res.redirect(cleanUrl.pathname + cleanUrl.search);
}
}
return next();
})
.catch(() => {
if (cookieToken) {
res.clearCookie('nodered_auth');
}
return res.status(401).json({ error: "unauthorized_error", message: "Invalid token!" });
});
};
// Mount Node-RED admin/editor under /nodered; HTTP nodes (including dashboard)
// are served from httpNodeRoot ('/dashboard') so they appear at /dashboard/... etc.
app.use('/nodered', allowDashboard, RED.httpAdmin);
app.use('/dashboard', allowDashboard, RED.httpNode);
await RED.start();
RED.httpAdmin.get('/fuxa/devices', function(req, res) {
const devices = runtime.project.getDevices();
const result = [];
for (const id in devices) {
const device = devices[id];
const tags = [];
for (const tagId in device.tags) {
tags.push({ id: tagId, name: device.tags[tagId].name });
}
result.push({ id: device.id, name: device.name, tags });
}
res.json(result);
});
RED.httpAdmin.get('/fuxa/scripts', async function(req, res) {
try {
const scripts = await runtime.project.getScripts();
const result = scripts ? scripts.map(script => ({ id: script.id, name: script.name })) : [];
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
RED.httpAdmin.get('/fuxa/views', async function(req, res) {
try {
const project = await runtime.project.getProject();
const result = project && project.hmi && project.hmi.views ?
project.hmi.views.map(view => ({ id: view.id, name: view.name })) : [];
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
RED.httpAdmin.get('/fuxa/alarms', async function(req, res) {
try {
const alarms = await runtime.project.getAlarms();
const result = alarms ?
alarms.map(alarm => ({ id: alarm.id, name: alarm.name })) : [];
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// set api to listen (moved here to avoid rate limiting Node-RED routes)
if (settings.disableServer !== false) {
// Catch-all route for SPA - serve Angular index.html for client routes
// Exclude API routes and static assets
app.get('*', (req, res, next) => {
// Skip API routes and static assets
if (req.path.startsWith('/api/') ||
req.path.includes('.') ||
req.path.startsWith('/nodered') ||
req.path.startsWith('/dashboard')) {
return next();
}
res.sendFile(path.join(settings.httpStatic, 'index.html'));
});
}
logger.info('[Node-RED] Started at /nodered');
}
module.exports = { mountNodeRedIfInstalled };
@@ -0,0 +1 @@
// This file can be empty if using node-red.nodes in package.json
@@ -0,0 +1,49 @@
<script type="text/javascript">
RED.nodes.registerType('ack-alarm',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
alarmName: {value:"", required:true},
types: {value:""}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||"ack alarm";
},
oneditprepare: function() {
$.getJSON('/nodered/fuxa/alarms', function(data) {
var datalist = $('#fuxa-alarms');
datalist.empty();
data.forEach(function(alarm) {
datalist.append('<option value="' + alarm.name + '">' + alarm.name + '</option>');
});
});
}
});
</script>
<script type="text/x-red" data-template-name="ack-alarm">
<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-alarmName"><i class="fa fa-bell"></i> Alarm Name</label>
<input type="text" id="node-input-alarmName" list="fuxa-alarms" placeholder="Alarm name">
<datalist id="fuxa-alarms"></datalist>
</div>
<div class="form-row">
<label for="node-input-types"><i class="fa fa-list"></i> Types (comma separated)</label>
<input type="text" id="node-input-types" placeholder="Alarm types (optional)">
</div>
</script>
<script type="text/x-red" data-help-name="ack-alarm">
<p>Acknowledge an alarm in FUXA.</p>
<p>Use <code>alarmName</code> and optional <code>types</code> array.</p>
<p>Use <code>msg.alarmName</code>, <code>msg.types</code>, or <code>msg.payload</code> to override.</p>
<p><strong>Available alarms:</strong> Dropdown populated from FUXA project alarms.</p>
</script>
@@ -0,0 +1,27 @@
module.exports = function(RED) {
function FuxaAckAlarmNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var alarmName = config.alarmName || msg.alarmName || msg.payload;
var types = config.types || msg.types;
if (typeof types === 'string') {
types = types.split(',').map(s => s.trim());
}
if (alarmName) {
var result = await fuxa.ackAlarm(alarmName, types);
msg.payload = result;
node.send(msg);
} else {
node.error('Missing alarm name', msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("ack-alarm", FuxaAckAlarmNode);
}
@@ -0,0 +1,31 @@
<script type="text/javascript">
RED.nodes.registerType('emit-event',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
eventType: {value:"", required:true}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||this.eventType||"emit event";
}
});
</script>
<script type="text/x-red" data-template-name="emit-event">
<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-eventType"><i class="fa fa-bolt"></i> Event Type</label>
<input type="text" id="node-input-eventType" placeholder="Event type">
</div>
</script>
<script type="text/x-red" data-help-name="emit-event">
<p>Emit a FUXA event with <code>msg.payload</code> as data.</p>
</script>
@@ -0,0 +1,17 @@
module.exports = function(RED) {
function FuxaEmitEventNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', function(msg) {
try {
fuxa.emit(config.eventType, msg.payload);
node.send(msg);
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("emit-event", FuxaEmitEventNode);
}
@@ -0,0 +1,49 @@
<script type="text/javascript">
RED.nodes.registerType('enable-device',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
deviceName: {value:"", required:true},
enabled: {value:true}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||this.deviceName||"enable device";
},
oneditprepare: function() {
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-devices-enable');
datalist.empty();
data.forEach(function(device) {
datalist.append('<option value="' + device.name + '">' + device.name + '</option>');
});
});
}
});
</script>
<script type="text/x-red" data-template-name="enable-device">
<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-deviceName"><i class="fa fa-cogs"></i> Device Name</label>
<input type="text" id="node-input-deviceName" list="fuxa-devices-enable" placeholder="Device Name">
<datalist id="fuxa-devices-enable"></datalist>
</div>
<div class="form-row">
<label for="node-input-enabled">&nbsp;</label>
<input type="checkbox" id="node-input-enabled" style="display:inline-block; width:15px; vertical-align:baseline;" checked>
<span>Enabled</span>
</div>
</script>
<script type="text/x-red" data-help-name="enable-device">
<p>Enable or disable a FUXA device.</p>
<p>The result is set to <code>msg.payload</code>.</p>
<p>Use <code>msg.deviceName</code> and <code>msg.enabled</code> to override the configured values.</p>
</script>
@@ -0,0 +1,25 @@
module.exports = function(RED) {
function FuxaEnableDeviceNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var deviceName = config.deviceName || msg.deviceName;
var enabled = (config.enabled !== undefined) ? config.enabled : (msg.enabled !== undefined ? msg.enabled : true);
if (deviceName) {
var result = await fuxa.enableDevice(deviceName, enabled);
msg.payload = result;
node.send(msg);
} else {
node.error('Device name not specified', msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("enable-device", FuxaEnableDeviceNode);
}
@@ -0,0 +1,50 @@
<script type="text/javascript">
RED.nodes.registerType('execute-script',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
script: {value:"", required:true}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||this.script||"execute script";
},
oneditprepare: function() {
$.getJSON('/nodered/fuxa/scripts', function(data) {
var datalist = $('#fuxa-scripts');
datalist.empty();
data.forEach(function(script) {
datalist.append('<option value="' + script.name + '">' + script.name + '</option>');
});
});
}
});
</script>
<script type="text/x-red" data-template-name="execute-script">
<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-script"><i class="fa fa-code"></i> Script</label>
<input type="text" id="node-input-script" list="fuxa-scripts" placeholder="Script Name">
<datalist id="fuxa-scripts"></datalist>
</div>
</script>
<script type="text/x-red" data-help-name="execute-script">
<p>Execute a FUXA script.</p>
<p><strong>Available scripts:</strong> Dropdown populated from FUXA project scripts.</p>
<p>The output <code>msg.payload</code> contains execution status:</p>
<ul>
<li><code>success</code>: boolean indicating if execution succeeded</li>
<li><code>script</code>: name of the executed script</li>
<li><code>result</code>: script return value (on success)</li>
<li><code>error</code>: error message (on failure)</li>
<li><code>timestamp</code>: execution timestamp</li>
</ul>
</script>
@@ -0,0 +1,49 @@
module.exports = function(RED) {
function FuxaExecuteScriptNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', function(msg) {
try {
var scriptName = config.script;
var params = msg.payload || {};
if (scriptName) {
fuxa.runScript(scriptName, params).then(function(result) {
msg.payload = {
success: true,
script: scriptName,
result: result,
timestamp: new Date().toISOString()
};
node.send(msg);
}).catch(function(err) {
msg.payload = {
success: false,
script: scriptName,
error: err.message,
timestamp: new Date().toISOString()
};
node.send(msg);
});
} else {
msg.payload = {
success: false,
error: 'No script selected',
timestamp: new Date().toISOString()
};
node.send(msg);
}
} catch (err) {
msg.payload = {
success: false,
error: err.message,
timestamp: new Date().toISOString()
};
node.send(msg);
}
});
}
RED.nodes.registerType("execute-script", FuxaExecuteScriptNode);
}
@@ -0,0 +1,27 @@
<script type="text/javascript">
RED.nodes.registerType('get-alarms',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||"get alarms";
}
});
</script>
<script type="text/x-red" data-template-name="get-alarms">
<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>
</script>
<script type="text/x-red" data-help-name="get-alarms">
<p>Get current alarms from FUXA.</p>
<p>The alarms array is set to <code>msg.payload</code>.</p>
</script>
@@ -0,0 +1,19 @@
module.exports = function(RED) {
function FuxaGetAlarmsNode(config) {
RED.nodes.createNode(this, config);
var node = this;
// Access FUXA functions from global context
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var alarms = await fuxa.getAlarms();
msg.payload = alarms;
node.send(msg);
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("get-alarms", FuxaGetAlarmsNode);
}
@@ -0,0 +1,151 @@
<script type="text/javascript">
RED.nodes.registerType('get-daq',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
tag: {value:""}, // Keep: tag name for display and backward compatibility
tagId: {value:""}, // New: unique identifier (optional, for backward compatibility)
from: {value:""},
to: {value:""}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
if (this.name) {
return this.name;
}
// Display tag name or tagId
return this.tag || this.tagId || "get daq";
},
oneditprepare: function() {
var node = this;
var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-tags-daq');
datalist.empty();
data.forEach(function(device) {
device.tags.forEach(function(tag) {
var fullName = device.name + ' - ' + tag.name;
// datalist option value format: tag.name(tag.id)
var optionValue = tag.name + '(' + tag.id + ')';
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
tagMap[tag.id] = {
name: tag.name,
deviceName: device.name,
fullName: fullName
};
});
});
// Listen for tagSelected input changes
$('#node-input-tagSelected').on('change', function() {
var selectedValue = $(this).val();
// Parse format: tag.name(tag.id)
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
if (match) {
var tagName = match[1];
var tagId = match[2];
// Set the actual stored fields
$('#node-input-tag').val(tagName);
$('#node-input-tagId').val(tagId);
}
});
// Initialize display (when editing existing node)
if (node.tagId && tagMap[node.tagId]) {
// Has tagId, construct tagSelected value
var tagName = node.tag || tagMap[node.tagId].name;
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
} else if (node.tag && !node.tagId) {
// Old node: only has tag (tag.name), need to find corresponding tagId
// Note: if there are duplicate names, only the first match will be found
for (var tagId in tagMap) {
if (tagMap[tagId].name === node.tag) {
$('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
$('#node-input-tagId').val(tagId);
break;
}
}
}
});
// Convert timestamp format for datetime-local inputs
function formatForInput(timestamp) {
if (!timestamp) return '';
// Convert from "YYYY-MM-DD HH:MM:SS" to "YYYY-MM-DDTHH:MM"
return timestamp.replace(' ', 'T').substring(0, 16);
}
function formatForStorage(datetimeLocal) {
if (!datetimeLocal) return '';
// Convert from "YYYY-MM-DDTHH:MM" to "YYYY-MM-DD HH:MM:SS"
return datetimeLocal.replace('T', ' ') + ':00';
}
// Set initial values
$("#node-input-from").val(formatForInput(this.from));
$("#node-input-to").val(formatForInput(this.to));
// Handle changes to convert back to storage format
$("#node-input-from").on('change', function() {
$("#node-input-from").data('stored-value', formatForStorage($(this).val()));
});
$("#node-input-to").on('change', function() {
$("#node-input-to").data('stored-value', formatForStorage($(this).val()));
});
},
oneditsave: function() {
// Save the converted values
this.from = $("#node-input-from").data('stored-value') || $("#node-input-from").val();
this.to = $("#node-input-to").data('stored-value') || $("#node-input-to").val();
}
});
</script>
<script type="text/x-red" data-template-name="get-daq">
<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-tagSelected"><i class="fa fa-tag"></i> Tag</label>
<input type="text" id="node-input-tagSelected" list="fuxa-tags-daq" placeholder="Select Tag">
<datalist id="fuxa-tags-daq"></datalist>
</div>
<input type="hidden" id="node-input-tag">
<input type="hidden" id="node-input-tagId">
<div class="form-row">
<label for="node-input-from"><i class="fa fa-clock-o"></i> From (timestamp)</label>
<input type="datetime-local" id="node-input-from" placeholder="From timestamp (optional)">
</div>
<div class="form-row">
<label for="node-input-to"><i class="fa fa-clock-o"></i> To (timestamp)</label>
<input type="datetime-local" id="node-input-to" placeholder="To timestamp (optional)">
</div>
</script>
<script type="text/x-red" data-help-name="get-daq">
<p>Get DAQ data for a FUXA tag between timestamps.</p>
<p>The data array is set to <code>msg.payload</code>.</p>
<h4>Timestamp Configuration</h4>
<p><strong>Static timestamps</strong> can be set in the node configuration.</p>
<p><strong>Dynamic timestamps</strong> can override the configured values:</p>
<ul>
<li><code>msg.from</code> - Start timestamp (Unix timestamp or ISO string)</li>
<li><code>msg.to</code> - End timestamp (Unix timestamp or ISO string)</li>
</ul>
<p>If no timestamps are provided, defaults to the last hour.</p>
<h4>Examples</h4>
<pre>// Get data for last 24 hours
msg.from = Date.now() - 86400000;
msg.to = Date.now();
// Get data for specific date range
msg.from = "2025-01-01 00:00:00";
msg.to = "2025-01-01 23:59:59";</pre>
</script>
@@ -0,0 +1,31 @@
module.exports = function(RED) {
function FuxaGetDaqNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
// Prefer config.tagId, fallback to config.tag for backward compatibility
var tagId = config.tagId;
if (!tagId && config.tag) {
// Backward compatibility: old nodes use tag.name, need to convert to tagId
tagId = fuxa.getTagId(config.tag, null);
}
if (tagId) {
var fromts = config.from || msg.from || Date.now() - 3600000; // default 1 hour ago
var tots = config.to || msg.to || Date.now();
var data = await fuxa.getDaq(tagId, fromts, tots);
msg.payload = data;
node.send(msg);
} else {
node.error('Tag not found: ' + (config.tag || config.tagId), msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("get-daq", FuxaGetDaqNode);
}
@@ -0,0 +1,48 @@
<script type="text/javascript">
RED.nodes.registerType('get-device-property',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
deviceName: {value:"", required:true},
property: {value:"", required:true}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||this.deviceName||"get device property";
},
oneditprepare: function() {
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-devices-property');
datalist.empty();
data.forEach(function(device) {
datalist.append('<option value="' + device.name + '">' + device.name + '</option>');
});
});
}
});
</script>
<script type="text/x-red" data-template-name="get-device-property">
<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-deviceName"><i class="fa fa-cogs"></i> Device Name</label>
<input type="text" id="node-input-deviceName" list="fuxa-devices-property" placeholder="Device Name">
<datalist id="fuxa-devices-property"></datalist>
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-info-circle"></i> Property</label>
<input type="text" id="node-input-property" placeholder="Property name">
</div>
</script>
<script type="text/x-red" data-help-name="get-device-property">
<p>Get a property value from a FUXA device.</p>
<p>The property value is set to <code>msg.payload</code>.</p>
<p>Use <code>msg.deviceName</code> and <code>msg.property</code> to override the configured values.</p>
</script>
@@ -0,0 +1,25 @@
module.exports = function(RED) {
function FuxaGetDevicePropertyNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var deviceName = config.deviceName || msg.deviceName;
var property = config.property || msg.property;
if (deviceName && property) {
var result = await fuxa.getDeviceProperty(deviceName, property);
msg.payload = result;
node.send(msg);
} else {
node.error('Device name and property not specified', msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("get-device-property", FuxaGetDevicePropertyNode);
}
@@ -0,0 +1,49 @@
<script type="text/javascript">
RED.nodes.registerType('get-device',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
deviceName: {value:"", required:true},
includeTags: {value:true}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||this.deviceName||"get device";
},
oneditprepare: function() {
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-devices-get');
datalist.empty();
data.forEach(function(device) {
datalist.append('<option value="' + device.name + '">' + device.name + '</option>');
});
});
}
});
</script>
<script type="text/x-red" data-template-name="get-device">
<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-deviceName"><i class="fa fa-cogs"></i> Device Name</label>
<input type="text" id="node-input-deviceName" list="fuxa-devices-get" placeholder="Device Name">
<datalist id="fuxa-devices-get"></datalist>
</div>
<div class="form-row">
<label for="node-input-includeTags">&nbsp;</label>
<input type="checkbox" id="node-input-includeTags" style="display:inline-block; width:15px; vertical-align:baseline;" checked>
<span>Include Tags</span>
</div>
</script>
<script type="text/x-red" data-help-name="get-device">
<p>Get information about a FUXA device.</p>
<p>The device object is set to <code>msg.payload</code>.</p>
<p>Use <code>msg.deviceName</code> and <code>msg.includeTags</code> to override the configured values.</p>
</script>
@@ -0,0 +1,25 @@
module.exports = function(RED) {
function FuxaGetDeviceNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var deviceName = config.deviceName || msg.deviceName;
var includeTags = (config.includeTags !== undefined) ? config.includeTags : (msg.includeTags !== undefined ? msg.includeTags : true);
if (deviceName) {
var device = await fuxa.getDevice(deviceName, includeTags);
msg.payload = device;
node.send(msg);
} else {
node.error('Device name not specified', msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("get-device", FuxaGetDeviceNode);
}
@@ -0,0 +1,152 @@
<script type="text/javascript">
RED.nodes.registerType('get-historical-tags',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
tags: {value:"[]"},
from: {value:""},
to: {value:""}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||"get historical tags";
},
oneditprepare: function() {
var node = this;
// Load available tags for autocomplete
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-tags-historical');
datalist.empty();
data.forEach(function(device) {
device.tags.forEach(function(tag) {
datalist.append('<option value="' + tag.name + '">' + device.name + ' - ' + tag.name + '</option>');
});
});
});
// Convert timestamp format for datetime-local inputs
function formatForInput(timestamp) {
if (!timestamp) return '';
// Convert from "YYYY-MM-DD HH:MM:SS" to "YYYY-MM-DDTHH:MM"
return timestamp.replace(' ', 'T').substring(0, 16);
}
function formatForStorage(datetimeLocal) {
if (!datetimeLocal) return '';
// Convert from "YYYY-MM-DDTHH:MM" to "YYYY-MM-DD HH:MM:SS"
return datetimeLocal.replace('T', ' ') + ':00';
}
// Set initial values for timestamps
$("#node-input-from").val(formatForInput(this.from));
$("#node-input-to").val(formatForInput(this.to));
// Handle timestamp changes
$("#node-input-from").on('change', function() {
$("#node-input-from").data('stored-value', formatForStorage($(this).val()));
});
$("#node-input-to").on('change', function() {
$("#node-input-to").data('stored-value', formatForStorage($(this).val()));
});
// Initialize tags container
var tagsContainer = $('#tags-container');
var tags = [];
try {
tags = JSON.parse(this.tags || "[]");
} catch(e) {
// Fallback for old comma-separated format
if (this.tags && typeof this.tags === 'string') {
tags = this.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
}
}
// Function to add a tag input
function addTagInput(tagValue) {
var tagRow = $('<div class="tag-row" style="display: flex; align-items: center; margin-bottom: 5px;">');
var tagInput = $('<input type="text" class="tag-input" list="fuxa-tags-historical" placeholder="Tag Name" style="flex: 1; margin-right: 5px;">');
var removeBtn = $('<button type="button" class="red-ui-button red-ui-button-small" style="min-width: 20px; padding: 0 5px;">-</button>');
tagInput.val(tagValue || '');
tagRow.append(tagInput);
tagRow.append(removeBtn);
tagsContainer.append(tagRow);
// Remove button handler
removeBtn.on('click', function() {
tagRow.remove();
});
}
// Add existing tags
if (tags.length === 0) {
addTagInput(''); // Add at least one empty input
} else {
tags.forEach(function(tag) {
addTagInput(tag);
});
}
// Add tag button handler
$('#add-tag-btn').on('click', function() {
addTagInput('');
});
},
oneditsave: function() {
// Save timestamp values
this.from = $("#node-input-from").data('stored-value') || $("#node-input-from").val();
this.to = $("#node-input-to").data('stored-value') || $("#node-input-to").val();
// Collect all tag inputs
var tags = [];
$('.tag-input').each(function() {
var tagValue = $(this).val().trim();
if (tagValue) {
tags.push(tagValue);
}
});
this.tags = JSON.stringify(tags);
}
});
</script>
<script type="text/x-red" data-template-name="get-historical-tags">
<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="tags-container"><i class="fa fa-tags"></i> Tags</label>
<div id="tags-container" style="border: 1px solid #ccc; padding: 5px; min-height: 40px; background-color: #f9f9f9;">
<!-- Tag inputs will be added here dynamically -->
</div>
<button type="button" id="add-tag-btn" class="red-ui-button" style="margin-top: 5px;">+ Add Tag</button>
<datalist id="fuxa-tags-historical"></datalist>
</div>
<div class="form-row">
<label for="node-input-from"><i class="fa fa-clock-o"></i> From (timestamp)</label>
<input type="datetime-local" id="node-input-from" placeholder="From timestamp (optional)">
</div>
<div class="form-row">
<label for="node-input-to"><i class="fa fa-clock-o"></i> To (timestamp)</label>
<input type="datetime-local" id="node-input-to" placeholder="To timestamp (optional)">
</div>
</script>
<script type="text/x-red" data-help-name="get-historical-tags">
<p>Get historical data for multiple FUXA tags.</p>
<p>The data array is set to <code>msg.payload</code>.</p>
<h4>Configuration</h4>
<p><strong>Tags:</strong> Select multiple tag names using the individual input fields. Click "+ Add Tag" to add more fields.</p>
<p><strong>Timestamps:</strong> Can be set in config or overridden via message</p>
<h4>Message Properties</h4>
<ul>
<li><code>msg.tags</code> - Array of tag names (overrides config)</li>
<li><code>msg.from</code> - Start timestamp</li>
<li><code>msg.to</code> - End timestamp</li>
</ul>
</script>
@@ -0,0 +1,76 @@
module.exports = function(RED) {
function FuxaGetHistoricalTagsNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = this.context().global.get('fuxa');
this.on('input', async function(msg) {
try {
var tags = config.tags || msg.tags;
// Handle new JSON format
if (typeof tags === 'string' && tags.startsWith('[')) {
try {
tags = JSON.parse(tags);
} catch(e) {
// Fallback to old comma-separated format
tags = tags.split(',').map(tag => tag.trim());
}
} else if (typeof tags === 'string') {
// Old comma-separated format
tags = tags.split(',').map(tag => tag.trim());
}
var fromts = config.from || msg.from || Date.now() - 3600000; // default 1 hour ago
var tots = config.to || msg.to || Date.now();
// Convert string timestamps to numbers if needed
if (typeof fromts === 'string') {
fromts = new Date(fromts).getTime();
}
if (typeof tots === 'string') {
tots = new Date(tots).getTime();
}
// Convert tag names to tag IDs (like advanced reports do)
var tagIds = [];
for (var i = 0; i < tags.length; i++) {
var tagId = fuxa.getTagId(tags[i]);
if (tagId) {
tagIds.push(tagId);
} else {
node.warn('Tag not found: ' + tags[i]);
}
}
if (tagIds.length > 0) {
var data = await fuxa.getHistoricalTags(tagIds, fromts, tots);
// Filter out null values and convert to the expected format (like advanced reports)
var filteredData = {};
for (var tagId in data) {
if (data.hasOwnProperty(tagId)) {
var tagData = data[tagId].filter(function(item) {
return item && item.x !== undefined && item.y !== undefined && item.y !== null;
});
// Convert back to tag name keys for consistency
var tagName = tags[tagIds.indexOf(tagId)];
if (tagName) {
filteredData[tagName] = tagData;
}
}
}
msg.payload = filteredData;
msg.topic = tags[0]; // Set topic to first tag name for join operations
node.send(msg);
} else {
node.error('No valid tags found', msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("get-historical-tags", FuxaGetHistoricalTagsNode);
}
@@ -0,0 +1,88 @@
<script type="text/javascript">
RED.nodes.registerType('get-history-alarms',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
start: {value:""},
end: {value:""}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||"get history alarms";
},
oneditprepare: function() {
// Convert timestamp format for datetime-local inputs
function formatForInput(timestamp) {
if (!timestamp) return '';
// Convert from "YYYY-MM-DD HH:MM:SS" to "YYYY-MM-DDTHH:MM"
return timestamp.replace(' ', 'T').substring(0, 16);
}
function formatForStorage(datetimeLocal) {
if (!datetimeLocal) return '';
// Convert from "YYYY-MM-DDTHH:MM" to "YYYY-MM-DD HH:MM:SS"
return datetimeLocal.replace('T', ' ') + ':00';
}
// Set initial values
$("#node-input-start").val(formatForInput(this.start));
$("#node-input-end").val(formatForInput(this.end));
// Handle changes to convert back to storage format
$("#node-input-start").on('change', function() {
$("#node-input-start").data('stored-value', formatForStorage($(this).val()));
});
$("#node-input-end").on('change', function() {
$("#node-input-end").data('stored-value', formatForStorage($(this).val()));
});
},
oneditsave: function() {
// Save the converted values
this.start = $("#node-input-start").data('stored-value') || $("#node-input-start").val();
this.end = $("#node-input-end").data('stored-value') || $("#node-input-end").val();
}
});
</script><script type="text/x-red" data-template-name="get-history-alarms">
<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-start"><i class="fa fa-clock-o"></i> Start (timestamp)</label>
<input type="datetime-local" id="node-input-start" placeholder="Start timestamp (optional)">
</div>
<div class="form-row">
<label for="node-input-end"><i class="fa fa-clock-o"></i> End (timestamp)</label>
<input type="datetime-local" id="node-input-end" placeholder="End timestamp (optional)">
</div>
</script>
<script type="text/x-red" data-help-name="get-history-alarms">
<p>Get historical alarms from FUXA between timestamps.</p>
<p>The alarms array is set to <code>msg.payload</code>.</p>
<h4>Timestamp Configuration</h4>
<p><strong>Static timestamps</strong> can be set in the node configuration using datetime picker.</p>
<p><strong>Dynamic timestamps</strong> can override the configured values:</p>
<ul>
<li><code>msg.start</code> - Start timestamp (Unix timestamp in milliseconds, or ISO date string)</li>
<li><code>msg.end</code> - End timestamp (Unix timestamp in milliseconds, or ISO date string)</li>
</ul>
<p>If no timestamps are provided, defaults to the last 24 hours.</p>
<h4>Supported Timestamp Formats</h4>
<ul>
<li><strong>Unix timestamp:</strong> <code>1693526400000</code> (milliseconds since epoch)</li>
<li><strong>ISO string:</strong> <code>"2025-10-26T13:54:21.000Z"</code></li>
<li><strong>Date string:</strong> <code>"2025-10-26 13:54:21"</code></li>
</ul>
<h4>Examples</h4>
<pre>// Get alarms for last week
msg.start = Date.now() - 604800000; // 7 days ago
msg.end = Date.now();
// Get alarms for specific date range
msg.start = "2025-10-20T00:00:00.000Z";
msg.end = "2025-10-26T23:59:59.999Z";</pre>
</script>
@@ -0,0 +1,28 @@
module.exports = function(RED) {
function FuxaGetHistoryAlarmsNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var start = config.start || msg.start || Date.now() - 86400000; // default 1 day ago
var end = config.end || msg.end || Date.now();
// Convert string timestamps to numbers if needed (same as get-historical-tags)
if (typeof start === 'string') {
start = new Date(start).getTime();
}
if (typeof end === 'string') {
end = new Date(end).getTime();
}
var alarms = await fuxa.getHistoryAlarms(start, end);
msg.payload = alarms;
node.send(msg);
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("get-history-alarms", FuxaGetHistoryAlarmsNode);
}
@@ -0,0 +1,104 @@
<script type="text/javascript">
RED.nodes.registerType('get-tag-change',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
tag: {value:""}, // Keep: tag name for display and backward compatibility
tagId: {value:""} // New: unique identifier (optional, for backward compatibility)
},
inputs:0,
outputs:1,
icon: "white-globe.png",
label: function() {
if (this.name) {
return this.name;
}
// Display tag name or tagId
return this.tag || this.tagId || "get tag change";
},
oneditprepare: function() {
var node = this;
var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-change-tags');
datalist.empty();
data.forEach(function(device) {
device.tags.forEach(function(tag) {
var fullName = device.name + ' - ' + tag.name;
// datalist option value format: tag.name(tag.id)
var optionValue = tag.name + '(' + tag.id + ')';
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
tagMap[tag.id] = {
name: tag.name,
deviceName: device.name,
fullName: fullName
};
});
});
// Listen for tagSelected input changes
$('#node-input-tagSelected').on('change', function() {
var selectedValue = $(this).val();
// Parse format: tag.name(tag.id)
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
if (match) {
var tagName = match[1];
var tagId = match[2];
// Set the actual stored fields
$('#node-input-tag').val(tagName);
$('#node-input-tagId').val(tagId);
}
});
// Initialize display (when editing existing node)
if (node.tagId && tagMap[node.tagId]) {
// Has tagId, construct tagSelected value
var tagName = node.tag || tagMap[node.tagId].name;
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
} else if (node.tag && !node.tagId) {
// Old node: only has tag (tag.name), need to find corresponding tagId
// Note: if there are duplicate names, only the first match will be found
for (var tagId in tagMap) {
if (tagMap[tagId].name === node.tag) {
$('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
$('#node-input-tagId').val(tagId);
break;
}
}
}
});
}
});
</script>
<script type="text/x-red" data-template-name="get-tag-change">
<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-tagSelected"><i class="fa fa-tag"></i> Tag</label>
<input type="text" id="node-input-tagSelected" list="fuxa-change-tags" placeholder="Select Tag">
<datalist id="fuxa-change-tags"></datalist>
</div>
<input type="hidden" id="node-input-tag">
<input type="hidden" id="node-input-tagId">
</script>
<script type="text/x-red" data-help-name="get-tag-change">
<p>Monitor a FUXA tag for value changes in real-time.</p>
<p>This node listens for tag change events and outputs the new value whenever the specified tag changes.</p>
<h4>Output</h4>
<ul>
<li><code>msg.payload</code>: The new tag value</li>
<li><code>msg.topic</code>: The tag name</li>
<li><code>msg.tagId</code>: The internal tag ID</li>
<li><code>msg.tagName</code>: The tag name</li>
<li><code>msg.timestamp</code>: Timestamp of the change</li>
</ul>
<p>This node is event-driven and does not poll for changes, making it efficient for real-time monitoring. Use multiple instances to monitor different tags.</p>
</script>
@@ -0,0 +1,78 @@
module.exports = function(RED) {
function FuxaGetTagChangeNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
// Store previous values to detect actual changes
var previousValues = {};
// Prefer config.tagId, fallback to config.tag for backward compatibility
var configuredTagId = config.tagId;
if (!configuredTagId && config.tag) {
// Backward compatibility: old nodes use tag.name, need to convert to tagId
configuredTagId = fuxa.getTagId(config.tag, null);
}
// Initialize previous value for the configured tag
if (configuredTagId) {
var initialValue = fuxa.getTag(configuredTagId);
if (initialValue !== undefined) {
previousValues[configuredTagId] = initialValue;
}
}
// Event listener for device value changes (raw events from devices)
var deviceEventListener = function(deviceEvent) {
try {
// deviceEvent format: { id: deviceId, values: { tagId: tagObject, ... } }
if (deviceEvent && deviceEvent.values) {
if (configuredTagId && deviceEvent.values[configuredTagId]) {
var tagData = deviceEvent.values[configuredTagId];
// Get the current tag value directly from FUXA
var currentValue = fuxa.getTag(configuredTagId);
if (currentValue !== undefined) {
// Check if the value has actually changed compared to our stored previous value
var previousValue = previousValues[configuredTagId];
if (previousValue !== currentValue) {
// Update previous value
previousValues[configuredTagId] = currentValue;
// Output the tag change
var outputMsg = {
payload: currentValue,
topic: config.tag || configuredTagId,
tagId: configuredTagId,
tagName: config.tag,
timestamp: tagData.timestamp || new Date().toISOString(),
previousValue: previousValue
};
node.send(outputMsg);
}
}
}
}
} catch (err) {
// Silently handle errors
}
};
// Register the event listener for device value changes (listen to all device changes and filter)
fuxa.runtime.events.on('device-value:changed', deviceEventListener);
// Clean up on node close
this.on('close', function(done) {
try {
fuxa.runtime.events.removeListener('device-value:changed', deviceEventListener);
} catch (err) {
// Silently handle cleanup errors
}
done();
});
}
RED.nodes.registerType("get-tag-change", FuxaGetTagChangeNode);
}
@@ -0,0 +1,96 @@
<script type="text/javascript">
RED.nodes.registerType('get-tag-daq-settings',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
tag: {value:""}, // Keep: tag name for display and backward compatibility
tagId: {value:""} // New: unique identifier (optional, for backward compatibility)
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
if (this.name) {
return this.name;
}
// Display tag name or tagId
return this.tag || this.tagId || "get tag daq settings";
},
oneditprepare: function() {
var node = this;
var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-tags-daq-settings');
datalist.empty();
data.forEach(function(device) {
device.tags.forEach(function(tag) {
var fullName = device.name + ' - ' + tag.name;
// datalist option value format: tag.name(tag.id)
var optionValue = tag.name + '(' + tag.id + ')';
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
tagMap[tag.id] = {
name: tag.name,
deviceName: device.name,
fullName: fullName
};
});
});
// Listen for tagSelected input changes
$('#node-input-tagSelected').on('change', function() {
var selectedValue = $(this).val();
// Parse format: tag.name(tag.id)
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
if (match) {
var tagName = match[1];
var tagId = match[2];
// Set the actual stored fields
$('#node-input-tag').val(tagName);
$('#node-input-tagId').val(tagId);
}
});
// Initialize display (when editing existing node)
if (node.tagId && tagMap[node.tagId]) {
// Has tagId, construct tagSelected value
var tagName = node.tag || tagMap[node.tagId].name;
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
} else if (node.tag && !node.tagId) {
// Old node: only has tag (tag.name), need to find corresponding tagId
// Note: if there are duplicate names, only the first match will be found
for (var tagId in tagMap) {
if (tagMap[tagId].name === node.tag) {
$('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
$('#node-input-tagId').val(tagId);
break;
}
}
}
});
}
});
</script>
<script type="text/x-red" data-template-name="get-tag-daq-settings">
<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-tagSelected"><i class="fa fa-tag"></i> Tag</label>
<input type="text" id="node-input-tagSelected" list="fuxa-tags-daq-settings" placeholder="Select Tag">
<datalist id="fuxa-tags-daq-settings"></datalist>
</div>
<input type="hidden" id="node-input-tag">
<input type="hidden" id="node-input-tagId">
</script>
<script type="text/x-red" data-help-name="get-tag-daq-settings">
<p>Get DAQ settings for a FUXA tag.</p>
<p>The DAQ settings object is set to <code>msg.payload</code>.</p>
<p>Use <code>msg.tag</code> to override the configured tag name.</p>
</script>
@@ -0,0 +1,32 @@
module.exports = function(RED) {
function FuxaGetTagDaqSettingsNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
// Prefer config.tagId, fallback to config.tag or msg.tag for backward compatibility
var tagId = config.tagId;
if (!tagId) {
var tagName = config.tag || msg.tag;
if (tagName) {
// Backward compatibility: old nodes use tag.name, need to convert to tagId
tagId = fuxa.getTagId(tagName, null);
}
}
if (tagId) {
var settings = await fuxa.getTagDaqSettings(tagId);
msg.payload = settings;
node.send(msg);
} else {
node.error('Tag not found: ' + (config.tag || msg.tag || config.tagId), msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("get-tag-daq-settings", FuxaGetTagDaqSettingsNode);
}
@@ -0,0 +1,45 @@
<script type="text/javascript">
RED.nodes.registerType('get-tag-id',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
tagName: {value:"", required:true}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||this.tagName||"get tag id";
},
oneditprepare: function() {
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-tags-id');
datalist.empty();
data.forEach(function(device) {
device.tags.forEach(function(tag) {
datalist.append('<option value="' + tag.name + '">' + device.name + ' - ' + tag.name + '</option>');
});
});
});
}
});
</script>
<script type="text/x-red" data-template-name="get-tag-id">
<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-tagName"><i class="fa fa-tag"></i> Tag Name</label>
<input type="text" id="node-input-tagName" list="fuxa-tags-id" placeholder="Tag Name">
<datalist id="fuxa-tags-id"></datalist>
</div>
</script>
<script type="text/x-red" data-help-name="get-tag-id">
<p>Get the ID of a FUXA tag by name.</p>
<p>The tag ID is set to <code>msg.payload</code>.</p>
<p>Use <code>msg.tagName</code> to override the configured tag name.</p>
</script>
@@ -0,0 +1,23 @@
module.exports = function(RED) {
function FuxaGetTagIdNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var tagName = config.tagName || msg.tagName;
if (tagName) {
var tagId = fuxa.getTagId(tagName, null);
msg.payload = tagId;
node.send(msg);
} else {
node.error('Tag name not specified', msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("get-tag-id", FuxaGetTagIdNode);
}
@@ -0,0 +1,99 @@
<script type="text/javascript">
RED.nodes.registerType('get-tag', {
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value: ""},
tag: {value: ""},
tagId: {value: ""}
},
inputs: 1,
outputs: 1,
icon: "white-globe.png",
label: function() {
if (this.name) {
return this.name;
}
return this.tag || this.tagId || "get tag";
},
oneditprepare: function() {
var node = this;
var tagMap = {};
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-tags');
datalist.empty();
(data || []).forEach(function(device) {
(device.tags || []).forEach(function(tag) {
var fullName = device.name + ' - ' + tag.name;
var optionValue = tag.name + '(' + tag.id + ')';
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
tagMap[tag.id] = {
name: tag.name,
deviceName: device.name,
fullName: fullName
};
});
});
$('#node-input-tagSelected').on('change', function() {
var selectedValue = $(this).val();
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
if (match) {
$('#node-input-tag').val(match[1]);
$('#node-input-tagId').val(match[2]);
} else if (!selectedValue || !selectedValue.trim()) {
$('#node-input-tag').val('');
$('#node-input-tagId').val('');
}
});
if (node.tagId && tagMap[node.tagId]) {
var tagName = node.tag || tagMap[node.tagId].name;
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
} else if (node.tag && !node.tagId) {
for (var id in tagMap) {
if (tagMap[id].name === node.tag) {
$('#node-input-tagSelected').val(node.tag + '(' + id + ')');
$('#node-input-tagId').val(id);
break;
}
}
}
});
$('#node-input-tagSelected').attr('placeholder', 'Optional - leave empty to use msg.topic');
}
});
</script>
<script type="text/x-red" data-template-name="get-tag">
<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-tagSelected"><i class="fa fa-tag"></i> Tag</label>
<input type="text" id="node-input-tagSelected" list="fuxa-tags" placeholder="Optional - leave empty to use msg.topic">
<datalist id="fuxa-tags"></datalist>
</div>
<input type="hidden" id="node-input-tag">
<input type="hidden" id="node-input-tagId">
<div class="form-row">
<div style="margin-left:105px;font-size:12px;color:#666;">
If <b>Tag</b> is empty, the node uses <code>msg.topic</code>.
</div>
</div>
</script>
<script type="text/x-red" data-help-name="get-tag">
<p>Gets the value of a FUXA tag.</p>
<ul>
<li>If <b>Tag</b> is set in the node, that tag is used.</li>
<li>If <b>Tag</b> is empty, <code>msg.topic</code> is used.</li>
</ul>
<p>The tag value is returned in <code>msg.payload</code>.</p>
<p>The resolved tag name is always set on <code>msg.topic</code>.</p>
<pre>msg.topic = "V2";</pre>
</script>
@@ -0,0 +1,57 @@
module.exports = function(RED) {
function FuxaGetTagNode(config) {
RED.nodes.createNode(this, config);
const node = this;
// Access FUXA functions from global context
const fuxa = RED.settings?.functionGlobalContext?.fuxa;
node.on("input", function(msg, send, done) {
send = send || node.send.bind(node);
try {
if (!fuxa) {
node.error("FUXA not available in functionGlobalContext", msg);
return done && done();
}
const uiTag = (typeof config.tag === "string") ? config.tag.trim() : "";
const topicTag = (typeof msg.topic === "string") ? msg.topic.trim() : "";
const tagRef = uiTag || topicTag;
let tagId = config.tagId || null;
// Keep master behavior (prefer configured tagId), then fallback to UI tag/topic lookup.
if (!tagId && !tagRef) {
node.error("No tag provided: set Tag in the node OR provide msg.topic", msg);
return done && done();
}
if (!tagId) {
tagId = fuxa.getTagId(tagRef, null);
}
if (!tagId) {
node.error("Tag not found: " + tagRef, msg);
return done && done();
}
const value = fuxa.getTag(tagId);
msg.payload = value;
if (tagRef) {
msg.topic = tagRef;
} else if (typeof config.tag === "string" && config.tag.trim()) {
msg.topic = config.tag.trim();
}
send(msg);
return done && done();
} catch (err) {
node.error(err, msg);
return done && done(err);
}
});
}
RED.nodes.registerType("get-tag", FuxaGetTagNode);
};
@@ -0,0 +1,44 @@
<script type="text/javascript">
RED.nodes.registerType('open-card',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
cardName: {value:"", required:true}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||this.cardName||"open card";
},
oneditprepare: function() {
$.getJSON('/nodered/fuxa/views', function(data) {
var datalist = $('#fuxa-cards');
datalist.empty();
data.forEach(function(view) {
datalist.append('<option value="' + view.name + '">' + view.name + '</option>');
});
});
}
});
</script>
<script type="text/x-red" data-template-name="open-card">
<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-cardName"><i class="fa fa-window-maximize"></i> Card Name</label>
<input type="text" id="node-input-cardName" list="fuxa-cards" placeholder="Card Name">
<datalist id="fuxa-cards"></datalist>
</div>
</script>
<script type="text/x-red" data-help-name="open-card">
<p>Open a specific card/view in FUXA.</p>
<p>The result is set to <code>msg.payload</code>.</p>
<p>Use <code>msg.cardName</code> to override the configured card name.</p>
<p><strong>Available cards:</strong> Dropdown populated from FUXA project views.</p>
</script>
@@ -0,0 +1,23 @@
module.exports = function(RED) {
function FuxaOpenCardNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var cardName = config.cardName || msg.cardName;
if (cardName) {
var result = await fuxa.openCard(cardName);
msg.payload = result;
node.send(msg);
} else {
node.error('Card name not specified', msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("open-card", FuxaOpenCardNode);
}
@@ -0,0 +1,43 @@
<script type="text/javascript">
RED.nodes.registerType('send-message',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
address: {value:"", required:true},
subject: {value:"", required:true},
message: {value:""}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||"send message";
}
});
</script>
<script type="text/x-red" data-template-name="send-message">
<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-address"><i class="fa fa-envelope"></i> Address</label>
<input type="text" id="node-input-address" placeholder="Recipient address">
</div>
<div class="form-row">
<label for="node-input-subject"><i class="fa fa-tag"></i> Subject</label>
<input type="text" id="node-input-subject" placeholder="Subject">
</div>
<div class="form-row">
<label for="node-input-message"><i class="fa fa-comment"></i> Message</label>
<input type="text" id="node-input-message" placeholder="Message (optional, uses msg.payload)">
</div>
</script>
<script type="text/x-red" data-help-name="send-message">
<p>Send a message via FUXA's notificator.</p>
<p>Sends to <code>address</code> with <code>subject</code> and <code>message</code>.</p>
<p>Use <code>msg.address</code>, <code>msg.subject</code>, <code>msg.message</code> or <code>msg.payload</code> to override.</p>
</script>
@@ -0,0 +1,24 @@
module.exports = function(RED) {
function FuxaSendMessageNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var address = config.address || msg.address;
var subject = config.subject || msg.subject;
var message = config.message || msg.message || msg.payload;
if (address && subject && message) {
await fuxa.sendMessage(address, subject, message);
node.send(msg);
} else {
node.error('Missing address, subject, or message', msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("send-message", FuxaSendMessageNode);
}
@@ -0,0 +1,53 @@
<script type="text/javascript">
RED.nodes.registerType('set-device-property',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
deviceName: {value:"", required:true},
property: {value:"", required:true},
value: {value:""}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
return this.name||this.deviceName||"set device property";
},
oneditprepare: function() {
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-devices-set-property');
datalist.empty();
data.forEach(function(device) {
datalist.append('<option value="' + device.name + '">' + device.name + '</option>');
});
});
}
});
</script>
<script type="text/x-red" data-template-name="set-device-property">
<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-deviceName"><i class="fa fa-cogs"></i> Device Name</label>
<input type="text" id="node-input-deviceName" list="fuxa-devices-set-property" placeholder="Device Name">
<datalist id="fuxa-devices-set-property"></datalist>
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-info-circle"></i> Property</label>
<input type="text" id="node-input-property" placeholder="Property name">
</div>
<div class="form-row">
<label for="node-input-value"><i class="fa fa-edit"></i> Value</label>
<input type="text" id="node-input-value" placeholder="Property value">
</div>
</script>
<script type="text/x-red" data-help-name="set-device-property">
<p>Set a property value on a FUXA device.</p>
<p>The result is set to <code>msg.payload</code>.</p>
<p>Use message properties to override: <code>msg.deviceName</code>, <code>msg.property</code>, <code>msg.value</code>.</p>
</script>
@@ -0,0 +1,27 @@
module.exports = function(RED) {
function FuxaSetDevicePropertyNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
var deviceName = config.deviceName || msg.deviceName;
var property = config.property || msg.property;
var value = config.value;
if (msg.value !== undefined) value = msg.value;
if (deviceName && property !== undefined) {
var result = await fuxa.setDeviceProperty(deviceName, property, value);
msg.payload = result;
node.send(msg);
} else {
node.error('Device name and property not specified', msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("set-device-property", FuxaSetDevicePropertyNode);
}
@@ -0,0 +1,112 @@
<script type="text/javascript">
RED.nodes.registerType('set-tag-daq-settings',{
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value:""},
tag: {value:""}, // Keep: tag name for display and backward compatibility
tagId: {value:""}, // New: unique identifier (optional, for backward compatibility)
enabled: {value:true},
interval: {value:1000},
deadband: {value:0}
},
inputs:1,
outputs:1,
icon: "white-globe.png",
label: function() {
if (this.name) {
return this.name;
}
// Display tag name or tagId
return this.tag || this.tagId || "set tag daq settings";
},
oneditprepare: function() {
var node = this;
var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-tags-set-daq-settings');
datalist.empty();
data.forEach(function(device) {
device.tags.forEach(function(tag) {
var fullName = device.name + ' - ' + tag.name;
// datalist option value format: tag.name(tag.id)
var optionValue = tag.name + '(' + tag.id + ')';
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
tagMap[tag.id] = {
name: tag.name,
deviceName: device.name,
fullName: fullName
};
});
});
// Listen for tagSelected input changes
$('#node-input-tagSelected').on('change', function() {
var selectedValue = $(this).val();
// Parse format: tag.name(tag.id)
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
if (match) {
var tagName = match[1];
var tagId = match[2];
// Set the actual stored fields
$('#node-input-tag').val(tagName);
$('#node-input-tagId').val(tagId);
}
});
// Initialize display (when editing existing node)
if (node.tagId && tagMap[node.tagId]) {
// Has tagId, construct tagSelected value
var tagName = node.tag || tagMap[node.tagId].name;
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
} else if (node.tag && !node.tagId) {
// Old node: only has tag (tag.name), need to find corresponding tagId
// Note: if there are duplicate names, only the first match will be found
for (var tagId in tagMap) {
if (tagMap[tagId].name === node.tag) {
$('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
$('#node-input-tagId').val(tagId);
break;
}
}
}
});
}
});
</script>
<script type="text/x-red" data-template-name="set-tag-daq-settings">
<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-tagSelected"><i class="fa fa-tag"></i> Tag</label>
<input type="text" id="node-input-tagSelected" list="fuxa-tags-set-daq-settings" placeholder="Select Tag">
<datalist id="fuxa-tags-set-daq-settings"></datalist>
</div>
<input type="hidden" id="node-input-tag">
<input type="hidden" id="node-input-tagId">
<div class="form-row">
<label for="node-input-enabled">&nbsp;</label>
<input type="checkbox" id="node-input-enabled" style="display:inline-block; width:15px; vertical-align:baseline;">
<span>Enabled</span>
</div>
<div class="form-row">
<label for="node-input-interval"><i class="fa fa-clock-o"></i> Interval (ms)</label>
<input type="number" id="node-input-interval" placeholder="1000">
</div>
<div class="form-row">
<label for="node-input-deadband"><i class="fa fa-filter"></i> Deadband</label>
<input type="number" id="node-input-deadband" step="0.01" placeholder="0">
</div>
</script>
<script type="text/x-red" data-help-name="set-tag-daq-settings">
<p>Set DAQ settings for a FUXA tag.</p>
<p>The result is set to <code>msg.payload</code>.</p>
<p>Use message properties to override: <code>msg.tag</code>, <code>msg.enabled</code>, <code>msg.interval</code>, <code>msg.deadband</code>.</p>
</script>
@@ -0,0 +1,43 @@
module.exports = function(RED) {
function FuxaSetTagDaqSettingsNode(config) {
RED.nodes.createNode(this, config);
var node = this;
var fuxa = RED.settings.functionGlobalContext.fuxa;
this.on('input', async function(msg) {
try {
// Prefer config.tagId, fallback to config.tag or msg.tag for backward compatibility
var tagId = config.tagId;
if (!tagId) {
var tagName = config.tag || msg.tag;
if (tagName) {
// Backward compatibility: old nodes use tag.name, need to convert to tagId
tagId = fuxa.getTagId(tagName, null);
}
}
if (tagId) {
var settings = {
enabled: config.enabled,
interval: config.interval,
deadband: config.deadband
};
// Override with message properties if provided
if (msg.enabled !== undefined) settings.enabled = msg.enabled;
if (msg.interval !== undefined) settings.interval = msg.interval;
if (msg.deadband !== undefined) settings.deadband = msg.deadband;
var result = await fuxa.setTagDaqSettings(tagId, settings);
msg.payload = result;
node.send(msg);
} else {
node.error('Tag not found: ' + (config.tag || msg.tag || config.tagId), msg);
}
} catch (err) {
node.error(err, msg);
}
});
}
RED.nodes.registerType("set-tag-daq-settings", FuxaSetTagDaqSettingsNode);
}
@@ -0,0 +1,102 @@
<script type="text/javascript">
RED.nodes.registerType('set-tag', {
category: 'FUXA',
color: '#a6bbcf',
defaults: {
name: {value: ""},
tag: {value: ""},
tagId: {value: ""}
},
inputs: 1,
outputs: 1,
icon: "white-globe.png",
label: function() {
if (this.name) {
return this.name;
}
return this.tag || this.tagId || "set tag";
},
oneditprepare: function() {
var node = this;
var tagMap = {};
$.getJSON('/nodered/fuxa/devices', function(data) {
var datalist = $('#fuxa-tags-set');
datalist.empty();
(data || []).forEach(function(device) {
(device.tags || []).forEach(function(tag) {
var fullName = device.name + ' - ' + tag.name;
var optionValue = tag.name + '(' + tag.id + ')';
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
tagMap[tag.id] = {
name: tag.name,
deviceName: device.name,
fullName: fullName
};
});
});
$('#node-input-tagSelected').on('change', function() {
var selectedValue = $(this).val();
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
if (match) {
$('#node-input-tag').val(match[1]);
$('#node-input-tagId').val(match[2]);
} else if (!selectedValue || !selectedValue.trim()) {
$('#node-input-tag').val('');
$('#node-input-tagId').val('');
}
});
if (node.tagId && tagMap[node.tagId]) {
var tagName = node.tag || tagMap[node.tagId].name;
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
} else if (node.tag && !node.tagId) {
for (var id in tagMap) {
if (tagMap[id].name === node.tag) {
$('#node-input-tagSelected').val(node.tag + '(' + id + ')');
$('#node-input-tagId').val(id);
break;
}
}
}
});
$('#node-input-tagSelected').attr('placeholder', 'Optional - leave empty to use msg.topic');
}
});
</script>
<script type="text/x-red" data-template-name="set-tag">
<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-tagSelected"><i class="fa fa-tag"></i> Tag</label>
<input type="text" id="node-input-tagSelected" list="fuxa-tags-set" placeholder="Optional - leave empty to use msg.topic">
<datalist id="fuxa-tags-set"></datalist>
</div>
<input type="hidden" id="node-input-tag">
<input type="hidden" id="node-input-tagId">
<div class="form-row">
<div style="margin-left: 105px; font-size: 12px; color: #666;">
If <b>Tag</b> is empty, the node uses <code>msg.topic</code> as the tag name/id.
</div>
</div>
</script>
<script type="text/x-red" data-help-name="set-tag">
<p>Sets the value of a FUXA tag to <code>msg.payload</code>.</p>
<p><b>Tag selection:</b></p>
<ul>
<li>If the node's <b>Tag</b> field is set, that tag is used.</li>
<li>If the node's <b>Tag</b> field is empty, the node uses <code>msg.topic</code> as the tag name/id.</li>
</ul>
<p><b>Example:</b></p>
<pre>msg.topic = "V2";
msg.payload = true;</pre>
</script>
@@ -0,0 +1 @@
{"id": "GHSA-rg3m-cfq7-g6h6", "repo": "frangoteam/FUXA", "scope": ["server/api/command", "server/integrations/node-red", "server/runtime/scripts"], "files": 40, "objective": "Audit this real-world source tree for the single most serious exploitable security vulnerability."}