agent updates

This commit is contained in:
znetsixe
2026-01-13 15:38:59 +01:00
parent 3971b4e328
commit 9708127a20
5 changed files with 243 additions and 49 deletions

View File

@@ -107,6 +107,30 @@ class Monster{
} }
_syncOutput() {
this.output = this.output || {};
this.output.pulse = this.pulse;
this.output.running = this.running;
this.output.bucketVol = this.bucketVol;
this.output.bucketWeight = this.bucketWeight;
this.output.sumPuls = this.sumPuls;
this.output.predFlow = this.predFlow;
this.output.predM3PerSec = this.predM3PerSec;
this.output.timePassed = this.timePassed;
this.output.timeLeft = this.timeLeft;
this.output.m3Total = this.m3Total;
this.output.q = this.q;
this.output.maxVolume = this.maxVolume;
this.output.minVolume = this.minVolume;
this.output.nextDate = this.nextDate;
this.output.daysPerYear = this.daysPerYear;
}
getOutput() {
this._syncOutput();
return this.output;
}
/*------------------- GETTER/SETTERS Dynamics -------------------*/ /*------------------- GETTER/SETTERS Dynamics -------------------*/
set monsternametijden(value){ set monsternametijden(value){
@@ -351,6 +375,19 @@ zip(...arrays) {
} }
get_model_prediction(){ get_model_prediction(){
// Offline-safe fallback: assume constant inflow `q` during `sampling_time`.
// `q` is in m3/h; `sampling_time` is in hours; result is total predicted volume in m3.
const samplingHours = Number(this.sampling_time) || 0;
const flowM3PerHour = Number(this.q) || 0;
const fallback = Math.max(0, flowM3PerHour * samplingHours);
this.predFlow = fallback;
this._syncOutput();
return this.predFlow;
}
// Legacy/experimental model-based prediction (kept for reference; not used by default).
get_model_prediction_from_rain(){
// combine 24 hourly predictions to make one daily prediction (for the next 24 hours including the current hour) // combine 24 hourly predictions to make one daily prediction (for the next 24 hours including the current hour)
let inputs = []; let inputs = [];
@@ -591,6 +628,8 @@ async model_loader(inputs){
//logQ for predictions / forecasts //logQ for predictions / forecasts
this.logQoverTime(); this.logQoverTime();
this._syncOutput();
} }
regNextDate(monsternametijden){ regNextDate(monsternametijden){
@@ -675,6 +714,8 @@ async model_loader(inputs){
module.exports = Monster; module.exports = Monster;
// Local smoke-test harness (kept for debugging) should not run when this file is imported by Node-RED.
if (require.main === module) {
const mConfig={ const mConfig={
general: { general: {
@@ -704,3 +745,5 @@ monster.get_model_prediction();
//const intervalId = setInterval(() => {monster.tick();},1000) //const intervalId = setInterval(() => {monster.tick();},1000)
}) })
}

View File

@@ -8,6 +8,9 @@
color: "#4f8582", color: "#4f8582",
defaults: { defaults: {
// Define default properties
name: { value: "" },
// Define specific properties // Define specific properties
samplingtime: { value: 0 }, samplingtime: { value: 0 },
minvolume: { value: 5 }, minvolume: { value: 5 },

View File

@@ -41,10 +41,13 @@ class nodeClass {
* @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();
this.defaultConfig = cfgMgr.getConfig(this.name);
// Merge UI config over defaults // Merge UI config over defaults
this.config = { this.config = {
general: { general: {
name: uiConfig.name || uiConfig.category || this.name,
id: node.id, // node.id is for the child registration process 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) 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: { logging: {
@@ -53,16 +56,23 @@ class nodeClass {
} }
}, },
asset: { asset: {
uuid: uiConfig.assetUuid, //need to add this later to the asset model uuid: uiConfig.uuid,
tagCode: uiConfig.assetTagCode, //need to add this later to the asset model tagCode: uiConfig.assetTagCode,
supplier: uiConfig.supplier, supplier: uiConfig.supplier,
category: uiConfig.category, //add later to define as the software type category: uiConfig.category, //add later to define as the software type
type: uiConfig.assetType, type: uiConfig.assetType,
model: uiConfig.model, model: uiConfig.model,
unit: uiConfig.unit unit: uiConfig.unit,
emptyWeightBucket: Number(uiConfig.emptyWeightBucket)
},
constraints: {
samplingtime: Number(uiConfig.samplingtime),
minVolume: Number(uiConfig.minvolume),
maxWeight: Number(uiConfig.maxweight),
}, },
functionality: { functionality: {
positionVsParent: uiConfig.positionVsParent positionVsParent: uiConfig.positionVsParent || 'atEquipment',
distance: uiConfig.hasDistance ? uiConfig.distance : undefined
} }
}; };
@@ -78,6 +88,10 @@ class nodeClass {
this.source = new Specific(monsterConfig); this.source = new Specific(monsterConfig);
if (uiConfig?.aquon_sample_name) {
this.source.aquonSampleName = uiConfig.aquon_sample_name;
}
//store in node //store in node
this.node.source = this.source; // Store the source in the node instance for easy access this.node.source = this.source; // Store the source in the node instance for easy access
@@ -111,7 +125,7 @@ try{
return status; return status;
} catch (error) { } catch (error) {
node.error("Error in updateNodeStatus: " + error); this.node.error("Error in updateNodeStatus: " + error);
return { fill: "red", shape: "ring", text: "Status Error" }; return { fill: "red", shape: "ring", text: "Status Error" };
} }
} }
@@ -166,41 +180,52 @@ try{
this.node.on('input', (msg, send, done) => { this.node.on('input', (msg, send, done) => {
/* Update to complete event based node by putting the tick function after an input event */ /* Update to complete event based node by putting the tick function after an input event */
const m = this.source; const m = this.source;
try {
switch(msg.topic) { switch(msg.topic) {
case 'registerChild': case 'registerChild': {
// Register this node as a child of the parent node
const childId = msg.payload; const childId = msg.payload;
const childObj = this.RED.nodes.getNode(childId); const childObj = this.RED.nodes.getNode(childId);
m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); if (childObj?.source) {
m.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
}
break; break;
}
case 'setMode': case 'setMode':
m.setMode(msg.payload); m.setMode(msg.payload);
break; break;
case 'execSequence': case 'execSequence': {
const { source, action, parameter } = msg.payload; const { source, action, parameter } = msg.payload || {};
m.handleInput(source, action, parameter); m.handleInput(source, action, parameter);
break; break;
case 'execMovement': }
const { source: mvSource, action: mvAction, setpoint } = msg.payload; case 'execMovement': {
const { source: mvSource, action: mvAction, setpoint } = msg.payload || {};
m.handleInput(mvSource, mvAction, Number(setpoint)); m.handleInput(mvSource, mvAction, Number(setpoint));
break; break;
case 'flowMovement': }
const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload; case 'flowMovement': {
const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload || {};
m.handleInput(fmSource, fmAction, Number(fmSetpoint)); m.handleInput(fmSource, fmAction, Number(fmSetpoint));
break; break;
case 'emergencystop': }
const { source: esSource, action: esAction } = msg.payload; case 'emergencystop': {
const { source: esSource, action: esAction } = msg.payload || {};
m.handleInput(esSource, esAction); m.handleInput(esSource, esAction);
break; break;
}
case 'showWorkingCurves': case 'showWorkingCurves':
m.showWorkingCurves();
send({ topic : "Showing curve" , payload: m.showWorkingCurves() }); send({ topic : "Showing curve" , payload: m.showWorkingCurves() });
break; break;
case 'CoG': case 'CoG':
m.showCoG();
send({ topic : "Showing CoG" , payload: m.showCoG() }); send({ topic : "Showing CoG" , payload: m.showCoG() });
break; break;
default:
break;
}
} catch (error) {
this.node.error(`Error handling input (${msg?.topic}): ${error?.message || error}`);
} finally {
done();
} }
}); });
} }

2
src/specificClass.js Normal file
View File

@@ -0,0 +1,2 @@
module.exports = require("../dependencies/monster/SpeficicClass");

121
test/monster.test.js Normal file
View File

@@ -0,0 +1,121 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const Monster = require('../src/specificClass');
function createConfig(overrides = {}) {
return {
general: {
name: 'monster-test',
logging: { enabled: false, logLevel: 'error' },
},
asset: {
emptyWeightBucket: 3,
...overrides.asset,
},
constraints: {
samplingtime: 24,
minVolume: 5,
maxWeight: 23,
...overrides.constraints,
},
...overrides,
};
}
test('constructor derives boundaries and targets from config', () => {
const monster = new Monster(createConfig());
assert.equal(monster.maxVolume, 20);
assert.equal(monster.minPuls, 100);
assert.equal(monster.maxPuls, 400);
assert.equal(monster.absMaxPuls, 1100);
assert.equal(monster.targetVolume, 10);
assert.equal(monster.targetPuls, 200);
assert.equal(monster.running, false);
});
test('bucket volume updates output and bucket weight', () => {
const monster = new Monster(createConfig());
monster.bucketVol = 1.5;
assert.equal(monster.bucketVol, 1.5);
assert.equal(monster.bucketWeight, 4.5);
const out = monster.getOutput();
assert.equal(out.bucketVol, 1.5);
assert.equal(out.bucketWeight, 4.5);
});
test('monsternametijden setter registers next date for matching sample', () => {
const monster = new Monster(createConfig());
const future = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
monster.monsternametijden = [
{
SAMPLE_NAME: monster.aquonSampleName,
DESCRIPTION: 'test',
SAMPLED_DATE: future,
START_DATE: future,
END_DATE: future,
},
];
assert.ok(monster.nextDate >= Date.now());
assert.ok(monster.daysPerYear >= 1);
});
test('sampling_program starts and emits pulses based on m3PerTick', () => {
const monster = new Monster(
createConfig({
constraints: {
samplingtime: 1,
minVolume: 0.1,
maxWeight: 23,
},
})
);
monster.nextDate = Date.now() + 60_000;
monster.i_start = true;
monster.q = 28; // => predFlow = 28 m3 for 1 hour (fallback)
monster.m3PerTick = 1; // with m3PerPuls ~1 this should trigger a pulse
monster.sampling_program();
assert.equal(monster.running, true);
assert.equal(monster.sumPuls, 1);
assert.equal(monster.pulse, true);
assert.equal(monster.bucketVol, 0.05);
// Next loop without flow should stop pulsing
monster.m3PerTick = 0;
monster.sampling_program();
assert.equal(monster.pulse, false);
});
test('sampling_program stops and resets after stop_time has passed', () => {
const monster = new Monster(
createConfig({
constraints: { samplingtime: 1, minVolume: 0.1, maxWeight: 23 },
})
);
monster.running = true;
monster.stop_time = Date.now() - 1;
monster.sumPuls = 10;
monster.bucketVol = 0.5;
monster.predFlow = 123;
monster.predM3PerSec = 1;
monster.m3Total = 10;
monster.sampling_program();
assert.equal(monster.running, false);
assert.equal(monster.sumPuls, 0);
assert.equal(monster.bucketVol, 0);
assert.equal(monster.predFlow, 0);
assert.equal(monster.predM3PerSec, 0);
assert.equal(monster.m3Total, 0);
});