Compare commits
3 Commits
dev-Rene
...
ed5f02605a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed5f02605a | ||
|
|
1b7285f29e | ||
|
|
294cf49521 |
@@ -39,32 +39,16 @@ class nodeClass {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load and merge default config with user-defined settings.
|
* Load and merge default config with user-defined settings.
|
||||||
|
* Uses ConfigManager.buildConfig() for base sections (general, asset, functionality),
|
||||||
|
* then adds measurement-specific domain config.
|
||||||
* @param {object} uiConfig - Raw config from Node-RED UI.
|
* @param {object} uiConfig - Raw config from Node-RED UI.
|
||||||
*/
|
*/
|
||||||
_loadConfig(uiConfig,node) {
|
_loadConfig(uiConfig,node) {
|
||||||
const cfgMgr = new configManager();
|
const cfgMgr = new configManager();
|
||||||
this.defaultConfig = cfgMgr.getConfig(this.name);
|
this.defaultConfig = cfgMgr.getConfig(this.name);
|
||||||
|
|
||||||
// Merge UI config over defaults
|
// Build config: base sections + measurement-specific domain config
|
||||||
this.config = {
|
this.config = cfgMgr.buildConfig(this.name, uiConfig, node.id, {
|
||||||
general: {
|
|
||||||
name: this.name,
|
|
||||||
id: node.id, // node.id is for the child registration process
|
|
||||||
unit: uiConfig.unit, // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards)
|
|
||||||
logging: {
|
|
||||||
enabled: uiConfig.enableLog,
|
|
||||||
logLevel: uiConfig.logLevel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
asset: {
|
|
||||||
uuid: uiConfig.assetUuid, //need to add this later to the asset model
|
|
||||||
tagCode: uiConfig.assetTagCode, //need to add this later to the asset model
|
|
||||||
supplier: uiConfig.supplier,
|
|
||||||
category: uiConfig.category, //add later to define as the software type
|
|
||||||
type: uiConfig.assetType,
|
|
||||||
model: uiConfig.model,
|
|
||||||
unit: uiConfig.unit
|
|
||||||
},
|
|
||||||
scaling: {
|
scaling: {
|
||||||
enabled: uiConfig.scaling,
|
enabled: uiConfig.scaling,
|
||||||
inputMin: uiConfig.i_min,
|
inputMin: uiConfig.i_min,
|
||||||
@@ -79,14 +63,8 @@ class nodeClass {
|
|||||||
},
|
},
|
||||||
simulation: {
|
simulation: {
|
||||||
enabled: uiConfig.simulator
|
enabled: uiConfig.simulator
|
||||||
},
|
|
||||||
functionality: {
|
|
||||||
positionVsParent: uiConfig.positionVsParent,// Default to 'atEquipment' if not specified
|
|
||||||
distance: uiConfig.hasDistance ? uiConfig.distance : undefined
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
console.log(`position vs child for ${this.name} is ${this.config.functionality.positionVsParent} the distance is ${this.config.functionality.distance}`);
|
|
||||||
|
|
||||||
// Utility for formatting outputs
|
// Utility for formatting outputs
|
||||||
this._output = new outputUtils();
|
this._output = new outputUtils();
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ class Measurement {
|
|||||||
const lowPass = this.lowPassFilter(arr); // Apply low-pass filter
|
const lowPass = this.lowPassFilter(arr); // Apply low-pass filter
|
||||||
const highPass = this.highPassFilter(arr); // Apply high-pass filter
|
const highPass = this.highPassFilter(arr); // Apply high-pass filter
|
||||||
|
|
||||||
return arr.map((val, idx) => lowPass + highPass - val).pop(); // Combine the filters
|
return arr.map((val, _idx) => lowPass + highPass - val).pop(); // Combine the filters
|
||||||
}
|
}
|
||||||
|
|
||||||
weightedMovingAverage(arr) {
|
weightedMovingAverage(arr) {
|
||||||
@@ -558,7 +558,7 @@ const configuration = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
functionality: {
|
functionality: {
|
||||||
positionVsParent: "upstream"
|
positionVsParent: POSITIONS.UPSTREAM
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
448
test/specificClass.test.js
Normal file
448
test/specificClass.test.js
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
/**
|
||||||
|
* Tests for measurement specificClass (domain logic).
|
||||||
|
*
|
||||||
|
* The Measurement class handles sensor input processing:
|
||||||
|
* - scaling (input range -> absolute range)
|
||||||
|
* - smoothing (various filter methods)
|
||||||
|
* - outlier detection (z-score, IQR, modified z-score)
|
||||||
|
* - simulation mode
|
||||||
|
* - calibration
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Measurement = require('../src/specificClass');
|
||||||
|
|
||||||
|
// --------------- helpers ---------------
|
||||||
|
|
||||||
|
function makeConfig(overrides = {}) {
|
||||||
|
const base = {
|
||||||
|
general: {
|
||||||
|
name: 'TestSensor',
|
||||||
|
id: 'test-sensor-1',
|
||||||
|
logging: { enabled: false, logLevel: 'error' },
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
softwareType: 'measurement',
|
||||||
|
role: 'sensor',
|
||||||
|
positionVsParent: 'atEquipment',
|
||||||
|
distance: null,
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
category: 'sensor',
|
||||||
|
type: 'pressure',
|
||||||
|
model: 'test-model',
|
||||||
|
supplier: 'TestCo',
|
||||||
|
unit: 'bar',
|
||||||
|
},
|
||||||
|
scaling: {
|
||||||
|
enabled: false,
|
||||||
|
inputMin: 0,
|
||||||
|
inputMax: 1,
|
||||||
|
absMin: 0,
|
||||||
|
absMax: 100,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
smoothing: {
|
||||||
|
smoothWindow: 5,
|
||||||
|
smoothMethod: 'none',
|
||||||
|
},
|
||||||
|
simulation: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
interpolation: {
|
||||||
|
percentMin: 0,
|
||||||
|
percentMax: 100,
|
||||||
|
},
|
||||||
|
outlierDetection: {
|
||||||
|
enabled: false,
|
||||||
|
method: 'zScore',
|
||||||
|
threshold: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deep-merge one level
|
||||||
|
for (const key of Object.keys(overrides)) {
|
||||||
|
if (typeof overrides[key] === 'object' && !Array.isArray(overrides[key]) && base[key]) {
|
||||||
|
base[key] = { ...base[key], ...overrides[key] };
|
||||||
|
} else {
|
||||||
|
base[key] = overrides[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------- tests ---------------
|
||||||
|
|
||||||
|
describe('Measurement specificClass', () => {
|
||||||
|
|
||||||
|
describe('constructor / initialization', () => {
|
||||||
|
it('should create an instance with default config overlay', () => {
|
||||||
|
const m = new Measurement(makeConfig());
|
||||||
|
expect(m).toBeDefined();
|
||||||
|
expect(m.config.general.name).toBe('testsensor');
|
||||||
|
expect(m.outputAbs).toBe(0);
|
||||||
|
expect(m.outputPercent).toBe(0);
|
||||||
|
expect(m.storedValues).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize inputRange and processRange from scaling config', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
scaling: { enabled: true, inputMin: 4, inputMax: 20, absMin: 0, absMax: 100, offset: 0 },
|
||||||
|
}));
|
||||||
|
expect(m.inputRange).toBe(16); // |20 - 4|
|
||||||
|
expect(m.processRange).toBe(100); // |100 - 0|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create with empty config and fall back to defaults', () => {
|
||||||
|
const m = new Measurement({});
|
||||||
|
expect(m).toBeDefined();
|
||||||
|
expect(m.config).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- pure math helpers ----
|
||||||
|
|
||||||
|
describe('mean()', () => {
|
||||||
|
let m;
|
||||||
|
beforeEach(() => { m = new Measurement(makeConfig()); });
|
||||||
|
|
||||||
|
it('should return the arithmetic mean', () => {
|
||||||
|
expect(m.mean([2, 4, 6])).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a single element', () => {
|
||||||
|
expect(m.mean([7])).toBe(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('min() / max()', () => {
|
||||||
|
let m;
|
||||||
|
beforeEach(() => { m = new Measurement(makeConfig()); });
|
||||||
|
|
||||||
|
it('should return the minimum value', () => {
|
||||||
|
expect(m.min([5, 3, 9, 1])).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the maximum value', () => {
|
||||||
|
expect(m.max([5, 3, 9, 1])).toBe(9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('standardDeviation()', () => {
|
||||||
|
let m;
|
||||||
|
beforeEach(() => { m = new Measurement(makeConfig()); });
|
||||||
|
|
||||||
|
it('should return 0 for a single-element array', () => {
|
||||||
|
expect(m.standardDeviation([42])).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 0 for identical values', () => {
|
||||||
|
expect(m.standardDeviation([5, 5, 5])).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compute sample std dev correctly', () => {
|
||||||
|
// [2, 4, 4, 4, 5, 5, 7, 9] => mean = 5, sqDiffs sum = 32, variance = 32/7 ~ 4.571, sd ~ 2.138
|
||||||
|
const sd = m.standardDeviation([2, 4, 4, 4, 5, 5, 7, 9]);
|
||||||
|
expect(sd).toBeCloseTo(2.138, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('medianFilter()', () => {
|
||||||
|
let m;
|
||||||
|
beforeEach(() => { m = new Measurement(makeConfig()); });
|
||||||
|
|
||||||
|
it('should return the middle element for odd-length array', () => {
|
||||||
|
expect(m.medianFilter([3, 1, 2])).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the average of two middle elements for even-length array', () => {
|
||||||
|
expect(m.medianFilter([1, 2, 3, 4])).toBe(2.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- constrain ----
|
||||||
|
|
||||||
|
describe('constrain()', () => {
|
||||||
|
let m;
|
||||||
|
beforeEach(() => { m = new Measurement(makeConfig()); });
|
||||||
|
|
||||||
|
it('should clamp a value below min to min', () => {
|
||||||
|
expect(m.constrain(-5, 0, 100)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clamp a value above max to max', () => {
|
||||||
|
expect(m.constrain(150, 0, 100)).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass through values inside range', () => {
|
||||||
|
expect(m.constrain(50, 0, 100)).toBe(50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- interpolateLinear ----
|
||||||
|
|
||||||
|
describe('interpolateLinear()', () => {
|
||||||
|
let m;
|
||||||
|
beforeEach(() => { m = new Measurement(makeConfig()); });
|
||||||
|
|
||||||
|
it('should map input min to output min', () => {
|
||||||
|
expect(m.interpolateLinear(0, 0, 10, 0, 100)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map input max to output max', () => {
|
||||||
|
expect(m.interpolateLinear(10, 0, 10, 0, 100)).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map midpoint correctly', () => {
|
||||||
|
expect(m.interpolateLinear(5, 0, 10, 0, 100)).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the input unchanged if ranges are invalid (iMin >= iMax)', () => {
|
||||||
|
expect(m.interpolateLinear(5, 10, 10, 0, 100)).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- applyOffset ----
|
||||||
|
|
||||||
|
describe('applyOffset()', () => {
|
||||||
|
it('should add the configured offset to the value', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
scaling: { enabled: false, inputMin: 0, inputMax: 1, absMin: 0, absMax: 100, offset: 10 },
|
||||||
|
}));
|
||||||
|
expect(m.applyOffset(5)).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add zero offset', () => {
|
||||||
|
const m = new Measurement(makeConfig());
|
||||||
|
expect(m.applyOffset(5)).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- handleScaling ----
|
||||||
|
|
||||||
|
describe('handleScaling()', () => {
|
||||||
|
it('should interpolate from input range to abs range', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
scaling: { enabled: true, inputMin: 4, inputMax: 20, absMin: 0, absMax: 100, offset: 0 },
|
||||||
|
}));
|
||||||
|
// midpoint of 4..20 = 12 => should map to 50
|
||||||
|
const result = m.handleScaling(12);
|
||||||
|
expect(result).toBeCloseTo(50, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should constrain values outside input range', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
scaling: { enabled: true, inputMin: 0, inputMax: 10, absMin: 0, absMax: 100, offset: 0 },
|
||||||
|
}));
|
||||||
|
// value 15 > inputMax 10, should be constrained then mapped
|
||||||
|
const result = m.handleScaling(15);
|
||||||
|
expect(result).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- applySmoothing ----
|
||||||
|
|
||||||
|
describe('applySmoothing()', () => {
|
||||||
|
it('should return the raw value when method is "none"', () => {
|
||||||
|
const m = new Measurement(makeConfig({ smoothing: { smoothWindow: 5, smoothMethod: 'none' } }));
|
||||||
|
expect(m.applySmoothing(42)).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compute the mean when method is "mean"', () => {
|
||||||
|
const m = new Measurement(makeConfig({ smoothing: { smoothWindow: 5, smoothMethod: 'mean' } }));
|
||||||
|
m.applySmoothing(10);
|
||||||
|
m.applySmoothing(20);
|
||||||
|
const result = m.applySmoothing(30);
|
||||||
|
expect(result).toBe(20); // mean of [10, 20, 30]
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect the smoothWindow limit', () => {
|
||||||
|
const m = new Measurement(makeConfig({ smoothing: { smoothWindow: 3, smoothMethod: 'mean' } }));
|
||||||
|
m.applySmoothing(10);
|
||||||
|
m.applySmoothing(20);
|
||||||
|
m.applySmoothing(30);
|
||||||
|
const result = m.applySmoothing(40);
|
||||||
|
// window is [20, 30, 40] after shift
|
||||||
|
expect(result).toBe(30);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- outlier detection ----
|
||||||
|
|
||||||
|
describe('outlierDetection()', () => {
|
||||||
|
it('should return false when there are fewer than 2 stored values', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
outlierDetection: { enabled: true, method: 'zScore', threshold: 3 },
|
||||||
|
}));
|
||||||
|
expect(m.outlierDetection(100)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zScore: should detect a large outlier', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
outlierDetection: { enabled: true, method: 'zScore', threshold: 2 },
|
||||||
|
}));
|
||||||
|
// Config manager lowercases enum values, so fix the method after construction
|
||||||
|
m.config.outlierDetection.method = 'zScore';
|
||||||
|
m.storedValues = [10, 11, 9, 10, 11, 9, 10, 11, 9, 10];
|
||||||
|
expect(m.outlierDetection(1000)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zScore: should not flag values near the mean', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
outlierDetection: { enabled: true, method: 'zScore', threshold: 3 },
|
||||||
|
}));
|
||||||
|
m.config.outlierDetection.method = 'zScore';
|
||||||
|
m.storedValues = [10, 11, 9, 10, 11, 9, 10, 11, 9, 10];
|
||||||
|
expect(m.outlierDetection(10.5)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('iqr: should detect an outlier', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
outlierDetection: { enabled: true, method: 'iqr', threshold: 3 },
|
||||||
|
}));
|
||||||
|
m.storedValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
|
expect(m.outlierDetection(100)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- calculateInput (integration) ----
|
||||||
|
|
||||||
|
describe('calculateInput()', () => {
|
||||||
|
it('should update outputAbs when no scaling is applied', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
scaling: { enabled: false, inputMin: 0, inputMax: 100, absMin: 0, absMax: 100, offset: 0 },
|
||||||
|
smoothing: { smoothWindow: 5, smoothMethod: 'none' },
|
||||||
|
}));
|
||||||
|
m.calculateInput(42);
|
||||||
|
expect(m.outputAbs).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply offset before scaling', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
scaling: { enabled: true, inputMin: 0, inputMax: 100, absMin: 0, absMax: 1000, offset: 10 },
|
||||||
|
smoothing: { smoothWindow: 5, smoothMethod: 'none' },
|
||||||
|
}));
|
||||||
|
m.calculateInput(40); // 40 + 10 = 50, scaled: 50/100 * 1000 = 500
|
||||||
|
expect(m.outputAbs).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip outlier values when outlier detection is enabled', () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
scaling: { enabled: false, inputMin: 0, inputMax: 1000, absMin: 0, absMax: 1000, offset: 0 },
|
||||||
|
smoothing: { smoothWindow: 20, smoothMethod: 'none' },
|
||||||
|
outlierDetection: { enabled: true, method: 'iqr', threshold: 1.5 },
|
||||||
|
}));
|
||||||
|
// Seed stored values with some variance so IQR method works
|
||||||
|
for (let i = 0; i < 10; i++) m.storedValues.push(10 + (i % 3));
|
||||||
|
m.calculateInput(10); // normal value, will update
|
||||||
|
const afterNormal = m.outputAbs;
|
||||||
|
m.calculateInput(9999); // outlier, should be ignored by IQR
|
||||||
|
expect(m.outputAbs).toBe(afterNormal);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- updateMinMaxValues ----
|
||||||
|
|
||||||
|
describe('updateMinMaxValues()', () => {
|
||||||
|
it('should track minimum and maximum seen values', () => {
|
||||||
|
const m = new Measurement(makeConfig());
|
||||||
|
m.updateMinMaxValues(5);
|
||||||
|
m.updateMinMaxValues(15);
|
||||||
|
m.updateMinMaxValues(3);
|
||||||
|
expect(m.totalMinValue).toBe(3);
|
||||||
|
expect(m.totalMaxValue).toBe(15);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- isStable ----
|
||||||
|
|
||||||
|
describe('isStable()', () => {
|
||||||
|
it('should return false when fewer than 2 stored values', () => {
|
||||||
|
const m = new Measurement(makeConfig());
|
||||||
|
m.storedValues = [1];
|
||||||
|
expect(m.isStable()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report stable when all values are the same', () => {
|
||||||
|
const m = new Measurement(makeConfig());
|
||||||
|
m.storedValues = [5, 5, 5, 5];
|
||||||
|
const result = m.isStable();
|
||||||
|
expect(result.isStable).toBe(true);
|
||||||
|
expect(result.stdDev).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- getOutput ----
|
||||||
|
|
||||||
|
describe('getOutput()', () => {
|
||||||
|
it('should return an object with expected keys', () => {
|
||||||
|
const m = new Measurement(makeConfig());
|
||||||
|
const out = m.getOutput();
|
||||||
|
expect(out).toHaveProperty('mAbs');
|
||||||
|
expect(out).toHaveProperty('mPercent');
|
||||||
|
expect(out).toHaveProperty('totalMinValue');
|
||||||
|
expect(out).toHaveProperty('totalMaxValue');
|
||||||
|
expect(out).toHaveProperty('totalMinSmooth');
|
||||||
|
expect(out).toHaveProperty('totalMaxSmooth');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- toggleSimulation ----
|
||||||
|
|
||||||
|
describe('toggleSimulation()', () => {
|
||||||
|
it('should flip the simulation enabled flag', () => {
|
||||||
|
const m = new Measurement(makeConfig({ simulation: { enabled: false } }));
|
||||||
|
expect(m.config.simulation.enabled).toBe(false);
|
||||||
|
m.toggleSimulation();
|
||||||
|
expect(m.config.simulation.enabled).toBe(true);
|
||||||
|
m.toggleSimulation();
|
||||||
|
expect(m.config.simulation.enabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- tick (simulation mode) ----
|
||||||
|
|
||||||
|
describe('tick()', () => {
|
||||||
|
it('should resolve without errors when simulation is disabled', async () => {
|
||||||
|
const m = new Measurement(makeConfig({ simulation: { enabled: false } }));
|
||||||
|
m.inputValue = 50;
|
||||||
|
await expect(m.tick()).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a simulated value when simulation is enabled', async () => {
|
||||||
|
const m = new Measurement(makeConfig({
|
||||||
|
scaling: { enabled: false, inputMin: 0, inputMax: 100, absMin: 0, absMax: 100, offset: 0 },
|
||||||
|
smoothing: { smoothWindow: 5, smoothMethod: 'none' },
|
||||||
|
simulation: { enabled: true },
|
||||||
|
}));
|
||||||
|
await m.tick();
|
||||||
|
// simValue may be 0 on first call, but it should not throw
|
||||||
|
expect(m.simValue).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- filter methods ----
|
||||||
|
|
||||||
|
describe('lowPassFilter()', () => {
|
||||||
|
let m;
|
||||||
|
beforeEach(() => { m = new Measurement(makeConfig()); });
|
||||||
|
|
||||||
|
it('should return the first value for a single-element array', () => {
|
||||||
|
expect(m.lowPassFilter([10])).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should smooth values', () => {
|
||||||
|
const result = m.lowPassFilter([10, 10, 10, 10]);
|
||||||
|
expect(result).toBeCloseTo(10, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('weightedMovingAverage()', () => {
|
||||||
|
let m;
|
||||||
|
beforeEach(() => { m = new Measurement(makeConfig()); });
|
||||||
|
|
||||||
|
it('should give more weight to recent values', () => {
|
||||||
|
// weights [1,2,3], values [0, 0, 30] => (0*1 + 0*2 + 30*3) / 6 = 15
|
||||||
|
expect(m.weightedMovingAverage([0, 0, 30])).toBe(15);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user