This commit is contained in:
znetsixe
2026-03-11 11:13:05 +01:00
parent c60aa40666
commit 27a6d3c709
20 changed files with 1555 additions and 229 deletions

View File

@@ -17,7 +17,7 @@ class MeasurementContainer {
this._currentDistance = null;
this._unit = null;
// Default units for each measurement type
// Default units for each measurement type (ingress/preferred)
this.defaultUnits = {
pressure: 'mbar',
flow: 'm3/h',
@@ -27,10 +27,48 @@ class MeasurementContainer {
length: 'm',
...options.defaultUnits // Allow override
};
// Canonical storage unit map (single conversion anchor per measurement type)
this.canonicalUnits = {
pressure: 'Pa',
atmPressure: 'Pa',
flow: 'm3/s',
power: 'W',
hydraulicPower: 'W',
temperature: 'K',
volume: 'm3',
length: 'm',
mass: 'kg',
energy: 'J',
...options.canonicalUnits,
};
// Auto-conversion settings
this.autoConvert = options.autoConvert !== false; // Default to true
this.preferredUnits = options.preferredUnits || {}; // Per-measurement overrides
this.storeCanonical = options.storeCanonical === true;
this.strictUnitValidation = options.strictUnitValidation === true;
this.throwOnInvalidUnit = options.throwOnInvalidUnit === true;
this.requireUnitForTypes = new Set(
(options.requireUnitForTypes || []).map((t) => String(t).trim().toLowerCase())
);
// Map EVOLV measurement types to convert-module measure families
this.measureMap = {
pressure: 'pressure',
atmpressure: 'pressure',
flow: 'volumeFlowRate',
power: 'power',
hydraulicpower: 'power',
reactivepower: 'reactivePower',
apparentpower: 'apparentPower',
temperature: 'temperature',
volume: 'volume',
length: 'length',
mass: 'mass',
energy: 'energy',
reactiveenergy: 'reactiveEnergy',
};
// For chaining context
this._currentType = null;
@@ -72,6 +110,11 @@ class MeasurementContainer {
return this;
}
setCanonicalUnit(measurementType, unit) {
this.canonicalUnits[measurementType] = unit;
return this;
}
// Get the target unit for a measurement type
_getTargetUnit(measurementType) {
return this.preferredUnits[measurementType] ||
@@ -79,6 +122,77 @@ class MeasurementContainer {
null;
}
_getCanonicalUnit(measurementType) {
return this.canonicalUnits[measurementType] || null;
}
_normalizeType(measurementType) {
return String(measurementType || '').trim().toLowerCase();
}
_describeUnit(unit) {
if (typeof unit !== 'string' || unit.trim() === '') return null;
try {
return convertModule().describe(unit.trim());
} catch (error) {
return null;
}
}
isUnitCompatible(measurementType, unit) {
const desc = this._describeUnit(unit);
if (!desc) return false;
const normalizedType = this._normalizeType(measurementType);
const expectedMeasure = this.measureMap[normalizedType];
if (!expectedMeasure) return true;
return desc.measure === expectedMeasure;
}
_handleUnitViolation(message) {
if (this.throwOnInvalidUnit) {
throw new Error(message);
}
if (this.logger) {
this.logger.warn(message);
}
}
_resolveUnitPolicy(measurementType, sourceUnit = null) {
const normalizedType = this._normalizeType(measurementType);
const rawSourceUnit = typeof sourceUnit === 'string' && sourceUnit.trim()
? sourceUnit.trim()
: null;
const fallbackIngressUnit = this._getTargetUnit(measurementType);
const canonicalUnit = this._getCanonicalUnit(measurementType);
const resolvedSourceUnit = rawSourceUnit || fallbackIngressUnit || canonicalUnit || null;
if (this.requireUnitForTypes.has(normalizedType) && !rawSourceUnit) {
this._handleUnitViolation(`Missing source unit for required measurement type '${measurementType}'.`);
return { valid: false };
}
if (resolvedSourceUnit && !this.isUnitCompatible(measurementType, resolvedSourceUnit)) {
this._handleUnitViolation(`Incompatible or unknown source unit '${resolvedSourceUnit}' for measurement type '${measurementType}'.`);
return { valid: false };
}
const resolvedStorageUnit = this.storeCanonical
? (canonicalUnit || fallbackIngressUnit || resolvedSourceUnit)
: (fallbackIngressUnit || canonicalUnit || resolvedSourceUnit);
if (resolvedStorageUnit && !this.isUnitCompatible(measurementType, resolvedStorageUnit)) {
this._handleUnitViolation(`Incompatible storage unit '${resolvedStorageUnit}' for measurement type '${measurementType}'.`);
return { valid: false };
}
return {
valid: true,
sourceUnit: resolvedSourceUnit,
storageUnit: resolvedStorageUnit || null,
strictValidation: this.strictUnitValidation,
};
}
getUnit(type) {
if (!type) return null;
if (this.preferredUnits && this.preferredUnits[type]) return this.preferredUnits[type];
@@ -135,34 +249,40 @@ class MeasurementContainer {
// ENHANCED: Update your existing value method
value(val, timestamp = Date.now(), sourceUnit = null) {
if (!this._ensureChainIsValid()) return this;
const unitPolicy = this._resolveUnitPolicy(this._currentType, sourceUnit);
if (!unitPolicy.valid) return this;
const measurement = this._getOrCreateMeasurement();
const targetUnit = this._getTargetUnit(this._currentType);
const targetUnit = unitPolicy.storageUnit;
let convertedValue = val;
let finalUnit = sourceUnit || targetUnit;
let finalUnit = targetUnit || unitPolicy.sourceUnit;
// Auto-convert if enabled and units are specified
if (this.autoConvert && sourceUnit && targetUnit && sourceUnit !== targetUnit) {
if (this.autoConvert && unitPolicy.sourceUnit && targetUnit && unitPolicy.sourceUnit !== targetUnit) {
try {
convertedValue = convertModule(val).from(sourceUnit).to(targetUnit);
convertedValue = convertModule(val).from(unitPolicy.sourceUnit).to(targetUnit);
finalUnit = targetUnit;
if (this.logger) {
this.logger.debug(`Auto-converted ${val} ${sourceUnit} to ${convertedValue} ${targetUnit}`);
this.logger.debug(`Auto-converted ${val} ${unitPolicy.sourceUnit} to ${convertedValue} ${targetUnit}`);
}
} catch (error) {
if (this.logger) {
this.logger.warn(`Auto-conversion failed from ${sourceUnit} to ${targetUnit}: ${error.message}`);
const message = `Auto-conversion failed from ${unitPolicy.sourceUnit} to ${targetUnit}: ${error.message}`;
if (this.strictUnitValidation) {
this._handleUnitViolation(message);
return this;
}
if (this.logger) this.logger.warn(message);
convertedValue = val;
finalUnit = sourceUnit;
finalUnit = unitPolicy.sourceUnit;
}
}
measurement.setValue(convertedValue, timestamp);
if (finalUnit && !measurement.unit) {
if (finalUnit) {
measurement.setUnit(finalUnit);
}
@@ -171,7 +291,7 @@ class MeasurementContainer {
value: convertedValue,
originalValue: val,
unit: finalUnit,
sourceUnit: sourceUnit,
sourceUnit: unitPolicy.sourceUnit,
timestamp,
position: this._currentPosition,
distance: this._currentDistance,
@@ -408,21 +528,22 @@ class MeasurementContainer {
.reduce((acc, v) => acc + v, 0);
}
getFlattenedOutput() {
getFlattenedOutput(options = {}) {
const requestedUnits = options.requestedUnits || (options.usePreferredUnits ? this.preferredUnits : null);
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();
out[`${type}.${variant}.${position}`] = this._resolveOutputValue(type, entry, requestedUnits);
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();
out[`${type}.${variant}.${position}.${childId}`] = this._resolveOutputValue(type, m, requestedUnits);
}
});
}
@@ -528,6 +649,18 @@ class MeasurementContainer {
Object.keys(this.measurements[this._currentType]) : [];
}
_resolveOutputValue(type, measurement, requestedUnits = null) {
const value = measurement.getCurrentValue();
if (!requestedUnits || value === null || typeof value === 'undefined') {
return value;
}
const targetUnit = requestedUnits[type];
if (!targetUnit) {
return value;
}
return this._convertValueToUnit(value, measurement.unit, targetUnit);
}
getPositions() {
if (!this._currentType || !this._currentVariant) {
if (this.logger) {
@@ -553,7 +686,7 @@ class MeasurementContainer {
// Helper method for value conversion
_convertValueToUnit(value, fromUnit, toUnit) {
if (!value || !fromUnit || !toUnit || fromUnit === toUnit) {
if ((value === null || typeof value === 'undefined') || !fromUnit || !toUnit || fromUnit === toUnit) {
return value;
}
@@ -572,19 +705,7 @@ class MeasurementContainer {
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];
const convertMeasure = this.measureMap[this._normalizeType(type)];
if (!convertMeasure) return [];
try {