This commit is contained in:
znetsixe
2026-02-23 13:17:47 +01:00
parent 1cfb36f604
commit c60aa40666
17 changed files with 358 additions and 53 deletions

View File

@@ -6,8 +6,18 @@ class ChildRegistrationUtils {
}
async registerChild(child, positionVsParent, distance) {
const { softwareType } = child.config.functionality;
const { name, id } = child.config.general;
if (!child || typeof child !== 'object') {
this.logger?.warn('registerChild skipped: invalid child payload');
return false;
}
if (!child.config?.functionality || !child.config?.general) {
this.logger?.warn('registerChild skipped: missing child config/functionality/general');
return false;
}
const softwareType = child.config.functionality.softwareType;
const name = child.config.general.name || child.config.general.id || 'unknown';
const id = child.config.general.id || name;
this.logger.debug(`Registering child: ${name} (${id}) as ${softwareType} at ${positionVsParent}`);
@@ -43,19 +53,21 @@ class ChildRegistrationUtils {
}
this.logger.info(`✅ Child ${name} registered successfully`);
return true;
}
_storeChild(child, softwareType) {
// Maintain your existing structure
if (!this.mainClass.child) this.mainClass.child = {};
if (!this.mainClass.child[softwareType]) this.mainClass.child[softwareType] = {};
const typeKey = softwareType || 'unknown';
if (!this.mainClass.child[typeKey]) this.mainClass.child[typeKey] = {};
const { category = "sensor" } = child.config.asset || {};
if (!this.mainClass.child[softwareType][category]) {
this.mainClass.child[softwareType][category] = [];
if (!this.mainClass.child[typeKey][category]) {
this.mainClass.child[typeKey][category] = [];
}
this.mainClass.child[softwareType][category].push(child);
this.mainClass.child[typeKey][category].push(child);
}
// NEW: Utility methods for parent to use
@@ -95,4 +107,4 @@ class ChildRegistrationUtils {
}
}
module.exports = ChildRegistrationUtils;
module.exports = ChildRegistrationUtils;

View File

