const { getFormatter } = require('./formatters'); //this class will handle the output events for the node red node class OutputUtils { constructor() { this.output = {}; } checkForChanges(output, format) { if (!output || typeof output !== 'object') { return {}; } this.output[format] = this.output[format] || {}; const changedFields = {}; for (const key in output) { if (Object.prototype.hasOwnProperty.call(output, key) && output[key] !== this.output[format][key]) { let value = output[key]; // For fields: if the value is an object (and not a Date), stringify it. if (value !== null && typeof value === 'object' && !(value instanceof Date)) { changedFields[key] = JSON.stringify(value); } else { changedFields[key] = value; } } } // Update the saved output state. this.output[format] = { ...this.output[format], ...changedFields }; return changedFields; } formatMsg(output, config, format) { let msg = {}; // Compare output with last output and only include changed values const changedFields = this.checkForChanges(output,format); if (Object.keys(changedFields).length > 0) { const measurement = config.general.name; const flatTags = this.flattenTags(this.extractRelevantConfig(config)); const formatterName = this.resolveFormatterName(config, format); const formatter = getFormatter(formatterName); const payload = formatter.format(measurement, { fields: changedFields, tags: flatTags, config, channel: format, }); msg = this.wrapMessage(measurement, payload); return msg; } return null; } resolveFormatterName(config, channel) { const outputConfig = config.output || {}; if (channel === 'process') { return outputConfig.process || 'process'; } if (channel === 'influxdb') { return outputConfig.dbase || 'influxdb'; } return outputConfig[channel] || channel; } wrapMessage(measurement, payload) { return { topic: measurement, payload, }; } flattenTags(obj) { const result = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const value = obj[key]; if (value !== null && typeof value === 'object' && !(value instanceof Date)) { // Recursively flatten the nested object. const flatChild = this.flattenTags(value); for (const childKey in flatChild) { if (Object.prototype.hasOwnProperty.call(flatChild, childKey)) { result[`${key}_${childKey}`] = String(flatChild[childKey]); } } } else { // InfluxDB tags must be strings. result[key] = String(value); } } } return result; } extractRelevantConfig(config) { return { // general properties id: config.general?.id, // functionality properties softwareType: config.functionality?.softwareType, role: config.functionality?.role, // asset properties (exclude machineCurve) uuid: config.asset?.uuid, tagcode: config.asset?.tagcode, geoLocation: config.asset?.geoLocation, category: config.asset?.category, type: config.asset?.type, model: config.asset?.model, unit: config.general?.unit, }; } } module.exports = OutputUtils;