Files
generalFunctions/src/measurements/MeasurementContainer.js
2025-11-28 09:59:39 +01:00

637 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const MeasurementBuilder = require('./MeasurementBuilder');
const EventEmitter = require('events');
const convertModule = require('../convert/index');
class MeasurementContainer {
constructor(options = {},logger) {
this.emitter = new EventEmitter();
this.measurements = {};
this.windowSize = options.windowSize || 10; // Default window size
// For chaining context
this._currentChildId = null;
this._currentType = null;
this._currentVariant = null;
this._currentPosition = null;
this._currentDistance = null;
this._unit = null;
// Default units for each measurement type
this.defaultUnits = {
pressure: 'mbar',
flow: 'm3/h',
power: 'kW',
temperature: 'C',
volume: 'm3',
length: 'm',
...options.defaultUnits // Allow override
};
// Auto-conversion settings
this.autoConvert = options.autoConvert !== false; // Default to true
this.preferredUnits = options.preferredUnits || {}; // Per-measurement overrides
// For chaining context
this._currentType = null;
this._currentVariant = null;
this._currentPosition = null;
this._unit = null;
// NEW: Enhanced child identification
this.childId = null;
this.childName = null;
this.parentRef = null;
}
// NEW: Methods to set child context
setChildId(childId) {
this.childId = childId;
return this;
}
child(childId) {
this._currentChildId = childId || 'default';
return this;
}
setChildName(childName) {
this.childName = childName;
return this;
}
setParentRef(parent) {
this.parentRef = parent;
return this;
}
// New method to set preferred units
setPreferredUnit(measurementType, unit) {
this.preferredUnits[measurementType] = unit;
return this;
}
// Get the target unit for a measurement type
_getTargetUnit(measurementType) {
return this.preferredUnits[measurementType] ||
this.defaultUnits[measurementType] ||
null;
}
getUnit(type) {
if (!type) return null;
if (this.preferredUnits && this.preferredUnits[type]) return this.preferredUnits[type];
if (this.defaultUnits && this.defaultUnits[type]) return this.defaultUnits[type];
return null;
}
// Chainable methods
type(typeName) {
this._currentType = typeName;
this._currentVariant = null;
this._currentPosition = null;
return this;
}
variant(variantName) {
if (!this._currentType) {
throw new Error('Type must be specified before variant');
}
this._currentVariant = variantName;
this._currentPosition = null;
return this;
}
position(positionValue) {
if (!this._currentVariant) {
throw new Error('Variant must be specified before position');
}
this._currentPosition = positionValue.toString().toLowerCase();;
return this;
}
distance(distance) {
// If distance is not provided, derive from positionVsParent
if(distance === null) {
distance = this._convertPositionStr2Num(this._currentPosition);
}
this._currentDistance = distance;
return this;
}
// ENHANCED: Update your existing value method
value(val, timestamp = Date.now(), sourceUnit = null) {
if (!this._ensureChainIsValid()) return this;
const measurement = this._getOrCreateMeasurement();
const targetUnit = this._getTargetUnit(this._currentType);
let convertedValue = val;
let finalUnit = sourceUnit || targetUnit;
// Auto-convert if enabled and units are specified
if (this.autoConvert && sourceUnit && targetUnit && sourceUnit !== targetUnit) {
try {
convertedValue = convertModule(val).from(sourceUnit).to(targetUnit);
finalUnit = targetUnit;
if (this.logger) {
this.logger.debug(`Auto-converted ${val} ${sourceUnit} to ${convertedValue} ${targetUnit}`);
}
} catch (error) {
if (this.logger) {
this.logger.warn(`Auto-conversion failed from ${sourceUnit} to ${targetUnit}: ${error.message}`);
}
convertedValue = val;
finalUnit = sourceUnit;
}
}
measurement.setValue(convertedValue, timestamp);
if (finalUnit && !measurement.unit) {
measurement.setUnit(finalUnit);
}
// ENHANCED: Emit event with rich context
const eventData = {
value: convertedValue,
originalValue: val,
unit: finalUnit,
sourceUnit: sourceUnit,
timestamp,
position: this._currentPosition,
distance: this._currentDistance,
variant: this._currentVariant,
type: this._currentType,
// NEW: Enhanced context
childId: this.childId,
childName: this.childName,
parentRef: this.parentRef,
};
// Emit the exact event your parent expects
this.emitter.emit(`${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
//console.log(`Emitted event: ${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
return this;
}
/**
* Check whether a measurement series exists.
*
* You can rely on the current chain (type/variant/position already set via
* type().variant().position()), or pass them explicitly via the options.
*
* @param {object} options
* @param {string} [options.type] Override the current type
* @param {string} [options.variant] Override the current variant
* @param {string} [options.position] Override the current position
* @param {boolean} [options.requireValues=false]
* When true, the series must contain at least one stored value.
*
* @returns {boolean}
*/
exists({ type, variant, position, requireValues = false } = {}) {
const typeKey = type ?? this._currentType;
if (!typeKey) return false;
const variantKey = variant ?? this._currentVariant;
if (!variantKey) return false;
const positionKey = position ?? this._currentPosition;
const typeBucket = this.measurements[typeKey];
if (!typeBucket) return false;
const variantBucket = typeBucket[variantKey];
if (!variantBucket) return false;
if (!positionKey) {
// No specific position requested just check the variant bucket.
return requireValues
? Object.values(variantBucket).some(m => m?.values?.length > 0)
: Object.keys(variantBucket).length > 0;
}
const measurement = variantBucket[positionKey];
if (!measurement) return false;
return requireValues ? measurement.values?.length > 0 : true;
}
unit(unitName) {
if (!this._ensureChainIsValid()) return this;
const measurement = this._getOrCreateMeasurement();
measurement.setUnit(unitName);
this._unit = unitName;
return this;
}
// Terminal operations - get data out
get() {
if (!this._ensureChainIsValid()) return null;
const variantBucket = this.measurements[this._currentType]?.[this._currentVariant];
if (!variantBucket) return null;
const posBucket = variantBucket[this._currentPosition];
if (!posBucket) return null;
// Legacy single measurement
if (posBucket?.getCurrentValue) return posBucket;
// Child-aware: pick requested child, otherwise fall back to default, otherwise first available
if (posBucket && typeof posBucket === 'object') {
const requestedKey = this._currentChildId || this.childId;
const keys = Object.keys(posBucket);
if (!keys.length) return null;
const measurement =
(requestedKey && posBucket[requestedKey]) ||
posBucket.default ||
posBucket[keys[0]];
return measurement || null;
}
return null;
}
getCurrentValue(requestedUnit = null) {
const measurement = this.get();
if (!measurement) return null;
const value = measurement.getCurrentValue();
if (value === null) return null;
if (!requestedUnit || !measurement.unit || requestedUnit === measurement.unit) {
return value;
}
try {
return convertModule(value).from(measurement.unit).to(requestedUnit);
} catch (error) {
if (this.logger) this.logger.error(`Unit conversion failed: ${error.message}`);
return value;
}
}
getAverage(requestedUnit = null) {
const measurement = this.get();
if (!measurement) return null;
const avgValue = measurement.getAverage();
if (avgValue === null) return null;
if (!requestedUnit || !measurement.unit || requestedUnit === measurement.unit) {
return avgValue;
}
try {
return convertModule(avgValue).from(measurement.unit).to(requestedUnit);
} catch (error) {
if (this.logger) {
this.logger.error(`Unit conversion failed: ${error.message}`);
}
return avgValue;
}
}
getMin() {
const measurement = this.get();
return measurement ? measurement.getMin() : null;
}
getMax() {
const measurement = this.get();
return measurement ? measurement.getMax() : null;
}
getAllValues() {
const measurement = this.get();
return measurement ? measurement.getAllValues() : null;
}
getLaggedValue(lag = 1,requestedUnit = null ){
const measurement = this.get();
if (!measurement) return null;
let sample = measurement.getLaggedSample(lag);
if (sample === null) return null;
const value = sample.value;
// Return as-is if no unit conversion requested
if (!requestedUnit) {
return value;
}
// Convert if needed
if (measurement.unit && requestedUnit !== measurement.unit) {
try {
const convertedValue = convertModule(value).from(measurement.unit).to(requestedUnit);
//replace old value in sample and return obj
sample.value = convertedValue ;
sample.unit = requestedUnit;
return sample;
} catch (error) {
if (this.logger) {
this.logger.error(`Unit conversion failed: ${error.message}`);
}
return sample; // Return original value if conversion fails
}
}
return value;
}
getLaggedSample(lag = 1,requestedUnit = null ){
const measurement = this.get();
if (!measurement) return null;
let sample = measurement.getLaggedSample(lag);
if (sample === null) return null;
// Return as-is if no unit conversion requested
if (!requestedUnit) {
return sample;
}
// Convert if needed
if (measurement.unit && requestedUnit !== measurement.unit) {
try {
const convertedValue = convertModule(value).from(measurement.unit).to(requestedUnit);
//replace old value in sample and return obj
sample.value = convertedValue ;
sample.unit = requestedUnit;
return sample;
} catch (error) {
if (this.logger) {
this.logger.error(`Unit conversion failed: ${error.message}`);
}
return sample; // Return original value if conversion fails
}
}
return sample;
}
sum(type, variant, positions = [], targetUnit = null) {
const bucket = this.measurements?.[type]?.[variant];
if (!bucket) return 0;
return positions
.map((pos) => {
const posBucket = bucket[pos];
if (!posBucket) return 0;
return Object.values(posBucket)
.map((m) => {
if (!m?.getCurrentValue) return 0;
const val = m.getCurrentValue();
if (val == null) return 0;
const fromUnit = m.unit || targetUnit;
if (!targetUnit || !fromUnit || fromUnit === targetUnit) return val;
try { return convertModule(val).from(fromUnit).to(targetUnit); } catch { return val; }
})
.reduce((acc, v) => acc + (Number.isFinite(v) ? v : 0), 0);
})
.reduce((acc, v) => acc + v, 0);
}
getFlattenedOutput() {
const out = {};
Object.entries(this.measurements).forEach(([type, variants]) => {
Object.entries(variants).forEach(([variant, positions]) => {
Object.entries(positions).forEach(([position, entry]) => {
// Legacy single series
if (entry?.getCurrentValue) {
out[`${type}.${variant}.${position}`] = entry.getCurrentValue();
return;
}
// Child-bucketed series
if (entry && typeof entry === 'object') {
Object.entries(entry).forEach(([childId, m]) => {
if (m?.getCurrentValue) {
out[`${type}.${variant}.${position}.${childId}`] = m.getCurrentValue();
}
});
}
});
});
});
return out;
}
// Difference calculations between positions
difference({ from = "downstream", to = "upstream", unit: requestedUnit } = {}) {
if (!this._currentType || !this._currentVariant) {
throw new Error("Type and variant must be specified for difference calculation");
}
const get = pos => {
const bucket = this.measurements?.[this._currentType]?.[this._currentVariant]?.[pos];
if (!bucket) return null;
// child-aware bucket: pick current childId/default or first available
if (bucket && typeof bucket === 'object' && !bucket.getCurrentValue) {
const childKey = this._currentChildId || this.childId || Object.keys(bucket)[0];
return bucket?.[childKey] || null;
}
// legacy single measurement
return bucket;
};
const a = get(from);
const b = get(to);
if (!a || !b || !a.values || !b.values || a.values.length === 0 || b.values.length === 0) {
return null;
}
const targetUnit = requestedUnit || a.unit || b.unit;
const aVal = this._convertValueToUnit(a.getCurrentValue(), a.unit, targetUnit);
const bVal = this._convertValueToUnit(b.getCurrentValue(), b.unit, targetUnit);
const aAvg = this._convertValueToUnit(a.getAverage(), a.unit, targetUnit);
const bAvg = this._convertValueToUnit(b.getAverage(), b.unit, targetUnit);
return { value: aVal - bVal, avgDiff: aAvg - bAvg, unit: targetUnit, from, to };
}
// Helper methods
_ensureChainIsValid() {
if (!this._currentType || !this._currentVariant || !this._currentPosition) {
if (this.logger) {
this.logger.error('Incomplete measurement chain, required: type, variant, and position');
}
return false;
}
return true;
}
_getOrCreateMeasurement() {
// Initialize nested structure if needed
if (!this.measurements[this._currentType]) {
this.measurements[this._currentType] = {};
}
if (!this.measurements[this._currentType][this._currentVariant]) {
this.measurements[this._currentType][this._currentVariant] = {};
}
const positionKey = this._currentPosition;
const childKey = this._currentChildId || this.childId || 'default';
if (!this.measurements[this._currentType][this._currentVariant][positionKey]) {
this.measurements[this._currentType][this._currentVariant][positionKey] = {};
}
const bucket = this.measurements[this._currentType][this._currentVariant][positionKey];
if (!bucket[childKey]) {
bucket[childKey] = new MeasurementBuilder()
.setType(this._currentType)
.setVariant(this._currentVariant)
.setPosition(positionKey)
.setWindowSize(this.windowSize)
.setDistance(this._currentDistance)
.build();
}
return bucket[childKey];
}
// Additional utility methods
getTypes() {
return Object.keys(this.measurements);
}
getVariants() {
if (!this._currentType) {
throw new Error('Type must be specified before listing variants');
}
return this.measurements[this._currentType] ?
Object.keys(this.measurements[this._currentType]) : [];
}
getPositions() {
if (!this._currentType || !this._currentVariant) {
throw new Error('Type and variant must be specified before listing positions');
}
if (!this.measurements[this._currentType] ||
!this.measurements[this._currentType][this._currentVariant]) {
return [];
}
return Object.keys(this.measurements[this._currentType][this._currentVariant]);
}
clear() {
this.measurements = {};
this._currentType = null;
this._currentVariant = null;
this._currentPosition = null;
}
// Helper method for value conversion
_convertValueToUnit(value, fromUnit, toUnit) {
if (!value || !fromUnit || !toUnit || fromUnit === toUnit) {
return value;
}
try {
return convertModule(value).from(fromUnit).to(toUnit);
} catch (error) {
if (this.logger) {
this.logger.warn(`Conversion failed from ${fromUnit} to ${toUnit}: ${error.message}`);
}
return value;
}
}
// Get available units for a measurement type
getAvailableUnits(measurementType = null) {
const type = measurementType || this._currentType;
if (!type) return [];
// Map measurement types to convert module measures
const measureMap = {
pressure: 'pressure',
flow: 'volumeFlowRate',
power: 'power',
temperature: 'temperature',
volume: 'volume',
length: 'length',
mass: 'mass',
energy: 'energy'
};
const convertMeasure = measureMap[type];
if (!convertMeasure) return [];
try {
return convertModule().possibilities(convertMeasure);
} catch (error) {
return [];
}
}
// Get best unit for current value
getBestUnit(excludeUnits = []) {
const measurement = this.get();
if (!measurement || !measurement.unit) return null;
const currentValue = measurement.getCurrentValue();
if (currentValue === null) return null;
try {
const best = convertModule(currentValue)
.from(measurement.unit)
.toBest({ exclude: excludeUnits });
return best;
} catch (error) {
if (this.logger) {
this.logger.error(`getBestUnit failed: ${error.message}`);
}
return null;
}
}
_convertPositionStr2Num(positionString) {
switch(positionString) {
case "atEquipment":
return 0;
case "upstream":
return Number.POSITIVE_INFINITY;
case "downstream":
return Number.NEGATIVE_INFINITY;
default:
if (this.logger) {
this.logger.error(`Invalid positionVsParent provided: ${positionString}`);
}
return;
}
}
_convertPositionNum2Str(positionValue) {
switch (positionValue) {
case 0:
return "atEquipment";
case (positionValue < 0):
return "upstream";
case (positionValue > 0):
return "downstream";
default:
console.log(`Invalid position provided: ${positionValue}`);
}
}
}
module.exports = MeasurementContainer;