Compare commits
4 Commits
455f15dc55
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e35302b4 | ||
|
|
0a3a0be15b | ||
|
|
889221fffd | ||
|
|
a8d9895cbf |
@@ -322,6 +322,22 @@
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1230,
|
||||
"y": 300,
|
||||
"wires": []
|
||||
"wires": [],
|
||||
"interpolation": "linear",
|
||||
"xAxisPropertyType": "timestamp",
|
||||
"yAxisProperty": "payload",
|
||||
"yAxisPropertyType": "msg",
|
||||
"action": "append",
|
||||
"colors": [
|
||||
"#0095FF",
|
||||
"#FF0000",
|
||||
"#FF7F0E",
|
||||
"#2CA02C",
|
||||
"#A347E1",
|
||||
"#D62728",
|
||||
"#FF9896",
|
||||
"#9467BD",
|
||||
"#C5B0D5"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,6 +16,16 @@ function _logger(source, ctx) {
|
||||
return ctx?.logger || source?.logger || null;
|
||||
}
|
||||
|
||||
// Resolve the command origin (control authority: parent | GUI | fysical).
|
||||
// The shared commandRegistry stamps msg.origin (default 'parent'); legacy flows
|
||||
// carried the origin as payload.source. Prefer the legacy field when present so
|
||||
// existing flows keep working, otherwise use the registry-stamped msg.origin.
|
||||
function _origin(msg) {
|
||||
const p = msg && msg.payload;
|
||||
if (p && typeof p === 'object' && typeof p.source === 'string' && p.source) return p.source;
|
||||
return (typeof msg?.origin === 'string' && msg.origin) ? msg.origin : 'parent';
|
||||
}
|
||||
|
||||
function _send(ctx, ports) {
|
||||
if (typeof ctx?.send === 'function') ctx.send(ports);
|
||||
}
|
||||
@@ -28,19 +38,19 @@ exports.setMode = (source, msg) => {
|
||||
// forwards to these directly so behaviour is identical.
|
||||
exports.startup = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
await source.handleInput(p.source ?? 'parent', 'execSequence', 'startup');
|
||||
await source.handleInput(_origin(msg), 'execSequence', 'startup');
|
||||
};
|
||||
|
||||
exports.shutdown = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
await source.handleInput(p.source ?? 'parent', 'execSequence', 'shutdown');
|
||||
await source.handleInput(_origin(msg), 'execSequence', 'shutdown');
|
||||
};
|
||||
|
||||
exports.estop = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
// Legacy emergencystop carried { source, action } — action defaults to
|
||||
// 'emergencystop' when only source is supplied via the canonical topic.
|
||||
await source.handleInput(p.source ?? 'parent', p.action ?? 'emergencystop');
|
||||
await source.handleInput(_origin(msg), p.action ?? 'emergencystop');
|
||||
};
|
||||
|
||||
// Content-based alias router: legacy `execSequence` carried payload.action in
|
||||
@@ -57,13 +67,13 @@ exports.execSequenceAlias = async (source, msg, ctx) => {
|
||||
exports.setSetpoint = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
const action = p.action ?? 'execMovement';
|
||||
await source.handleInput(p.source ?? 'parent', action, Number(p.setpoint));
|
||||
await source.handleInput(_origin(msg), action, Number(p.setpoint));
|
||||
};
|
||||
|
||||
exports.setFlowSetpoint = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
const action = p.action ?? 'flowMovement';
|
||||
await source.handleInput(p.source ?? 'parent', action, Number(p.setpoint));
|
||||
await source.handleInput(_origin(msg), action, Number(p.setpoint));
|
||||
};
|
||||
|
||||
exports.simulateMeasurement = (source, msg, ctx) => {
|
||||
|
||||
@@ -63,7 +63,7 @@ module.exports = [
|
||||
topic: 'set.flow-setpoint',
|
||||
aliases: ['flowMovement'],
|
||||
payloadSchema: { type: 'object' },
|
||||
units: { measure: 'volumeFlowRate', default: 'm3/h' },
|
||||
unit: 'm3/h',
|
||||
description: 'Move the machine to a flow setpoint via flowMovement.',
|
||||
handler: handlers.setFlowSetpoint,
|
||||
},
|
||||
|
||||
@@ -11,6 +11,10 @@ class nodeClass extends BaseNodeAdapter {
|
||||
static commands = commands;
|
||||
static tickInterval = null;
|
||||
static statusInterval = 1000;
|
||||
// Realized control position holds constant in steady state, so delta
|
||||
// compression would emit it ~once and the Grafana "% Control" line goes
|
||||
// invisible. Force it every tick so the pump's movement always traces.
|
||||
static alwaysEmitFields = ['ctrl'];
|
||||
|
||||
buildDomainConfig(uiConfig) {
|
||||
_rejectLegacyAssetFields(uiConfig);
|
||||
|
||||
@@ -229,10 +229,18 @@ class Machine extends BaseDomain {
|
||||
this.measurements.type('temperature').variant('measured').position('atEquipment').value(15, Date.now(), tu);
|
||||
this.measurements.type('atmPressure').variant('measured').position('atEquipment').value(101325, Date.now(), 'Pa');
|
||||
const fu = this.unitPolicy.canonical.flow;
|
||||
const pu = this.unitPolicy.canonical.power;
|
||||
const fmin = this.predictFlow ? this.predictFlow.currentFxyYMin : 0;
|
||||
const fmax = this.predictFlow ? this.predictFlow.currentFxyYMax : 0;
|
||||
this.measurements.type('flow').variant('predicted').position('max').value(fmax, Date.now(), fu);
|
||||
this.measurements.type('flow').variant('predicted').position('min').value(fmin, Date.now(), fu);
|
||||
// Seed the operating-point series at boot so telemetry always carries them
|
||||
// (0 while idle, real values once calcFlow/calcPower run when operational).
|
||||
// Without this an idle-from-boot machine never emits these keys — the
|
||||
// dashboard can't even show the off/0 state. Mirrors max/min above.
|
||||
this.measurements.type('flow').variant('predicted').position('downstream').value(0, Date.now(), fu);
|
||||
this.measurements.type('flow').variant('predicted').position('atEquipment').value(0, Date.now(), fu);
|
||||
this.measurements.type('power').variant('predicted').position('atEquipment').value(0, Date.now(), pu);
|
||||
}
|
||||
|
||||
_callMeasurementHandler(measurementType, value, position, context = {}) {
|
||||
|
||||
@@ -36,6 +36,31 @@ test('getOutput contains all required fields in idle state', () => {
|
||||
assert.ok('pressureDriftFlags' in output);
|
||||
});
|
||||
|
||||
test('getOutput seeds operating-point flow/power telemetry at boot (idle = 0, not absent)', () => {
|
||||
// Regression: an idle-from-boot machine must still emit the operating-point
|
||||
// series so dashboards can show the off/0 state. These keys are otherwise
|
||||
// only written once the pump runs (calcFlow/calcPower) or on a state
|
||||
// transition, leaving them absent in telemetry for a pump that never starts.
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||
const output = machine.getOutput();
|
||||
|
||||
const hasPrefix = (p) => Object.keys(output).some((k) => k.startsWith(p));
|
||||
const valueFor = (p) => output[Object.keys(output).find((k) => k.startsWith(p))];
|
||||
|
||||
for (const prefix of [
|
||||
'flow.predicted.downstream',
|
||||
'flow.predicted.atequipment',
|
||||
'power.predicted.atequipment',
|
||||
]) {
|
||||
assert.ok(hasPrefix(prefix), `${prefix}.* must be present at boot (idle)`);
|
||||
assert.equal(valueFor(prefix), 0, `${prefix}.* should be 0 while idle`);
|
||||
}
|
||||
|
||||
// The envelope keys remain present too.
|
||||
assert.ok(hasPrefix('flow.predicted.max'));
|
||||
assert.ok(hasPrefix('flow.predicted.min'));
|
||||
});
|
||||
|
||||
test('getOutput flow drift fields appear after sufficient measured flow samples', async () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user