T3MP3ST public launch build
This commit is contained in:
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'
|
||||
};
|
||||
+286
@@ -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 };
|
||||
+1
@@ -0,0 +1 @@
|
||||
// This file can be empty if using node-red.nodes in package.json
|
||||
+49
@@ -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>
|
||||
+27
@@ -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);
|
||||
}
|
||||
+31
@@ -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>
|
||||
+17
@@ -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);
|
||||
}
|
||||
+49
@@ -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"> </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>
|
||||
+25
@@ -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);
|
||||
}
|
||||
+50
@@ -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>
|
||||
+49
@@ -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);
|
||||
}
|
||||
+27
@@ -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>
|
||||
+19
@@ -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);
|
||||
}
|
||||
+151
@@ -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>
|
||||
+31
@@ -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);
|
||||
}
|
||||
+48
@@ -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>
|
||||
+25
@@ -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);
|
||||
}
|
||||
+49
@@ -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"> </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>
|
||||
+25
@@ -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);
|
||||
}
|
||||
+152
@@ -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>
|
||||
+76
@@ -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);
|
||||
}
|
||||
+88
@@ -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>
|
||||
+28
@@ -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);
|
||||
}
|
||||
+104
@@ -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>
|
||||
+78
@@ -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);
|
||||
}
|
||||
+96
@@ -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>
|
||||
+32
@@ -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);
|
||||
}
|
||||
+45
@@ -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>
|
||||
+23
@@ -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);
|
||||
}
|
||||
+99
@@ -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>
|
||||
+57
@@ -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);
|
||||
};
|
||||
+44
@@ -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>
|
||||
+23
@@ -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);
|
||||
}
|
||||
+43
@@ -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>
|
||||
+24
@@ -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);
|
||||
}
|
||||
+53
@@ -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>
|
||||
+27
@@ -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);
|
||||
}
|
||||
+112
@@ -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"> </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>
|
||||
+43
@@ -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);
|
||||
}
|
||||
+102
@@ -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."}
|
||||
Reference in new issue
Block a user