From 9708127a2006bff545cd6d47f610b434ba52dc01 Mon Sep 17 00:00:00 2001
From: znetsixe <73483679+znetsixe@users.noreply.github.com>
Date: Tue, 13 Jan 2026 15:38:59 +0100
Subject: [PATCH] agent updates
---
dependencies/monster/SpeficicClass.js | 49 ++++++++++-
monster.html | 15 ++--
src/nodeClass.js | 105 +++++++++++++---------
src/specificClass.js | 2 +
test/monster.test.js | 121 ++++++++++++++++++++++++++
5 files changed, 243 insertions(+), 49 deletions(-)
create mode 100644 src/specificClass.js
create mode 100644 test/monster.test.js
diff --git a/dependencies/monster/SpeficicClass.js b/dependencies/monster/SpeficicClass.js
index eeb9c21..cfc8b30 100644
--- a/dependencies/monster/SpeficicClass.js
+++ b/dependencies/monster/SpeficicClass.js
@@ -105,10 +105,34 @@ class Monster{
this.set_boundries_and_targets();
- }
+ }
- /*------------------- GETTER/SETTERS Dynamics -------------------*/
- set monsternametijden(value){
+ _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 -------------------*/
+ set monsternametijden(value){
if(this.init){
if(Object.keys(value).length > 0){
@@ -351,6 +375,19 @@ zip(...arrays) {
}
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)
let inputs = [];
@@ -591,6 +628,8 @@ async model_loader(inputs){
//logQ for predictions / forecasts
this.logQoverTime();
+
+ this._syncOutput();
}
regNextDate(monsternametijden){
@@ -675,6 +714,8 @@ async model_loader(inputs){
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={
general: {
@@ -704,3 +745,5 @@ monster.get_model_prediction();
//const intervalId = setInterval(() => {monster.tick();},1000)
})
+}
+
diff --git a/monster.html b/monster.html
index 90e425e..a9d205c 100644
--- a/monster.html
+++ b/monster.html
@@ -6,12 +6,15 @@
RED.nodes.registerType("monster", {
category: "EVOLV",
color: "#4f8582",
- defaults: {
+ defaults: {
- // Define specific properties
- samplingtime: { value: 0 },
- minvolume: { value: 5 },
- maxweight: { value: 22 },
+ // Define default properties
+ name: { value: "" },
+
+ // Define specific properties
+ samplingtime: { value: 0 },
+ minvolume: { value: 5 },
+ maxweight: { value: 22 },
emptyWeightBucket: { value: 3 },
aquon_sample_name: { value: "" },
@@ -140,4 +143,4 @@
-
\ No newline at end of file
+
diff --git a/src/nodeClass.js b/src/nodeClass.js
index a72ca9c..bbe71af 100644
--- a/src/nodeClass.js
+++ b/src/nodeClass.js
@@ -41,10 +41,13 @@ class nodeClass {
* @param {object} uiConfig - Raw config from Node-RED UI.
*/
_loadConfig(uiConfig,node) {
+ const cfgMgr = new configManager();
+ this.defaultConfig = cfgMgr.getConfig(this.name);
// Merge UI config over defaults
this.config = {
general: {
+ name: uiConfig.name || uiConfig.category || 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: {
@@ -53,16 +56,23 @@ class nodeClass {
}
},
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
+ uuid: uiConfig.uuid,
+ tagCode: uiConfig.assetTagCode,
supplier: uiConfig.supplier,
category: uiConfig.category, //add later to define as the software type
type: uiConfig.assetType,
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: {
- positionVsParent: uiConfig.positionVsParent
+ positionVsParent: uiConfig.positionVsParent || 'atEquipment',
+ distance: uiConfig.hasDistance ? uiConfig.distance : undefined
}
};
@@ -78,6 +88,10 @@ class nodeClass {
this.source = new Specific(monsterConfig);
+ if (uiConfig?.aquon_sample_name) {
+ this.source.aquonSampleName = uiConfig.aquon_sample_name;
+ }
+
//store in node
this.node.source = this.source; // Store the source in the node instance for easy access
@@ -111,7 +125,7 @@ try{
return status;
} catch (error) {
- node.error("Error in updateNodeStatus: " + error);
+ this.node.error("Error in updateNodeStatus: " + error);
return { fill: "red", shape: "ring", text: "Status Error" };
}
}
@@ -166,42 +180,53 @@ try{
this.node.on('input', (msg, send, done) => {
/* Update to complete event based node by putting the tick function after an input event */
const m = this.source;
- switch(msg.topic) {
- case 'registerChild':
- // Register this node as a child of the parent node
- const childId = msg.payload;
- const childObj = this.RED.nodes.getNode(childId);
- m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
- break;
- case 'setMode':
- m.setMode(msg.payload);
- break;
- case 'execSequence':
- const { source, action, parameter } = msg.payload;
- m.handleInput(source, action, parameter);
- break;
- case 'execMovement':
- const { source: mvSource, action: mvAction, setpoint } = msg.payload;
- m.handleInput(mvSource, mvAction, Number(setpoint));
- break;
- case 'flowMovement':
- const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload;
- m.handleInput(fmSource, fmAction, Number(fmSetpoint));
-
- break;
- case 'emergencystop':
- const { source: esSource, action: esAction } = msg.payload;
- m.handleInput(esSource, esAction);
- break;
- case 'showWorkingCurves':
- m.showWorkingCurves();
- send({ topic : "Showing curve" , payload: m.showWorkingCurves() });
- break;
- case 'CoG':
- m.showCoG();
- send({ topic : "Showing CoG" , payload: m.showCoG() });
- break;
+ try {
+ switch(msg.topic) {
+ case 'registerChild': {
+ const childId = msg.payload;
+ const childObj = this.RED.nodes.getNode(childId);
+ if (childObj?.source) {
+ m.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
+ }
+ break;
}
+ case 'setMode':
+ m.setMode(msg.payload);
+ break;
+ case 'execSequence': {
+ const { source, action, parameter } = msg.payload || {};
+ m.handleInput(source, action, parameter);
+ break;
+ }
+ case 'execMovement': {
+ const { source: mvSource, action: mvAction, setpoint } = msg.payload || {};
+ m.handleInput(mvSource, mvAction, Number(setpoint));
+ break;
+ }
+ case 'flowMovement': {
+ const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload || {};
+ m.handleInput(fmSource, fmAction, Number(fmSetpoint));
+ break;
+ }
+ case 'emergencystop': {
+ const { source: esSource, action: esAction } = msg.payload || {};
+ m.handleInput(esSource, esAction);
+ break;
+ }
+ case 'showWorkingCurves':
+ send({ topic : "Showing curve" , payload: m.showWorkingCurves() });
+ break;
+ case 'CoG':
+ send({ topic : "Showing CoG" , payload: m.showCoG() });
+ break;
+ default:
+ break;
+ }
+ } catch (error) {
+ this.node.error(`Error handling input (${msg?.topic}): ${error?.message || error}`);
+ } finally {
+ done();
+ }
});
}
diff --git a/src/specificClass.js b/src/specificClass.js
new file mode 100644
index 0000000..a902f81
--- /dev/null
+++ b/src/specificClass.js
@@ -0,0 +1,2 @@
+module.exports = require("../dependencies/monster/SpeficicClass");
+
diff --git a/test/monster.test.js b/test/monster.test.js
new file mode 100644
index 0000000..3f638ce
--- /dev/null
+++ b/test/monster.test.js
@@ -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);
+});
+