Compare commits
4 Commits
6287708c1e
...
ae5bc750cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae5bc750cd | ||
|
|
548778c3f5 | ||
|
|
d594131cfc | ||
|
|
aaa88a7792 |
23
CLAUDE.md
Normal file
23
CLAUDE.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# valve — Claude Code context
|
||||||
|
|
||||||
|
Individual valve modelling and control.
|
||||||
|
Part of the [EVOLV](https://gitea.wbd-rd.nl/RnD/EVOLV) wastewater-automation platform.
|
||||||
|
|
||||||
|
## S88 classification
|
||||||
|
|
||||||
|
| Level | Colour | Placement lane |
|
||||||
|
|---|---|---|
|
||||||
|
| **Equipment Module** | `#86bbdd` | L3 |
|
||||||
|
|
||||||
|
## Flow layout rules
|
||||||
|
|
||||||
|
When wiring this node into a multi-node demo or production flow, follow the
|
||||||
|
placement rule set in the **EVOLV superproject**:
|
||||||
|
|
||||||
|
> `.claude/rules/node-red-flow-layout.md` (in the EVOLV repo root)
|
||||||
|
|
||||||
|
Key points for this node:
|
||||||
|
- Place on lane **L3** (x-position per the lane table in the rule).
|
||||||
|
- Stack same-level siblings vertically.
|
||||||
|
- Parent/children sit on adjacent lanes (children one lane left, parent one lane right).
|
||||||
|
- Wrap in a Node-RED group box coloured `#86bbdd` (Equipment Module).
|
||||||
231
src/nodeClass.js
231
src/nodeClass.js
@@ -42,32 +42,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) {
|
||||||
|
// Resolve flow unit with validation before building config
|
||||||
const flowUnit = this._resolveUnitOrFallback(uiConfig.unit, 'volumeFlowRate', 'm3/h', 'flow');
|
const flowUnit = this._resolveUnitOrFallback(uiConfig.unit, 'volumeFlowRate', 'm3/h', 'flow');
|
||||||
|
const resolvedUiConfig = { ...uiConfig, unit: flowUnit };
|
||||||
|
|
||||||
// Merge UI config over defaults
|
// Build config: base sections handle general, asset, functionality
|
||||||
this.config = {
|
const cfgMgr = new configManager();
|
||||||
general: {
|
this.config = cfgMgr.buildConfig(this.name, resolvedUiConfig, node.id);
|
||||||
name: uiConfig.name,
|
|
||||||
id: node.id, // node.id is for the child registration process
|
|
||||||
unit: flowUnit,
|
|
||||||
logging: {
|
|
||||||
enabled: uiConfig.enableLog,
|
|
||||||
logLevel: uiConfig.logLevel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
asset: {
|
|
||||||
uuid: uiConfig.uuid || uiConfig.assetUuid || null,
|
|
||||||
tagCode: uiConfig.tagCode || uiConfig.assetTagCode || null,
|
|
||||||
supplier: uiConfig.supplier,
|
|
||||||
category: uiConfig.category, //add later to define as the software type
|
|
||||||
type: uiConfig.assetType,
|
|
||||||
model: uiConfig.model,
|
|
||||||
unit: flowUnit
|
|
||||||
},
|
|
||||||
functionality: {
|
|
||||||
positionVsParent: uiConfig.positionVsParent || 'atEquipment', // Default to 'atEquipment' if not specified
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Utility for formatting outputs
|
// Utility for formatting outputs
|
||||||
this._output = new outputUtils();
|
this._output = new outputUtils();
|
||||||
@@ -141,111 +122,109 @@ class nodeClass {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateNodeStatus() {
|
_updateNodeStatus() {
|
||||||
const v = this.source;
|
const v = this.source;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mode = v.currentMode; // modus is bijv. auto, manual, etc.
|
const mode = v.currentMode;
|
||||||
const state = v.state.getCurrentState(); //is bijv. operational, idle, off, etc.
|
const state = v.state.getCurrentState();
|
||||||
const fluidCompatibility = typeof v.getFluidCompatibility === "function"
|
const fluidCompatibility = typeof v.getFluidCompatibility === "function"
|
||||||
? v.getFluidCompatibility()
|
? v.getFluidCompatibility()
|
||||||
: null;
|
: null;
|
||||||
const fluidWarningText = (
|
const fluidWarningText = (
|
||||||
fluidCompatibility
|
fluidCompatibility
|
||||||
&& (fluidCompatibility.status === "mismatch" || fluidCompatibility.status === "conflict")
|
&& (fluidCompatibility.status === "mismatch" || fluidCompatibility.status === "conflict")
|
||||||
)
|
)
|
||||||
? fluidCompatibility.message
|
? fluidCompatibility.message
|
||||||
: "";
|
: "";
|
||||||
const flowUnit = v?.unitPolicy?.output?.flow || this.config.general.unit || "m3/h";
|
const flowUnit = v?.unitPolicy?.output?.flow || this.config.general.unit || "m3/h";
|
||||||
const pressureUnit = v?.unitPolicy?.output?.pressure || "mbar";
|
const pressureUnit = v?.unitPolicy?.output?.pressure || "mbar";
|
||||||
// check if measured flow is available otherwise use predicted flow
|
const flow = Math.round(v.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(flowUnit));
|
||||||
const flow = Math.round(v.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(flowUnit));
|
|
||||||
|
|
||||||
let deltaP = v.measurements.type("pressure").variant("predicted").position("delta").getCurrentValue(pressureUnit);
|
let deltaP = v.measurements.type("pressure").variant("predicted").position("delta").getCurrentValue(pressureUnit);
|
||||||
if (deltaP !== null) {
|
if (deltaP !== null) {
|
||||||
deltaP = parseFloat(deltaP.toFixed(0));
|
deltaP = parseFloat(deltaP.toFixed(0));
|
||||||
} //afronden op 4 decimalen indien geen "null"
|
|
||||||
if(isNaN(deltaP)) {
|
|
||||||
deltaP = "∞";
|
|
||||||
}
|
|
||||||
const roundedPosition = Math.round(v.state.getCurrentPosition() * 100) / 100;
|
|
||||||
let symbolState;
|
|
||||||
switch(state){
|
|
||||||
case "off":
|
|
||||||
symbolState = "⬛";
|
|
||||||
break;
|
|
||||||
case "idle":
|
|
||||||
symbolState = "⏸️";
|
|
||||||
break;
|
|
||||||
case "operational":
|
|
||||||
symbolState = "⏵️";
|
|
||||||
break;
|
|
||||||
case "starting":
|
|
||||||
symbolState = "⏯️";
|
|
||||||
break;
|
|
||||||
case "warmingup":
|
|
||||||
symbolState = "🔄";
|
|
||||||
break;
|
|
||||||
case "accelerating":
|
|
||||||
symbolState = "⏩";
|
|
||||||
break;
|
|
||||||
case "stopping":
|
|
||||||
symbolState = "⏹️";
|
|
||||||
break;
|
|
||||||
case "coolingdown":
|
|
||||||
symbolState = "❄️";
|
|
||||||
break;
|
|
||||||
case "decelerating":
|
|
||||||
symbolState = "⏪";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let status;
|
|
||||||
switch (state) {
|
|
||||||
case "off":
|
|
||||||
status = { fill: "red", shape: "dot", text: `${mode}: OFF` };
|
|
||||||
break;
|
|
||||||
case "idle":
|
|
||||||
status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
break;
|
|
||||||
case "operational":
|
|
||||||
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}${flowUnit} | ΔP${deltaP} ${pressureUnit}`}; //deltaP toegevoegd
|
|
||||||
break;
|
|
||||||
case "starting":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
break;
|
|
||||||
case "warmingup":
|
|
||||||
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}${flowUnit} | ΔP${deltaP} ${pressureUnit}`}; //deltaP toegevoegd
|
|
||||||
break;
|
|
||||||
case "accelerating":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}${flowUnit} | ΔP${deltaP} ${pressureUnit}` }; //deltaP toegevoegd
|
|
||||||
break;
|
|
||||||
case "stopping":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
break;
|
|
||||||
case "coolingdown":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
break;
|
|
||||||
case "decelerating":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}${flowUnit} | ΔP${deltaP} ${pressureUnit}`}; //deltaP toegevoegd
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
}
|
|
||||||
if (fluidWarningText) {
|
|
||||||
status = {
|
|
||||||
fill: "yellow",
|
|
||||||
shape: "ring",
|
|
||||||
text: `${status.text} | ⚠ ${fluidWarningText}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
} catch (error) {
|
|
||||||
this.node.error("Error in updateNodeStatus: " + error.message);
|
|
||||||
return { fill: "red", shape: "ring", text: "Status Error" };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if(isNaN(deltaP)) {
|
||||||
|
deltaP = "∞";
|
||||||
|
}
|
||||||
|
const roundedPosition = Math.round(v.state.getCurrentPosition() * 100) / 100;
|
||||||
|
let symbolState;
|
||||||
|
switch(state){
|
||||||
|
case "off":
|
||||||
|
symbolState = "⬛";
|
||||||
|
break;
|
||||||
|
case "idle":
|
||||||
|
symbolState = "⏸️";
|
||||||
|
break;
|
||||||
|
case "operational":
|
||||||
|
symbolState = "⏵️";
|
||||||
|
break;
|
||||||
|
case "starting":
|
||||||
|
symbolState = "⏯️";
|
||||||
|
break;
|
||||||
|
case "warmingup":
|
||||||
|
symbolState = "🔄";
|
||||||
|
break;
|
||||||
|
case "accelerating":
|
||||||
|
symbolState = "⏩";
|
||||||
|
break;
|
||||||
|
case "stopping":
|
||||||
|
symbolState = "⏹️";
|
||||||
|
break;
|
||||||
|
case "coolingdown":
|
||||||
|
symbolState = "❄️";
|
||||||
|
break;
|
||||||
|
case "decelerating":
|
||||||
|
symbolState = "⏪";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status;
|
||||||
|
switch (state) {
|
||||||
|
case "off":
|
||||||
|
status = { fill: "red", shape: "dot", text: `${mode}: OFF` };
|
||||||
|
break;
|
||||||
|
case "idle":
|
||||||
|
status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
|
break;
|
||||||
|
case "operational":
|
||||||
|
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}${flowUnit} | ΔP${deltaP} ${pressureUnit}`};
|
||||||
|
break;
|
||||||
|
case "starting":
|
||||||
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
|
break;
|
||||||
|
case "warmingup":
|
||||||
|
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}${flowUnit} | ΔP${deltaP} ${pressureUnit}`};
|
||||||
|
break;
|
||||||
|
case "accelerating":
|
||||||
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}${flowUnit} | ΔP${deltaP} ${pressureUnit}` };
|
||||||
|
break;
|
||||||
|
case "stopping":
|
||||||
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
|
break;
|
||||||
|
case "coolingdown":
|
||||||
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
|
break;
|
||||||
|
case "decelerating":
|
||||||
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}${flowUnit} | ΔP${deltaP} ${pressureUnit}`};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
|
}
|
||||||
|
if (fluidWarningText) {
|
||||||
|
status = {
|
||||||
|
fill: "yellow",
|
||||||
|
shape: "ring",
|
||||||
|
text: `${status.text} | ⚠ ${fluidWarningText}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
} catch (error) {
|
||||||
|
this.node.error("Error in updateNodeStatus: " + error.message);
|
||||||
|
return { fill: "red", shape: "ring", text: "Status Error" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register this node as a child upstream and downstream.
|
* Register this node as a child upstream and downstream.
|
||||||
|
|||||||
@@ -703,7 +703,7 @@ class Valve {
|
|||||||
this.logger.debug(`Updating pressure: variant=${variant}, value=${value}, position=${position}`);
|
this.logger.debug(`Updating pressure: variant=${variant}, value=${value}, position=${position}`);
|
||||||
|
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case ("measured"):
|
case ("measured"): {
|
||||||
// put value in measurements container
|
// put value in measurements container
|
||||||
this._writeMeasurement("pressure", "measured", position, Number(value), unit);
|
this._writeMeasurement("pressure", "measured", position, Number(value), unit);
|
||||||
// get latest downstream pressure measurement
|
// get latest downstream pressure measurement
|
||||||
@@ -712,18 +712,20 @@ class Valve {
|
|||||||
const predictedFlow = this._readMeasurement("flow", "predicted", "downstream", FORMULA_UNITS.flow);
|
const predictedFlow = this._readMeasurement("flow", "predicted", "downstream", FORMULA_UNITS.flow);
|
||||||
const activeFlow = Number.isFinite(predictedFlow) ? predictedFlow : measuredFlow;
|
const activeFlow = Number.isFinite(predictedFlow) ? predictedFlow : measuredFlow;
|
||||||
// update predicted flow measurement
|
// update predicted flow measurement
|
||||||
this.updateDeltaPKlep(activeFlow,this.kv,measuredDownStreamP,this.rho,this.T); //update deltaP based on new flow
|
this.updateDeltaPKlep(activeFlow,this.kv,measuredDownStreamP,this.rho,this.T);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case ("predicted"):
|
case ("predicted"): {
|
||||||
// put value in measurements container
|
// put value in measurements container
|
||||||
this._writeMeasurement("pressure", "predicted", position, Number(value), unit);
|
this._writeMeasurement("pressure", "predicted", position, Number(value), unit);
|
||||||
const predictedDownStreamP = this._readMeasurement("pressure", "predicted", "downstream", FORMULA_UNITS.pressure);
|
const predictedDownStreamP = this._readMeasurement("pressure", "predicted", "downstream", FORMULA_UNITS.pressure);
|
||||||
const measuredFlowFromPred = this._readMeasurement("flow", "measured", "downstream", FORMULA_UNITS.flow);
|
const measuredFlowFromPred = this._readMeasurement("flow", "measured", "downstream", FORMULA_UNITS.flow);
|
||||||
const predictedFlowFromPred = this._readMeasurement("flow", "predicted", "downstream", FORMULA_UNITS.flow);
|
const predictedFlowFromPred = this._readMeasurement("flow", "predicted", "downstream", FORMULA_UNITS.flow);
|
||||||
const activeFlowFromPred = Number.isFinite(predictedFlowFromPred) ? predictedFlowFromPred : measuredFlowFromPred;
|
const activeFlowFromPred = Number.isFinite(predictedFlowFromPred) ? predictedFlowFromPred : measuredFlowFromPred;
|
||||||
this.updateDeltaPKlep(activeFlowFromPred,this.kv,predictedDownStreamP,this.rho,this.T); //update deltaP based on new flow
|
this.updateDeltaPKlep(activeFlowFromPred,this.kv,predictedDownStreamP,this.rho,this.T);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
|
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
|
||||||
@@ -784,23 +786,25 @@ class Valve {
|
|||||||
this.logger.debug(`Updating flow: variant=${variant}, value=${value}, position=${position}`);
|
this.logger.debug(`Updating flow: variant=${variant}, value=${value}, position=${position}`);
|
||||||
|
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case ("measured"):
|
case ("measured"): {
|
||||||
// put value in measurements container
|
// put value in measurements container
|
||||||
this._writeMeasurement("flow", "measured", position, Number(value), unit);
|
this._writeMeasurement("flow", "measured", position, Number(value), unit);
|
||||||
// get latest downstream pressure measurement
|
// get latest downstream pressure measurement
|
||||||
const measuredDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
|
const measuredDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
|
||||||
const measuredFlow = this._readMeasurement("flow", "measured", position, FORMULA_UNITS.flow);
|
const measuredFlow = this._readMeasurement("flow", "measured", position, FORMULA_UNITS.flow);
|
||||||
// update predicted flow measurement
|
// update predicted flow measurement
|
||||||
this.updateDeltaPKlep(measuredFlow,this.kv,measuredDownStreamP,this.rho,this.T); //update deltaP based on new flow
|
this.updateDeltaPKlep(measuredFlow,this.kv,measuredDownStreamP,this.rho,this.T);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case ("predicted"):
|
case ("predicted"): {
|
||||||
// put value in measurements container
|
// put value in measurements container
|
||||||
this._writeMeasurement("flow", "predicted", position, Number(value), unit);
|
this._writeMeasurement("flow", "predicted", position, Number(value), unit);
|
||||||
const predictedDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
|
const predictedDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
|
||||||
const predictedFlow = this._readMeasurement("flow", "predicted", position, FORMULA_UNITS.flow);
|
const predictedFlow = this._readMeasurement("flow", "predicted", position, FORMULA_UNITS.flow);
|
||||||
this.updateDeltaPKlep(predictedFlow,this.kv,predictedDownStreamP,this.rho,this.T); //update deltaP based on new flow
|
this.updateDeltaPKlep(predictedFlow,this.kv,predictedDownStreamP,this.rho,this.T);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
|
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
|
||||||
|
|||||||
20
valve.html
20
valve.html
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
// Define specific properties
|
// Define specific properties
|
||||||
speed: { value: 1, required: true },
|
speed: { value: 1, required: true },
|
||||||
|
processOutputFormat: { value: "process" },
|
||||||
|
dbaseOutputFormat: { value: "influxdb" },
|
||||||
|
|
||||||
//define asset properties
|
//define asset properties
|
||||||
uuid: { value: "" },
|
uuid: { value: "" },
|
||||||
@@ -113,6 +115,24 @@
|
|||||||
<input type="number" id="node-input-speed" style="width:60%;" />
|
<input type="number" id="node-input-speed" style="width:60%;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>Output Formats</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
|
||||||
|
<select id="node-input-processOutputFormat" style="width:60%;">
|
||||||
|
<option value="process">process</option>
|
||||||
|
<option value="json">json</option>
|
||||||
|
<option value="csv">csv</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
|
||||||
|
<select id="node-input-dbaseOutputFormat" style="width:60%;">
|
||||||
|
<option value="influxdb">influxdb</option>
|
||||||
|
<option value="json">json</option>
|
||||||
|
<option value="csv">csv</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Optional Extended Fields: supplier, cat, type, model, unit -->
|
<!-- Optional Extended Fields: supplier, cat, type, model, unit -->
|
||||||
<!-- Asset fields will be injected here -->
|
<!-- Asset fields will be injected here -->
|
||||||
<div id="asset-fields-placeholder"></div>
|
<div id="asset-fields-placeholder"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user