diff --git a/README.md b/README.md index 62583c2..9c39350 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ -# rotating machine \ No newline at end of file +# rotating machine +---Explanation KPIs--- +Mean Time Between Failures: MTBF is a key indicator of asset reliability. It represents the average amount of time between two consecutive failures during normal operation. The higher the value, the better. +- Formula: MTBF = runtime / failures + +Mean Time to Repair: MTTR focuses on the speed of failure recovery. It indicates how long it takes on average to repair a failure and restore the system to operational condition. The lower the value, the better. +- Formula: MTTR = downtime / failures + +Asset Availability: Asset availability indicates how often machines are available for use. Higher availability means that the equipment experiences fewer breakdowns and remains operational for a greater amount of time. +- Formula: availability = MTBF / (MTBF + MTTR) * 100% + +Asset Health Index: A score ranging from 0 (optimal condition) to 5 (worst case) that reflects the overall health status of an asset. + + +---KPI message--- +The message consists of the following components: +- asset tagnumber: +- asset availability +- mean time between failures +- mean time to repair +- asset health index +- asset health color: Gives a color based on the asset health index (0 = Darkgreen, 1 = Green, 2 = Yellow, 3 = Orange, 4 = Red, 5 = Darkred.) +- total failures: the total number of failures that have occured for a particular asset \ No newline at end of file diff --git a/src/specificClass.js b/src/specificClass.js index ccc56b7..028f95a 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -116,6 +116,37 @@ class Machine { this.child = {}; // object to hold child information so we know on what to subscribe this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility + // --- KPI tracking --- + this.kpi = { + failures: 0, + totalRuntimeHours: 0, + totalDowntimeHours: 0, + lastFailureTime: null, + lastRepairTime: null, + MTBF: 0, + MTTR: 0, + availability: 0 + }; + + this.assetHealth = { + index: 0 // 0 = optimal, 5 = failure + }; + + this.state.emitter.on('stateChange', (payload) => { + + const stateStr = typeof payload === 'string' + ? payload + : (payload?.state ?? payload?.newState ?? payload); + + if (typeof stateStr !== 'string') { + this.logger.warn(`stateChange event without parsable state: ${JSON.stringify(payload)}`); + return; + } + + this._handleStateChangeForKPI(stateStr); + }); + + } /*------------------- Register child events -------------------*/ @@ -574,6 +605,136 @@ _callMeasurementHandler(measurementType, value, position, context) { } } + +///////////////////////////// + /** + * Compute a single drift score in [0..1] using predicted vs measured series. + * Uses min/max of the *predicted* window as normalization range. + * If no usable data -> returns 0 (neutral). + */ + _computeDriftScore() { + try { + const metrics = [ + { key: "pressure", pos: "downstream" }, + { key: "flow", pos: "downstream" }, + { key: "power", pos: "atEquipment" } + ]; + + const values = []; + + for (const m of metrics) { + const pred = this.measurements.type(m.key).variant("predicted").position(m.pos).getAllValues()?.values; + const meas = this.measurements.type(m.key).variant("measured").position(m.pos).getAllValues()?.values; + + if (!Array.isArray(pred) || !Array.isArray(meas) || pred.length < 2 || meas.length < 2) continue; + + const expectedMin = Math.min(...pred); + const expectedMax = Math.max(...pred); + if (!Number.isFinite(expectedMin) || !Number.isFinite(expectedMax) || expectedMax === expectedMin) continue; + + const drift = this.errorMetrics.assessDrift(pred, meas, expectedMin, expectedMax); + if (Number.isFinite(drift)) { + // assessDrift is already normalized; keep it in [0..1] + values.push(Math.max(0, Math.min(1, Math.abs(drift)))); + } + } + + if (values.length === 0) return 0; // neutral if no data + const avg = values.reduce((s, v) => s + v, 0) / values.length; + return Math.max(0, Math.min(1, avg)); + } catch (e) { + this.logger?.warn?.(`Drift score error: ${e.message}`); + return 0; + } + } + + _calculateAssetHealthIndex() { + try { + // 1) Hard fail -> worst health + // if (this.state?.getCurrentState && this.state.getCurrentState() === "failed") + if (["off"].includes(this.state?.getCurrentState?.())){ + this.assetHealth.index = 5; + return 5; + } + + // 2) Inputs (clamped to 0..1) + const availability = typeof this.kpi?.availability === 'number' ? this.kpi.availability : 1; + const unavailability = 1 - Math.max(0, Math.min(1, availability)); + + const effPenalty = Math.max(0, Math.min(1, typeof this.relDistFromPeak === 'number' ? this.relDistFromPeak : 0)); + + const driftScore = this._computeDriftScore(); // 0..1 + + // 3) Blend (weights sum to 1.0) + // Tweak these if you like: e.g. make drift more/less important. + const wAvail = 0.4; // unavailability weight + const wDrift = 0.4; // drift weight + const wEff = 0.2; // efficiency distance weight + + const score01 = (wAvail * unavailability) + (wDrift * driftScore) + (wEff * effPenalty); + + // 4) Scale to 0..5 integer, clamp + const index = Math.max(0, Math.min(5, Math.round(score01 * 5))); + + this.assetHealth.index = index; + return index; + } catch (err) { + this.logger?.error?.(`AHI calc error: ${err.message}`); + this.assetHealth.index = 0; + return 0; + } + } + +_handleStateChangeForKPI(newState) { + const now = Date.now(); + const runtime = this.state.getRunTimeHours(); + const lastState = this.state.getPreviousState?.() || "unknown"; + + // --- Treat OFF as failure and start of downtime --- + if (newState === "off") { + this.kpi.failures++; // always count a new failure when OFF + this.kpi.lastFailureTime = now; // mark the start of downtime + this.logger.warn(`Machine OFF (counted as failure). Total failures: ${this.kpi.failures}`); + } + + // --- When we leave OFF and become OPERATIONAL, book downtime --- + if (newState === "operational") { + // Only calculate downtime if we had an OFF period before + if (this.kpi.lastFailureTime != null) { + const downtimeHours = (now - this.kpi.lastFailureTime) / 3600000; + this.kpi.totalDowntimeHours += downtimeHours; + this.kpi.lastRepairTime = now; // moment of "repaired" + this.kpi.lastFailureTime = null; // close downtime window + this.logger.info(`OFF → OPERATIONAL. Added ${downtimeHours.toFixed(2)}h downtime.`); + } + } + + // --- Compute KPI Metrics --- + const failures = this.kpi.failures; + const downtime = this.kpi.totalDowntimeHours; + + // If no failures yet: MTBF = total runtime; MTTR = 0 + this.kpi.MTBF = failures > 0 ? runtime / failures : runtime; + this.kpi.MTTR = failures > 0 ? downtime / failures : 0; + + // --- Compute Availability --- + const mtbf = this.kpi.MTBF ?? 0; + const mttr = this.kpi.MTTR ?? 0; + + if (mtbf <= 0 && mttr <= 0) { + this.kpi.availability = 1; // Default: 100% if no data + } else { + const availability = mtbf / (mtbf + mttr); + this.kpi.availability = Math.min(1, Math.max(0, availability)); // clamp 0–1 + } + + this.logger.debug( + `KPI updated — MTBF: ${this.kpi.MTBF.toFixed(2)}h, MTTR: ${this.kpi.MTTR.toFixed(2)}h, ` + + `Availability: ${(this.kpi.availability * 100).toFixed(2)}%` + ); +} + +////////////////////////////////////////////// calcDistanceFromPeak(currentEfficiency,peakEfficiency){ return Math.abs(currentEfficiency - peakEfficiency); @@ -748,6 +909,13 @@ _callMeasurementHandler(measurementType, value, position, context) { output["cog"] = this.cog; // flow / power efficiency output["NCog"] = this.NCog; // normalized cog output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ; + output["kpi_MTBF"] = this.kpi.MTBF; + output["kpi_MTTR"] = this.kpi.MTTR; + output["kpi_assetAvailability"] = Math.round(this.kpi.availability * 100 * 100) / 100; + output["kpi_totalFailuresCount"] = this.kpi.failures; + output["asset_tag_number"] = 'L001'; + + // output["asset_tag_number"] = this.assetTagNumber; if(this.flowDrift != null){ const flowDrift = this.flowDrift; @@ -762,6 +930,21 @@ _callMeasurementHandler(measurementType, value, position, context) { output["effRelDistFromPeak"] = this.relDistFromPeak; //this.logger.debug(`Output: ${JSON.stringify(output)}`); + + ///////////////////////////////// + // this._calculateAssetHealthIndex(); + // output["assetHealthIndex"] = this.assetHealth.index; + + this._calculateAssetHealthIndex(); + output["assetHealthIndex"] = this.assetHealth.index; + + // 0 = darkgreen, 1 = green, 2 = yellow, 3 = orange, 4 = red, 5 = darkred + // const healthColors = ["darkgreen", "green", "yellow", "orange", "red", "darkred"]; + const healthColors = ["#006400", "#008000", "#FFFF00", "#FFA500", "#FF0000", "#8B0000"]; + output["assetHealthColor"] = healthColors[this.assetHealth.index] || "unknown"; + ////////////////////////// + + return output; }