- Add 127 unit tests for measurement, pumpingStation, reactor, settler specificClass - Add 32 integration tests for parent-child registration flows - Fix pumpingStation tick() calling non-existent _calcTimeRemaining (was _calcRemainingTime) - Add API reference documentation for all generalFunctions modules Total tests: 536 (389 Jest + 23 node:test + 124 legacy), all passing Closes #17, #19, #20 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
15 KiB
generalFunctions API Reference
Shared library (nodes/generalFunctions/) used across all EVOLV Node-RED nodes.
const { logger, outputUtils, MeasurementContainer, ... } = require('generalFunctions');
Table of Contents
- Logger
- OutputUtils
- ValidationUtils
- MeasurementContainer
- ConfigManager
- ChildRegistrationUtils
- MenuUtils
- EndpointUtils
- Positions
- AssetLoader / loadCurve
Logger
Structured, level-filtered console logger.
File: src/helper/logger.js
Constructor
new Logger(logging = true, logLevel = 'debug', nameModule = 'N/A')
| Param | Type | Default | Description |
|---|---|---|---|
logging |
boolean |
true |
Enable/disable all output |
logLevel |
string |
'debug' |
Minimum severity: 'debug' | 'info' | 'warn' | 'error' |
nameModule |
string |
'N/A' |
Label prefixed to every message |
Methods
| Method | Signature | Description |
|---|---|---|
debug |
(message: string): void |
Log at DEBUG level |
info |
(message: string): void |
Log at INFO level |
warn |
(message: string): void |
Log at WARN level |
error |
(message: string): void |
Log at ERROR level |
setLogLevel |
(level: string): void |
Change minimum level at runtime |
toggleLogging |
(): void |
Flip logging on/off |
Example
const Logger = require('generalFunctions').logger;
const log = new Logger(true, 'info', 'MyNode');
log.info('Node started'); // [INFO] -> MyNode: Node started
log.debug('ignored'); // silent (below 'info')
log.setLogLevel('debug');
log.debug('now visible'); // [DEBUG] -> MyNode: now visible
OutputUtils
Tracks output state and formats messages for InfluxDB or process outputs. Only emits changed fields.
File: src/helper/outputUtils.js
Constructor
new OutputUtils() // no parameters
Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
formatMsg |
(output, config, format) |
object | undefined |
Diff against last output; returns formatted msg or undefined if nothing changed |
checkForChanges |
(output, format) |
object |
Returns only the key/value pairs that changed since last call |
format must be 'influxdb' or 'process'.
Example
const out = new OutputUtils();
const msg = out.formatMsg(
{ temperature: 22.5, pressure: 1013 },
config,
'influxdb'
);
// msg = { topic: 'nodeName', payload: { measurement, fields, tags, timestamp } }
ValidationUtils
Schema-driven config validation with type coercion, range clamping, and nested object support.
File: src/helper/validationUtils.js
Constructor
new ValidationUtils(loggerEnabled = true, loggerLevel = 'warn')
Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
validateSchema |
(config, schema, name) |
object |
Walk the schema, validate every field, return a clean config. Unknown keys are stripped. Missing keys get their schema default. |
constrain |
(value, min, max) |
number |
Clamp a numeric value to [min, max] |
removeUnwantedKeys |
(obj) |
object |
Strip rules/description metadata, collapse default values |
Supported rules.type values: number, integer, boolean, string, enum, array, set, object, curve, machineCurve.
Example
const ValidationUtils = require('generalFunctions').validation;
const v = new ValidationUtils(true, 'warn');
const schema = {
temperature: { default: 20, rules: { type: 'number', min: -40, max: 100 } },
unit: { default: 'C', rules: { type: 'enum', values: [{ value: 'C' }, { value: 'F' }] } }
};
const validated = v.validateSchema({ temperature: 999 }, schema, 'myNode');
// validated.temperature === 100 (clamped)
// validated.unit === 'C' (default applied)
MeasurementContainer
Chainable measurement storage organised by type / variant / position. Supports auto unit conversion, windowed statistics, events, and positional difference calculations.
File: src/measurements/MeasurementContainer.js
Constructor
new MeasurementContainer(options = {}, logger)
| Option | Type | Default | Description |
|---|---|---|---|
windowSize |
number |
10 |
Rolling window for statistics |
defaultUnits |
object |
{ pressure:'mbar', flow:'m3/h', ... } |
Default unit per measurement type |
autoConvert |
boolean |
true |
Auto-convert values to target unit |
preferredUnits |
object |
{} |
Per-type unit overrides |
Chainable Setters
All return this for chaining.
container
.type('pressure')
.variant('static')
.position('upstream')
.distance(5)
.unit('bar')
.value(3.2, Date.now(), 'bar');
| Method | Signature | Description |
|---|---|---|
type |
(typeName): this |
Set measurement type (e.g. 'pressure') |
variant |
(variantName): this |
Set variant (e.g. 'static', 'differential') |
position |
(positionValue): this |
Set position (e.g. 'upstream', 'downstream') |
distance |
(distance): this |
Set physical distance from parent |
unit |
(unitName): this |
Set unit on the underlying measurement |
value |
(val, timestamp?, sourceUnit?): this |
Store a value; auto-converts if sourceUnit differs from target |
Terminal / Query Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
get |
() |
Measurement | null |
Get the raw measurement object |
getCurrentValue |
(requestedUnit?) |
number | null |
Latest value, optionally converted |
getAverage |
(requestedUnit?) |
number | null |
Windowed average |
getMin |
() |
number | null |
Window minimum |
getMax |
() |
number | null |
Window maximum |
getAllValues |
() |
array | null |
All stored samples |
getLaggedValue |
(lag?, requestedUnit?) |
number | null |
Value from lag samples ago |
getLaggedSample |
(lag?, requestedUnit?) |
object | null |
Full sample { value, timestamp, unit } from lag samples ago |
exists |
({ type?, variant?, position?, requireValues? }) |
boolean |
Check if a measurement series exists |
difference |
({ from?, to?, unit? }) |
object | null |
Compute { value, avgDiff, unit } between two positions |
Introspection / Lifecycle
| Method | Signature | Returns | Description |
|---|---|---|---|
getTypes |
() |
string[] |
All registered measurement types |
getVariants |
() |
string[] |
Variants under current type |
getPositions |
() |
string[] |
Positions under current type+variant |
getAvailableUnits |
(measurementType?) |
string[] |
Units available for a type |
getBestUnit |
(excludeUnits?) |
object | null |
Best human-readable unit for current value |
setPreferredUnit |
(type, unit) |
this |
Override default unit for a type |
setChildId |
(id) |
this |
Tag container with a child node ID |
setChildName |
(name) |
this |
Tag container with a child node name |
setParentRef |
(parent) |
this |
Store reference to parent node |
clear |
() |
void |
Reset all measurements and chain state |
Events
The internal emitter fires "type.variant.position" on every value() call with:
{ value, originalValue, unit, sourceUnit, timestamp, position, distance, variant, type, childId, childName, parentRef }
Example
const { MeasurementContainer } = require('generalFunctions');
const mc = new MeasurementContainer({ windowSize: 5 });
mc.type('pressure').variant('static').position('upstream').value(3.2);
mc.type('pressure').variant('static').position('downstream').value(2.8);
const diff = mc.type('pressure').variant('static').difference();
// diff = { value: -0.4, avgDiff: -0.4, unit: 'mbar', from: 'downstream', to: 'upstream' }
ConfigManager
Loads JSON config files from disk and builds merged runtime configs.
File: src/configs/index.js
Constructor
new ConfigManager(relPath = '.')
relPath is resolved relative to the configs directory.
Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
getConfig |
(configName) |
object |
Load and parse <configName>.json |
getAvailableConfigs |
() |
string[] |
List config names (without .json) |
hasConfig |
(configName) |
boolean |
Check existence |
getBaseConfig |
() |
object |
Shortcut for getConfig('baseConfig') |
buildConfig |
(nodeName, uiConfig, nodeId, domainConfig?) |
object |
Merge base schema + UI overrides into a runtime config |
createEndpoint |
(nodeName) |
string |
Generate browser JS that injects config into window.EVOLV.nodes |
Example
const { configManager } = require('generalFunctions');
const cfg = configManager.buildConfig('measurement', uiConfig, node.id, {
scaling: { enabled: true, inputMin: 0, inputMax: 100 }
});
ChildRegistrationUtils
Manages parent-child node relationships: registration, lookup, and structure storage.
File: src/helper/childRegistrationUtils.js
Constructor
new ChildRegistrationUtils(mainClass)
mainClass is the parent node instance (must expose .logger and optionally .registerChild()).
Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
registerChild |
(child, positionVsParent, distance?) |
Promise<any> |
Register a child node under the parent. Sets up parent refs, measurement context, and stores by softwareType/category. |
getChildrenOfType |
(softwareType, category?) |
array |
Get children filtered by software type and optional category |
getChildById |
(childId) |
object | null |
Lookup a single child by its ID |
getAllChildren |
() |
array |
All registered children |
logChildStructure |
() |
void |
Debug-print the full child tree |
Example
const { childRegistrationUtils: CRU } = require('generalFunctions');
const cru = new CRU(parentNode);
await cru.registerChild(sensorNode, 'upstream');
cru.getChildrenOfType('measurement'); // [sensorNode]
MenuUtils
Browser-side UI helper for Node-RED editor. Methods are mixed in from separate modules: toggles, data fetching, URL utils, dropdown population, and HTML generation.
File: src/helper/menuUtils.js
Constructor
new MenuUtils() // no parameters; sets isCloud=false, configData=null
Key Methods
Toggles -- control UI element visibility:
| Method | Signature | Description |
|---|---|---|
initBasicToggles |
(elements) |
Bind log-level row visibility to log checkbox |
initMeasurementToggles |
(elements) |
Bind scaling input rows to scaling checkbox |
initTensionToggles |
(elements, node) |
Show/hide tension row based on interpolation method |
Data Fetching:
| Method | Signature | Returns | Description |
|---|---|---|---|
fetchData |
(url, fallbackUrl) |
Promise<array> |
Fetch JSON from primary URL; fall back on failure |
fetchProjectData |
(url) |
Promise<object> |
Fetch project-level data |
apiCall |
(node) |
Promise<object> |
POST to asset-register API |
URL Construction:
| Method | Signature | Returns | Description |
|---|---|---|---|
getSpecificConfigUrl |
(nodeName, cloudAPI) |
{ cloudConfigURL, localConfigURL } |
Build cloud + local config URLs |
constructUrl |
(base, ...paths) |
string |
Join URL segments safely |
constructCloudURL |
(base, ...paths) |
string |
Same as constructUrl, for cloud endpoints |
Dropdown Population:
| Method | Signature | Description |
|---|---|---|
fetchAndPopulateDropdowns |
(configUrls, elements, node) |
Cascading supplier > subType > model > unit dropdowns |
populateDropdown |
(htmlElement, options, node, property, callback?) |
Fill a <select> with options and wire change events |
populateLogLevelOptions |
(logLevelSelect, configData, node) |
Populate log-level dropdown from config |
populateSmoothingMethods |
(configUrls, elements, node) |
Populate smoothing method dropdown |
populateInterpolationMethods |
(configUrls, elements, node) |
Populate interpolation method dropdown |
generateHtml |
(htmlElement, options, savedValue) |
Write <option> HTML into an element |
EndpointUtils
Server-side helper that serves MenuUtils as browser JavaScript via Node-RED HTTP endpoints.
File: src/helper/endpointUtils.js
Constructor
new EndpointUtils({ MenuUtilsClass? })
| Param | Type | Default | Description |
|---|---|---|---|
MenuUtilsClass |
class |
MenuUtils |
The MenuUtils constructor to introspect |
Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
createMenuUtilsEndpoint |
(RED, nodeName, customHelpers?) |
void |
Register GET /<nodeName>/resources/menuUtils.js |
generateMenuUtilsCode |
(nodeName, customHelpers?) |
string |
Produce the browser JS string (introspects MenuUtils.prototype) |
Example
const EndpointUtils = require('generalFunctions/src/helper/endpointUtils');
const ep = new EndpointUtils();
ep.createMenuUtilsEndpoint(RED, 'valve');
// Browser can now load: GET /valve/resources/menuUtils.js
Positions
Canonical constants for parent-child spatial relationships.
File: src/constants/positions.js
Exports
const { POSITIONS, POSITION_VALUES, isValidPosition } = require('generalFunctions');
| Export | Type | Value |
|---|---|---|
POSITIONS |
object |
{ UPSTREAM: 'upstream', DOWNSTREAM: 'downstream', AT_EQUIPMENT: 'atEquipment', DELTA: 'delta' } |
POSITION_VALUES |
string[] |
['upstream', 'downstream', 'atEquipment', 'delta'] |
isValidPosition |
(pos: string): boolean |
Returns true if pos is one of the four values |
AssetLoader / loadCurve
Loads JSON asset files (machine curves, etc.) from the datasets directory with LRU caching.
File: datasets/assetData/curves/index.js
Singleton convenience functions
const { loadCurve } = require('generalFunctions');
| Function | Signature | Returns | Description |
|---|---|---|---|
loadCurve |
(curveType: string) |
object | null |
Load <curveType>.json from the curves directory |
loadAsset |
(datasetType, assetId) |
object | null |
Load any JSON asset by dataset folder and ID |
getAvailableAssets |
(datasetType) |
string[] |
List asset IDs in a dataset folder |
AssetLoader class
new AssetLoader(maxCacheSize = 100)
Same methods as above (loadCurve, loadAsset, getAvailableAssets), plus clearCache().
Example
const { loadCurve } = require('generalFunctions');
const curve = loadCurve('hidrostal-H05K-S03R');
// curve = { flow: [...], head: [...], ... } or null