pumpingStation schema: shiftArmPercent + MeasurementContainer .default doc
- control.levelbased.shiftArmPercent (default 95): output % threshold that
arms the shift on the way up. Once armed, the up-curve % at the
filling→draining transition becomes the held value, kept until level
drops to shiftLevel; from there it ramps to 0 % at startLevel.
- shiftLevel description updated — it is no longer the arming trigger,
it's the level at which the held output begins ramping down.
- MeasurementContainer.js: prominent doc block on the class plus a
JSDoc on getFlattenedOutput documenting the `${type}.${variant}.
${position}.${childId}` flatten format and the implicit 'default'
childId convention. This was the #1 footgun for new dashboard
consumers — the comments now make the rule impossible to miss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,51 @@ const EventEmitter = require('events');
|
||||
const convertModule = require('../convert/index');
|
||||
const { POSITIONS } = require('../constants/positions');
|
||||
|
||||
/* ============================================================================
|
||||
* MeasurementContainer — measurement storage with chainable type/variant/
|
||||
* position/child addressing.
|
||||
*
|
||||
* INTERNAL STORAGE SHAPE
|
||||
* measurements[type][variant][position][childId] = Measurement instance
|
||||
*
|
||||
* The childId layer is ALWAYS present, even when the caller doesn't specify
|
||||
* one. _getOrCreateMeasurement defaults childId to 'default' when no
|
||||
* .child(...) is in the chain. So writing
|
||||
*
|
||||
* mc.type('level').variant('measured').position('atequipment')
|
||||
* .value(2.5, ts, 'm');
|
||||
*
|
||||
* stores the value at measurements.level.measured.atequipment.default.
|
||||
*
|
||||
* READING — the chainable getters resolve the default child transparently,
|
||||
* so consumers usually don't see it:
|
||||
*
|
||||
* mc.type('level').variant('measured').position('atequipment')
|
||||
* .getCurrentValue('m'); // returns 2.5
|
||||
*
|
||||
* FLATTENED OUTPUT — getFlattenedOutput() emits ONE key per child, including
|
||||
* the implicit 'default' bucket:
|
||||
*
|
||||
* {
|
||||
* 'level.measured.atequipment.default': 2.5, // implicit child
|
||||
* 'flow.predicted.in.manual-qin': 0.05, // explicit .child('manual-qin')
|
||||
* 'flow.predicted.in.from-pump-A': 0.03,
|
||||
* …
|
||||
* }
|
||||
*
|
||||
* ⚠ DASHBOARDS / DOWNSTREAM PARSERS MUST INCLUDE THE CHILD KEY
|
||||
* The flat key format is `${type}.${variant}.${position}.${childId}`.
|
||||
* When you have not used .child(), the childId is the literal string
|
||||
* 'default'. Use 'level.measured.atequipment.default', NOT
|
||||
* 'level.measured.atequipment'. This trips up new consumers — see the
|
||||
* pumpingStation basic-dashboard parser for an example that gets it right.
|
||||
*
|
||||
* AGGREGATION — sum() folds all children of a position into one number:
|
||||
*
|
||||
* mc.sum('flow', 'predicted', ['in'], 'm3/s');
|
||||
* // = manual-qin + from-pump-A + … + (default if any)
|
||||
* ============================================================================
|
||||
*/
|
||||
class MeasurementContainer {
|
||||
constructor(options = {},logger) {
|
||||
this.logger = logger || null;
|
||||
@@ -535,18 +580,43 @@ class MeasurementContainer {
|
||||
.reduce((acc, v) => acc + v, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten the entire container to a key→value map, suitable for
|
||||
* dashboards / InfluxDB / debug dumps.
|
||||
*
|
||||
* KEY FORMAT — child-bucketed series (the common case):
|
||||
* `${type}.${variant}.${position}.${childId}`
|
||||
*
|
||||
* Even measurements written without an explicit `.child(...)` end up
|
||||
* here under `childId === 'default'` (see _getOrCreateMeasurement).
|
||||
* Examples:
|
||||
* level.measured.atequipment.default // implicit child
|
||||
* flow.predicted.in.manual-qin // explicit child
|
||||
* flow.predicted.in.from-pump-A // explicit child
|
||||
*
|
||||
* Consumers (Node-RED dashboards, parsers) MUST include the trailing
|
||||
* `.default` when reading default-bucket measurements. Stripping it
|
||||
* silently misses the value. This is the #1 footgun for new code that
|
||||
* uses MeasurementContainer.
|
||||
*
|
||||
* The "Legacy single series" branch below catches a pre-v2 storage
|
||||
* shape where a position held a Measurement directly (no child layer);
|
||||
* new code never produces that shape but old serialized state may.
|
||||
*/
|
||||
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
|
||||
// Legacy single series (no childId layer)
|
||||
if (entry?.getCurrentValue) {
|
||||
out[`${type}.${variant}.${position}`] = this._resolveOutputValue(type, entry, requestedUnits);
|
||||
return;
|
||||
}
|
||||
// Child-bucketed series
|
||||
// Child-bucketed series — ALWAYS the case for new writes,
|
||||
// including the implicit 'default' bucket when no .child() is
|
||||
// used. The flat key carries the childId.
|
||||
if (entry && typeof entry === 'object') {
|
||||
Object.entries(entry).forEach(([childId, m]) => {
|
||||
if (m?.getCurrentValue) {
|
||||
|
||||
Reference in New Issue
Block a user