@@ -73,17 +73,25 @@ class ConfigUtils {
return updatedConfig;
}
_isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
// loop through objects and merge them obj1 will be updated with obj2 values
mergeObjects(obj1, obj2) {
for (let key in obj2) {
if (obj2.hasOwnProperty(key)) {
if (typeof obj2[key] === 'object') {
if (!obj1[key]) {
const nextValue = obj2[key];
if (Array.isArray(nextValue)) {
obj1[key] = [...nextValue];
} else if (this._isPlainObject(nextValue)) {
if (!this._isPlainObject(obj1[key])) {
obj1[key] = {};
}
this.mergeObjects(obj1[key], obj2[key]);
this.mergeObjects(obj1[key], nextValue);
} else {
obj1[key] = obj2[key];
obj1[key] = nextValue;
}
}
}

View File

@@ -18,13 +18,102 @@ class EndpointUtils {
* @param {string} nodeName the name of the node (used in the URL)
* @param {object} customHelpers additional helper functions to inject
*/
createMenuUtilsEndpoint(RED, nodeName, customHelpers = {}) {
RED.httpAdmin.get(`/${nodeName}/resources/menuUtils.js`, (req, res) => {
console.log(`Serving menuUtils.js for ${nodeName} node`);
res.set('Content-Type', 'application/javascript');
const browserCode = this.generateMenuUtilsCode(nodeName, customHelpers);
res.send(browserCode);
createMenuUtilsEndpoint(RED, nodeName, customHelpers = {}, options = {}) {
const basePath = `/${nodeName}/resources`;
RED.httpAdmin.get(`${basePath}/menuUtilsData.json`, (req, res) => {
res.json(this.generateMenuUtilsData(nodeName, customHelpers, options));
});
RED.httpAdmin.get(`${basePath}/menuUtils.legacy.js`, (req, res) => {
res.set('Content-Type', 'application/javascript');
res.send(this.generateLegacyMenuUtilsCode(nodeName, customHelpers));
});
RED.httpAdmin.get(`${basePath}/menuUtils.js`, (req, res) => {
res.set('Content-Type', 'application/javascript');
res.send(this.generateMenuUtilsBootstrap(nodeName));
});
}
generateMenuUtilsData(nodeName, customHelpers = {}, options = {}) {
const defaultHelpers = {
validateRequired: `function(value) {
return value != null && value.toString().trim() !== '';
}`,
formatDisplayValue: `function(value, unit) {
return \`${'${'}value} ${'${'}unit || ''}\`.trim();
}`,
validateScaling: `function(min, max) {
return !isNaN(min) && !isNaN(max) && Number(min) < Number(max);
}`,
validateUnit: `function(unit) {
return typeof unit === 'string' && unit.trim() !== '';
}`,
};
return {
nodeName,
helpers: { ...defaultHelpers, ...customHelpers },
options: {
autoLoadLegacy: options.autoLoadLegacy !== false,
},
};
}
generateMenuUtilsBootstrap(nodeName) {
return `
// Stable bootstrap for EVOLV menu utils (${nodeName})
(function() {
const nodeName = ${JSON.stringify(nodeName)};
const basePath = '/' + nodeName + '/resources';
window.EVOLV = window.EVOLV || {};
window.EVOLV.nodes = window.EVOLV.nodes || {};
window.EVOLV.nodes[nodeName] = window.EVOLV.nodes[nodeName] || {};
window.EVOLV.nodes[nodeName].utils = window.EVOLV.nodes[nodeName].utils || {};
function parseHelper(fnBody) {
try {
return (new Function('return (' + fnBody + ')'))();
} catch (error) {
console.error('[menuUtils] helper parse failed:', error);
return function() { return null; };
}
}
function loadLegacyIfNeeded(autoLoadLegacy) {
if (!autoLoadLegacy || typeof window.MenuUtils === 'function') return Promise.resolve();
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = basePath + '/menuUtils.legacy.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
fetch(basePath + '/menuUtilsData.json')
.then(function(res) { return res.json(); })
.then(function(payload) {
const helperFns = {};
Object.entries(payload.helpers || {}).forEach(function(entry) {
helperFns[entry[0]] = parseHelper(entry[1]);
});
window.EVOLV.nodes[nodeName].utils.helpers = helperFns;
return loadLegacyIfNeeded(payload.options && payload.options.autoLoadLegacy);
})
.then(function() {
if (typeof window.MenuUtils === 'function' && !window.EVOLV.nodes[nodeName].utils.menuUtils) {
window.EVOLV.nodes[nodeName].utils.menuUtils = new window.MenuUtils();
}
})
.catch(function(error) {
console.error('[menuUtils] bootstrap failed for ' + nodeName, error);
});
})();
`;
}
/**
@@ -33,7 +122,7 @@ class EndpointUtils {
* @param {object} customHelpers map of name: functionString pairs
* @returns {string} a JS snippet to run in the browser
*/
generateMenuUtilsCode(nodeName, customHelpers = {}) {
generateLegacyMenuUtilsCode(nodeName, customHelpers = {}) {
// Default helper implementations to expose alongside MenuUtils
const defaultHelpers = {
validateRequired: `function(value) {
@@ -101,6 +190,11 @@ ${helpersCode}
console.log('Loaded EVOLV.nodes.${nodeName}.utils.menuUtils');
`;
}
// Backward-compatible alias.
generateMenuUtilsCode(nodeName, customHelpers = {}) {
return this.generateLegacyMenuUtilsCode(nodeName, customHelpers);
}
}
module.exports = EndpointUtils;

25
src/helper/index.js Normal file
View File

@@ -0,0 +1,25 @@
const assertions = require('./assertionUtils.js');
const assetUtils = require('./assetUtils.js');
const childRegistrationUtils = require('./childRegistrationUtils.js');
const configUtils = require('./configUtils.js');
const endpointUtils = require('./endpointUtils.js');
const gravity = require('./gravity.js');
const logger = require('./logger.js');
const menuUtils = require('./menuUtils.js');
const nodeTemplates = require('./nodeTemplates.js');
const outputUtils = require('./outputUtils.js');
const validation = require('./validationUtils.js');
module.exports = {
assertions,
assetUtils,
childRegistrationUtils,
configUtils,
endpointUtils,
gravity,
logger,
menuUtils,
nodeTemplates,
outputUtils,
validation,
};

View File

@@ -480,17 +480,26 @@ generateHtml(htmlElement, options, savedValue) {
}
}
createMenuUtilsEndpoint(RED, nodeName, customHelpers = {}) {
RED.httpAdmin.get(`/${nodeName}/resources/menuUtils.js`, function(req, res) {
console.log(`Serving menuUtils.js for ${nodeName} node`);
createMenuUtilsEndpoint(RED, nodeName, customHelpers = {}, options = {}) {
const basePath = `/${nodeName}/resources`;
RED.httpAdmin.get(`${basePath}/menuUtilsData.json`, function(req, res) {
res.json(this.generateMenuUtilsData(nodeName, customHelpers, options));
}.bind(this));
RED.httpAdmin.get(`${basePath}/menuUtils.legacy.js`, function(req, res) {
res.set('Content-Type', 'application/javascript');
const browserCode = this.generateMenuUtilsCode(nodeName, customHelpers);
const browserCode = this.generateLegacyMenuUtilsCode(nodeName, customHelpers);
res.send(browserCode);
}.bind(this));
RED.httpAdmin.get(`${basePath}/menuUtils.js`, function(req, res) {
res.set('Content-Type', 'application/javascript');
res.send(this.generateMenuUtilsBootstrap(nodeName));
}.bind(this));
}
generateMenuUtilsCode(nodeName, customHelpers = {}) {
generateMenuUtilsData(nodeName, customHelpers = {}, options = {}) {
const defaultHelpers = {
validateRequired: `function(value) {
return value && value.toString().trim() !== '';
@@ -500,7 +509,71 @@ generateMenuUtilsCode(nodeName, customHelpers = {}) {
}`
};
const allHelpers = { ...defaultHelpers, ...customHelpers };
return {
nodeName,
helpers: { ...defaultHelpers, ...customHelpers },
options: {
autoLoadLegacy: options.autoLoadLegacy !== false,
},
};
}
generateMenuUtilsBootstrap(nodeName) {
return `
// Stable bootstrap for EVOLV menu utils (${nodeName})
(function() {
const nodeName = ${JSON.stringify(nodeName)};
const basePath = '/' + nodeName + '/resources';
window.EVOLV = window.EVOLV || {};
window.EVOLV.nodes = window.EVOLV.nodes || {};
window.EVOLV.nodes[nodeName] = window.EVOLV.nodes[nodeName] || {};
window.EVOLV.nodes[nodeName].utils = window.EVOLV.nodes[nodeName].utils || {};
function parseHelper(fnBody) {
try {
return (new Function('return (' + fnBody + ')'))();
} catch (error) {
console.error('[menuUtils] helper parse failed:', error);
return function() { return null; };
}
}
function loadLegacyIfNeeded(autoLoadLegacy) {
if (!autoLoadLegacy || typeof window.MenuUtils === 'function') return Promise.resolve();
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = basePath + '/menuUtils.legacy.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
fetch(basePath + '/menuUtilsData.json')
.then(function(res) { return res.json(); })
.then(function(payload) {
const helperFns = {};
Object.entries(payload.helpers || {}).forEach(function(entry) {
helperFns[entry[0]] = parseHelper(entry[1]);
});
window.EVOLV.nodes[nodeName].utils.helpers = helperFns;
return loadLegacyIfNeeded(payload.options && payload.options.autoLoadLegacy);
})
.then(function() {
if (typeof window.MenuUtils === 'function' && !window.EVOLV.nodes[nodeName].utils.menuUtils) {
window.EVOLV.nodes[nodeName].utils.menuUtils = new window.MenuUtils();
}
})
.catch(function(error) {
console.error('[menuUtils] bootstrap failed for ' + nodeName, error);
});
})();
`;
}
generateLegacyMenuUtilsCode(nodeName, customHelpers = {}) {
const allHelpers = { ...this.generateMenuUtilsData(nodeName).helpers, ...customHelpers };
const helpersCode = Object.entries(allHelpers)
.map(([name, func]) => ` ${name}: ${func}`)
@@ -533,6 +606,11 @@ ${helpersCode}
`;
}
// Backward-compatible alias
generateMenuUtilsCode(nodeName, customHelpers = {}) {
return this.generateLegacyMenuUtilsCode(nodeName, customHelpers);
}
module.exports = MenuUtils;
}
module.exports = MenuUtils;

View File

@@ -53,4 +53,4 @@ const nodeTemplates = {
// …add more node “templates” here…
};
export default nodeTemplates;
module.exports = nodeTemplates;

View File

@@ -7,6 +7,9 @@ class OutputUtils {
}
checkForChanges(output, format) {
if (!output || typeof output !== 'object') {
return {};
}
const changedFields = {};
for (const key in output) {
if (output.hasOwnProperty(key) && output[key] !== this.output[format][key]) {
@@ -54,11 +57,11 @@ class OutputUtils {
break;
default:
console.log('Unknown format in output utils');
break;
return null;
}
return msg;
}
return null;
}