agent updates
This commit is contained in:
43
dependencies/monster/SpeficicClass.js
vendored
43
dependencies/monster/SpeficicClass.js
vendored
@@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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);
|
||||||
|
if (childObj?.source) {
|
||||||
m.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
|
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
2
src/specificClass.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module.exports = require("../dependencies/monster/SpeficicClass");
|
||||||
|
|
||||||
121
test/monster.test.js
Normal file
121
test/monster.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user