node-red-contrib-automatanexus-hvac-vibration
Version:
AutomataNexus Industrial Vibration Monitoring - Professional WTVB01-485 integration for industrial equipment with ISO 10816 compliance
727 lines (650 loc) • 34 kB
JavaScript
/**
* AutomataNexus Industrial Vibration Monitoring
* Professional WitMotion WTVB01-485 Integration for Industrial Equipment
* Supports up to 32 sensors with configurable applications
* (c) 2025 AutomataNexus AI & AutomataControls
*/
module.exports = function(RED) {
"use strict";
function IndustrialVibrationParserNode(config) {
RED.nodes.createNode(this, config);
var node = this;
// Configuration
node.outputFormat = config.outputFormat || "standard";
node.globalVars = config.globalVars !== false;
node.globalPrefix = config.globalPrefix || "industrial";
node.sensorMappings = config.sensorMappings || [];
// ISO 10816 vibration severity zones based on machine class
// Class I: Small machines (<15kW)
// Class II: Medium machines (15-75kW or up to 300kW on special foundations)
// Class III: Large machines (>75kW rigid foundation)
// Class IV: Large machines (>75kW soft foundation)
const ISO_10816_CLASSES = {
"I": { zones: { A: 0.71, B: 1.8, C: 4.5, D: 7.1 }, desc: "Small (<15kW)" },
"II": { zones: { A: 1.12, B: 2.8, C: 7.1, D: 11.2 }, desc: "Medium (15-75kW)" },
"III": { zones: { A: 1.8, B: 4.5, C: 11.2, D: 18.0 }, desc: "Large Rigid (>75kW)" },
"IV": { zones: { A: 2.8, B: 7.1, C: 18.0, D: 28.0 }, desc: "Large Soft (>75kW)" }
};
// Determine ISO class based on power rating
function getISOClass(powerKW, foundationType) {
if (powerKW < 15) return "I";
if (powerKW <= 75) return "II";
// For large machines, foundation type matters
if (foundationType === "soft" || foundationType === "flexible") return "IV";
return "III";
}
// Default ISO standards for different equipment types with power considerations
const ISO_STANDARDS = {
"HVAC_FAN": {
baseClass: "II",
adjustForPower: true,
unit: "mm/s RMS"
},
"CENTRIFUGAL_PUMP": {
baseClass: "II",
adjustForPower: true,
unit: "mm/s RMS"
},
"RECIPROCATING_COMPRESSOR": {
// Reciprocating equipment has higher allowable vibration
customZones: { A: 7.1, B: 11.0, C: 18.0, D: 28.0 },
adjustForPower: false,
unit: "mm/s RMS"
},
"SCREW_COMPRESSOR": {
baseClass: "II",
adjustForPower: true,
unit: "mm/s RMS"
},
"COOLING_TOWER": {
baseClass: "III",
adjustForPower: true,
unit: "mm/s RMS"
},
"CONVEYOR_MOTOR": {
baseClass: "II",
adjustForPower: true,
unit: "mm/s RMS"
},
"MIXER_AGITATOR": {
baseClass: "III",
adjustForPower: true,
unit: "mm/s RMS"
},
"BLOWER": {
baseClass: "II",
adjustForPower: true,
unit: "mm/s RMS"
},
"GEARBOX": {
customZones: { A: 3.5, B: 7.1, C: 11.0, D: 18.0 },
adjustForPower: false,
unit: "mm/s RMS"
},
"GENERATOR": {
baseClass: "III",
adjustForPower: true,
unit: "mm/s RMS"
},
"MOTOR_GENERAL": {
baseClass: "II",
adjustForPower: true,
unit: "mm/s RMS"
},
"TURBINE": {
customZones: { A: 1.8, B: 2.8, C: 4.5, D: 7.1 },
adjustForPower: false,
unit: "mm/s RMS"
},
"VACUUM_PUMP": {
baseClass: "II",
adjustForPower: true,
unit: "mm/s RMS"
},
"CRUSHER": {
customZones: { A: 7.1, B: 11.0, C: 18.0, D: 28.0 },
adjustForPower: false,
unit: "mm/s RMS"
},
"CUSTOM": {
baseClass: "II",
adjustForPower: true,
unit: "mm/s RMS"
}
};
// Build sensor lookup map from configuration
node.sensorMap = {};
if (node.sensorMappings && Array.isArray(node.sensorMappings)) {
node.sensorMappings.forEach(mapping => {
if (mapping.address && mapping.enabled) {
node.sensorMap[mapping.address] = {
name: mapping.name || `Sensor_${mapping.address}`,
type: mapping.type || "MOTOR_GENERAL",
location: mapping.location || "Unspecified",
powerHP: mapping.powerHP || null,
powerKW: mapping.powerKW || mapping.powerHP ? mapping.powerHP * 0.746 : null,
foundationType: mapping.foundationType || "rigid",
rpmNominal: mapping.rpmNominal || 1800,
customZones: mapping.customZones || null,
alarmThresholds: mapping.alarmThresholds || null
};
}
});
}
// Statistics
node.messageCount = 0;
node.lastUpdate = null;
node.sensorStatus = {};
// Node status
node.status({fill: "blue", shape: "dot", text: "Ready"});
// Process incoming messages
node.on('input', function(msg, send, done) {
send = send || function() { node.send.apply(node, arguments); };
try {
let parsedData = parseIndustrialData(msg.payload);
if (parsedData) {
node.messageCount++;
node.lastUpdate = new Date();
// Update sensor status tracking
node.sensorStatus[parsedData.sensor_address] = {
lastSeen: node.lastUpdate,
condition: parsedData.equipment_condition
};
// Set global variables
if (node.globalVars) {
setIndustrialGlobals(parsedData);
}
// Update status
updateNodeStatus();
// Send output
send([{
payload: parsedData,
topic: `${node.globalPrefix}/${parsedData.equipment_type.toLowerCase()}/${parsedData.sensor_address}`,
sensor_address: parsedData.sensor_address,
equipment_type: parsedData.equipment_type,
equipment_name: parsedData.equipment_name,
iso_zone: parsedData.iso_zone,
iso_class: parsedData.iso_class,
alerts: parsedData.alerts
}]);
if (done) done();
} else {
node.warn("Failed to parse industrial data");
if (done) done();
}
} catch (error) {
node.error("Industrial Parser Error: " + error.message, msg);
node.status({fill: "red", shape: "ring", text: "Error"});
if (done) done(error);
}
});
// Parse industrial data
function parseIndustrialData(payload) {
let data = {};
try {
if (typeof payload === 'string') {
// Parse console output or JSON
if (payload.includes('[OK]') || payload.includes('[WARN]') ||
payload.includes('[CRIT]') || payload.includes('[EMRG]')) {
data = parseConsoleOutput(payload);
} else {
data = JSON.parse(payload);
}
} else if (typeof payload === 'object') {
// Check if this is monitoring API data (has equipment names as keys)
if (isMonitoringAPIData(payload)) {
// Convert monitoring API format to standard format
data = convertMonitoringAPIData(payload);
} else {
data = payload;
}
}
return standardizeIndustrialData(data);
} catch (error) {
return null;
}
}
// Check if data is from monitoring API (equipment names as keys)
function isMonitoringAPIData(data) {
// Monitoring API has equipment names as keys with sensor data as values
for (let key in data) {
if (data.hasOwnProperty(key)) {
let value = data[key];
if (typeof value === 'object' &&
(value.hasOwnProperty('temperature_f') ||
value.hasOwnProperty('rms_acceleration') ||
value.hasOwnProperty('velocity_mms'))) {
return true;
}
}
}
return false;
}
// Convert monitoring API format to parser format
function convertMonitoringAPIData(apiData) {
// Take the first equipment entry
let equipmentName = Object.keys(apiData)[0];
let sensorData = apiData[equipmentName];
return {
equipment_name: equipmentName,
equipment_type: detectEquipmentType(equipmentName),
temperature_f: sensorData.temperature_f,
vibration_velocity: sensorData.velocity_mms,
rms_acceleration: sensorData.rms_acceleration,
iso_zone: sensorData.iso_zone,
alert_level: sensorData.alert_level || "NORMAL",
// Include any additional fields from API
hp: sensorData.hp,
voltage: sensorData.voltage,
phase: sensorData.phase
};
}
// Detect equipment type from name
function detectEquipmentType(name) {
let lowerName = name.toLowerCase();
if (lowerName.includes('cooling_tower')) return 'COOLING_TOWER';
if (lowerName.includes('pump')) return 'CENTRIFUGAL_PUMP';
if (lowerName.includes('compressor')) return 'SCREW_COMPRESSOR';
if (lowerName.includes('fan')) return 'HVAC_FAN';
if (lowerName.includes('blower')) return 'BLOWER';
if (lowerName.includes('mixer')) return 'MIXER_AGITATOR';
return 'MOTOR_GENERAL';
}
// Parse console output
function parseConsoleOutput(text) {
let data = {};
// Extract alert level
let alertMatch = text.match(/\[(OK|WARN|CRIT|EMRG)\]/);
if (alertMatch) data.alert_level = alertMatch[1];
// Extract timestamp if present
let timeMatch = text.match(/(\d{2}:\d{2}:\d{2})/);
if (timeMatch) data.timestamp_str = timeMatch[1];
// Extract equipment name (new format) or sensor address (old format)
// New format: [OK] 10:01:35 | Cooling_Tower_1 | RMS: ...
// Old format: [OK] Sensor 0x50 | RMS: ...
let parts = text.split('|').map(p => p.trim());
if (parts.length >= 2) {
let sensorPart = parts[1];
// Check if it's an address format
let addressMatch = sensorPart.match(/(?:Sensor|Motor|Address|TTYUSB\d+)\s*(?:0x([0-9A-F]+))?/i);
if (addressMatch && addressMatch[1]) {
data.sensor_address = parseInt(addressMatch[1], 16);
} else if (sensorPart.match(/TTYUSB(\d+)/i)) {
// Handle TTYUSB format
let usbMatch = sensorPart.match(/TTYUSB(\d+)/i);
data.sensor_port = `TTYUSB${usbMatch[1]}`;
data.equipment_name = sensorPart; // May be overridden if actual name found
} else {
// It's likely an equipment name
data.equipment_name = sensorPart;
// Try to extract equipment type from name
if (sensorPart.toLowerCase().includes('cooling_tower')) {
data.equipment_type = 'COOLING_TOWER';
} else if (sensorPart.toLowerCase().includes('pump')) {
data.equipment_type = 'CENTRIFUGAL_PUMP';
} else if (sensorPart.toLowerCase().includes('compressor')) {
data.equipment_type = 'SCREW_COMPRESSOR';
} else if (sensorPart.toLowerCase().includes('fan')) {
data.equipment_type = 'HVAC_FAN';
}
}
}
// Extract temperature
let tempMatch = text.match(/Temp:\s*([+-]?\d+(?:\.\d+)?)\s*°([FC])/i);
if (tempMatch) {
let temp = parseFloat(tempMatch[1]);
if (tempMatch[2] === 'F') {
data.temperature_f = temp;
} else {
data.temperature_c = temp;
}
}
// Extract vibration velocity (supports both "Vel:" and "Velocity:")
let velMatch = text.match(/(?:Vel|Velocity):\s*([+-]?\d+(?:\.\d+)?)\s*mm\/s/i);
if (velMatch) data.vibration_velocity = parseFloat(velMatch[1]);
// Extract RMS acceleration
let rmsMatch = text.match(/RMS:\s*([+-]?\d+(?:\.\d+)?)\s*g/i);
if (rmsMatch) data.rms_acceleration = parseFloat(rmsMatch[1]);
// Extract frequency
let freqMatch = text.match(/Freq:\s*([+-]?\d+(?:\.\d+)?)\s*Hz/i);
if (freqMatch) data.frequency = parseFloat(freqMatch[1]);
// Extract ISO zone if present
let zoneMatch = text.match(/(?:ISO\s*)?Zone:\s*([A-D])/i);
if (zoneMatch) data.iso_zone = zoneMatch[1];
return data;
}
// Standardize industrial data
function standardizeIndustrialData(rawData) {
if (!rawData) return null;
// Handle both old format (motor_id) and new format (sensor_address)
if (!rawData.sensor_address && rawData.motor_id !== undefined) {
rawData.sensor_address = rawData.motor_id;
}
// If we have equipment_name but no sensor_address, create a pseudo-address
if (!rawData.sensor_address && rawData.equipment_name) {
// Use a hash of the equipment name as address for consistency
let hash = 0;
for (let i = 0; i < rawData.equipment_name.length; i++) {
hash = ((hash << 5) - hash) + rawData.equipment_name.charCodeAt(i);
hash = hash & hash; // Convert to 32bit integer
}
rawData.sensor_address = Math.abs(hash) % 256; // Keep it in byte range
}
if (!rawData.sensor_address && !rawData.equipment_name) return null;
// Get sensor configuration - prefer data from message over internal config
let sensorConfig;
// If we have full equipment data from the monitoring API, use it directly
if (rawData.equipment_name && rawData.equipment_type) {
sensorConfig = {
name: rawData.equipment_name,
type: rawData.equipment_type,
location: rawData.location || "From Monitoring System",
powerHP: rawData.hp,
powerKW: rawData.hp ? rawData.hp * 0.746 : 30,
foundationType: rawData.mounting || "rigid",
rpmNominal: rawData.rpm || 1800,
voltage: rawData.voltage,
phase: rawData.phase
};
} else {
// Fall back to internal configuration or defaults
sensorConfig = node.sensorMap[rawData.sensor_address] || {
name: rawData.equipment_name || `Sensor_${rawData.sensor_address}`,
type: rawData.equipment_type || "MOTOR_GENERAL",
location: "Unknown",
powerKW: 30, // Default 30kW if not specified
foundationType: "rigid"
};
}
// Get equipment type configuration
let equipmentStandard = ISO_STANDARDS[sensorConfig.type] || ISO_STANDARDS["MOTOR_GENERAL"];
// Determine ISO zones based on power rating and equipment type
let isoZones;
let isoClass = "";
if (equipmentStandard.adjustForPower && sensorConfig.powerKW) {
// Get ISO class based on power
isoClass = getISOClass(sensorConfig.powerKW, sensorConfig.foundationType);
isoZones = ISO_10816_CLASSES[isoClass].zones;
} else if (equipmentStandard.customZones) {
// Use equipment-specific zones
isoZones = equipmentStandard.customZones;
isoClass = "Equipment Specific";
} else {
// Use base class zones
isoClass = equipmentStandard.baseClass;
isoZones = ISO_10816_CLASSES[isoClass].zones;
}
// Override with custom zones if specified
if (sensorConfig.customZones) {
isoZones = sensorConfig.customZones;
isoClass = "Custom";
}
let industrialData = {
timestamp: new Date().toISOString(),
sensor_address: rawData.sensor_address,
equipment_name: sensorConfig.name,
equipment_type: sensorConfig.type,
equipment_location: sensorConfig.location,
equipment_power: {
hp: sensorConfig.powerHP || (sensorConfig.powerKW ? sensorConfig.powerKW / 0.746 : null),
kw: sensorConfig.powerKW || (sensorConfig.powerHP ? sensorConfig.powerHP * 0.746 : null)
},
equipment_specs: {
foundation_type: sensorConfig.foundationType,
rpm_nominal: sensorConfig.rpmNominal,
iso_class: isoClass,
iso_zones: isoZones
},
temperature: {
fahrenheit: 0,
celsius: 0
},
vibration: {
velocity_mms: rawData.vibration_velocity || (rawData.vibration && rawData.vibration.velocity_mms) || 0,
rms_acceleration_g: rawData.rms_acceleration || (rawData.vibration && rawData.vibration.rms_acceleration_g) || 0,
frequency_hz: rawData.frequency || (rawData.vibration && rawData.vibration.frequency_hz) || 0,
displacement_um: rawData.displacement || (rawData.vibration && rawData.vibration.displacement_um) || 0
},
alert_level: rawData.alert_level || "NORMAL",
iso_zone: "",
iso_class: isoClass,
equipment_condition: "",
maintenance_priority: "LOW",
estimated_rul_days: 365, // Remaining Useful Life
alerts: []
};
// Temperature conversion - handle multiple formats
if (rawData.temperature_f !== undefined) {
industrialData.temperature.fahrenheit = rawData.temperature_f;
industrialData.temperature.celsius = (rawData.temperature_f - 32) * 5.0 / 9.0;
} else if (rawData.temperature_c !== undefined) {
industrialData.temperature.celsius = rawData.temperature_c;
industrialData.temperature.fahrenheit = (rawData.temperature_c * 9.0 / 5.0) + 32;
} else if (rawData.temperature && typeof rawData.temperature === 'object') {
// Handle nested temperature object
industrialData.temperature.fahrenheit = rawData.temperature.fahrenheit || 70;
industrialData.temperature.celsius = rawData.temperature.celsius || ((industrialData.temperature.fahrenheit - 32) * 5.0 / 9.0);
} else if (rawData.temperature !== undefined && typeof rawData.temperature === 'number') {
// Handle single temperature value (assume Fahrenheit from WTVB01-485)
industrialData.temperature.fahrenheit = rawData.temperature;
industrialData.temperature.celsius = (rawData.temperature - 32) * 5.0 / 9.0;
}
// Handle metrics from API format
if (rawData.metrics && typeof rawData.metrics === 'object') {
industrialData.vibration.velocity_mms = rawData.metrics.velocity_mms || rawData.metrics.vibration_velocity_rms || 0;
industrialData.vibration.rms_acceleration_g = rawData.metrics.rms_acceleration || 0;
industrialData.vibration.frequency_hz = rawData.metrics.dominant_frequency || 0;
if (rawData.metrics.iso_zone) {
rawData.iso_zone = rawData.metrics.iso_zone;
}
}
// Handle acceleration data from API format
if (rawData.acceleration && typeof rawData.acceleration === 'object') {
// Calculate RMS from 3-axis if not already provided
if (!industrialData.vibration.rms_acceleration_g &&
(rawData.acceleration.x !== undefined ||
rawData.acceleration.y !== undefined ||
rawData.acceleration.z !== undefined)) {
let x = rawData.acceleration.x || 0;
let y = rawData.acceleration.y || 0;
let z = rawData.acceleration.z || 0;
industrialData.vibration.rms_acceleration_g = Math.sqrt(x*x + y*y + z*z) / Math.sqrt(3);
}
}
// ISO zone classification - use provided zone if available
if (rawData.iso_zone) {
// Trust the ISO zone from the monitoring API
industrialData.iso_zone = rawData.iso_zone;
// Set condition based on zone
switch(rawData.iso_zone) {
case "A":
industrialData.equipment_condition = "EXCELLENT";
industrialData.maintenance_priority = "LOW";
industrialData.estimated_rul_days = 365;
break;
case "B":
industrialData.equipment_condition = "GOOD";
industrialData.maintenance_priority = "LOW";
industrialData.estimated_rul_days = 180;
break;
case "C":
industrialData.equipment_condition = "FAIR";
industrialData.maintenance_priority = "MEDIUM";
industrialData.estimated_rul_days = 90;
industrialData.alerts.push({
type: "VIBRATION_WARNING",
message: `Equipment in ISO Zone C - Schedule maintenance`,
severity: "WARNING"
});
break;
case "D":
industrialData.equipment_condition = "POOR";
industrialData.maintenance_priority = "HIGH";
industrialData.estimated_rul_days = 30;
industrialData.alerts.push({
type: "VIBRATION_CRITICAL",
message: `Equipment in ISO Zone D - Immediate attention required`,
severity: "CRITICAL"
});
break;
}
} else {
// Calculate ISO zone if not provided
let velocity = industrialData.vibration.velocity_mms;
if (velocity < isoZones.A) {
industrialData.iso_zone = "A";
industrialData.equipment_condition = "EXCELLENT";
industrialData.maintenance_priority = "LOW";
industrialData.estimated_rul_days = 365;
} else if (velocity < isoZones.B) {
industrialData.iso_zone = "B";
industrialData.equipment_condition = "GOOD";
industrialData.maintenance_priority = "LOW";
industrialData.estimated_rul_days = 180;
} else if (velocity < isoZones.C) {
industrialData.iso_zone = "C";
industrialData.equipment_condition = "FAIR";
industrialData.maintenance_priority = "MEDIUM";
industrialData.estimated_rul_days = 90;
industrialData.alerts.push({
type: "VIBRATION_WARNING",
message: `Vibration ${velocity.toFixed(2)} mm/s exceeds ISO ${isoClass} Zone B (${isoZones.B} mm/s)`,
severity: "WARNING"
});
} else {
industrialData.iso_zone = "D";
industrialData.equipment_condition = "POOR";
industrialData.maintenance_priority = "HIGH";
industrialData.estimated_rul_days = 30;
industrialData.alerts.push({
type: "VIBRATION_CRITICAL",
message: `Vibration ${velocity.toFixed(2)} mm/s exceeds ISO ${isoClass} Zone C (${isoZones.C} mm/s)`,
severity: "CRITICAL"
});
}
}
// Temperature alerts
if (industrialData.temperature.celsius > 80) {
industrialData.alerts.push({
type: "TEMPERATURE_HIGH",
message: `Temperature ${industrialData.temperature.celsius.toFixed(1)}°C exceeds 80°C threshold`,
severity: "WARNING"
});
industrialData.maintenance_priority = "HIGH";
} else if (industrialData.temperature.celsius > 90) {
industrialData.alerts.push({
type: "TEMPERATURE_CRITICAL",
message: `Temperature ${industrialData.temperature.celsius.toFixed(1)}°C exceeds 90°C critical threshold`,
severity: "CRITICAL"
});
}
// RPM deviation alerts (if frequency data available)
if (industrialData.vibration.frequency_hz > 0 && sensorConfig.rpmNominal) {
let measuredRPM = industrialData.vibration.frequency_hz * 60;
let rpmDeviation = Math.abs(measuredRPM - sensorConfig.rpmNominal) / sensorConfig.rpmNominal * 100;
if (rpmDeviation > 10) {
industrialData.alerts.push({
type: "RPM_DEVIATION",
message: `RPM deviation ${rpmDeviation.toFixed(1)}% from nominal ${sensorConfig.rpmNominal} RPM`,
severity: "WARNING"
});
}
}
// Custom alarm thresholds
if (sensorConfig.alarmThresholds) {
checkCustomAlarms(industrialData, sensorConfig.alarmThresholds);
}
return industrialData;
}
// Check custom alarm thresholds
function checkCustomAlarms(data, thresholds) {
if (thresholds.vibration && data.vibration.velocity_mms > thresholds.vibration) {
data.alerts.push({
type: "CUSTOM_VIBRATION_ALARM",
message: `Vibration ${data.vibration.velocity_mms.toFixed(2)} mm/s exceeds custom threshold ${thresholds.vibration} mm/s`,
severity: "CUSTOM"
});
}
if (thresholds.temperature && data.temperature.celsius > thresholds.temperature) {
data.alerts.push({
type: "CUSTOM_TEMPERATURE_ALARM",
message: `Temperature ${data.temperature.celsius.toFixed(1)}°C exceeds custom threshold ${thresholds.temperature}°C`,
severity: "CUSTOM"
});
}
if (thresholds.acceleration && data.vibration.rms_acceleration_g > thresholds.acceleration) {
data.alerts.push({
type: "CUSTOM_ACCELERATION_ALARM",
message: `Acceleration ${data.vibration.rms_acceleration_g.toFixed(3)}g exceeds custom threshold ${thresholds.acceleration}g`,
severity: "CUSTOM"
});
}
}
// Set global variables
function setIndustrialGlobals(data) {
let prefix = `${node.globalPrefix}_${data.equipment_type.toLowerCase()}_${data.sensor_address}_`;
try {
// Basic measurements
node.context().global.set(prefix + "temperature_f", data.temperature.fahrenheit);
node.context().global.set(prefix + "temperature_c", data.temperature.celsius);
node.context().global.set(prefix + "vibration_velocity", data.vibration.velocity_mms);
node.context().global.set(prefix + "vibration_acceleration", data.vibration.rms_acceleration_g);
node.context().global.set(prefix + "vibration_frequency", data.vibration.frequency_hz);
// Equipment status
node.context().global.set(prefix + "iso_zone", data.iso_zone);
node.context().global.set(prefix + "iso_class", data.iso_class);
node.context().global.set(prefix + "condition", data.equipment_condition);
node.context().global.set(prefix + "maintenance_priority", data.maintenance_priority);
node.context().global.set(prefix + "estimated_rul_days", data.estimated_rul_days);
// Equipment info
node.context().global.set(prefix + "name", data.equipment_name);
node.context().global.set(prefix + "type", data.equipment_type);
node.context().global.set(prefix + "location", data.equipment_location);
node.context().global.set(prefix + "power_hp", data.equipment_power.hp);
node.context().global.set(prefix + "power_kw", data.equipment_power.kw);
// Alerts
node.context().global.set(prefix + "alert_count", data.alerts.length);
node.context().global.set(prefix + "alerts", data.alerts);
// System status
node.context().global.set(node.globalPrefix + "_monitoring_active", true);
node.context().global.set(node.globalPrefix + "_last_update", data.timestamp);
node.context().global.set(node.globalPrefix + "_sensor_count", Object.keys(node.sensorStatus).length);
} catch (error) {
node.warn("Error setting globals: " + error.message);
}
}
// Update node status with overall system health
function updateNodeStatus() {
let totalSensors = Object.keys(node.sensorStatus).length;
let poorCondition = 0;
let warnings = 0;
for (let addr in node.sensorStatus) {
let status = node.sensorStatus[addr];
if (status.condition === "POOR") poorCondition++;
else if (status.condition === "FAIR") warnings++;
}
let statusColor = "green";
let statusShape = "dot";
let statusText = `${totalSensors} sensors active`;
if (poorCondition > 0) {
statusColor = "red";
statusShape = "ring";
statusText = `${poorCondition} critical, ${totalSensors} total`;
} else if (warnings > 0) {
statusColor = "yellow";
statusText = `${warnings} warnings, ${totalSensors} total`;
}
node.status({
fill: statusColor,
shape: statusShape,
text: statusText
});
}
// Cleanup
node.on('close', function() {
node.status({});
});
}
// Register node
RED.nodes.registerType("industrial-vibration-parser", IndustrialVibrationParserNode);
}