Compare commits
4 Commits
6287708c1e
...
main
| 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.
|
||||
*/
|
||||
_loadConfig(uiConfig,node) {
|
||||
// Resolve flow unit with validation before building config
|
||||
const flowUnit = this._resolveUnitOrFallback(uiConfig.unit, 'volumeFlowRate', 'm3/h', 'flow');
|
||||
const resolvedUiConfig = { ...uiConfig, unit: flowUnit };
|
||||
|
||||
// Merge UI config over defaults
|
||||
this.config = {
|
||||
general: {
|
||||
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
|
||||
}
|
||||
};
|
||||
// Build config: base sections handle general, asset, functionality
|
||||
const cfgMgr = new configManager();
|
||||
this.config = cfgMgr.buildConfig(this.name, resolvedUiConfig, node.id);
|
||||
|
||||
// Utility for formatting outputs
|
||||
this._output = new outputUtils();
|
||||
@@ -141,111 +122,109 @@ class nodeClass {
|
||||
|
||||
}
|
||||
|
||||
_updateNodeStatus() {
|
||||
const v = this.source;
|
||||
_updateNodeStatus() {
|
||||
const v = this.source;
|
||||
|
||||
try {
|
||||
const mode = v.currentMode; // modus is bijv. auto, manual, etc.
|
||||
const state = v.state.getCurrentState(); //is bijv. operational, idle, off, etc.
|
||||
const fluidCompatibility = typeof v.getFluidCompatibility === "function"
|
||||
? v.getFluidCompatibility()
|
||||
: null;
|
||||
const fluidWarningText = (
|
||||
fluidCompatibility
|
||||
&& (fluidCompatibility.status === "mismatch" || fluidCompatibility.status === "conflict")
|
||||
)
|
||||
? fluidCompatibility.message
|
||||
: "";
|
||||
const flowUnit = v?.unitPolicy?.output?.flow || this.config.general.unit || "m3/h";
|
||||
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));
|
||||
try {
|
||||
const mode = v.currentMode;
|
||||
const state = v.state.getCurrentState();
|
||||
const fluidCompatibility = typeof v.getFluidCompatibility === "function"
|
||||
? v.getFluidCompatibility()
|
||||
: null;
|
||||
const fluidWarningText = (
|
||||
fluidCompatibility
|
||||
&& (fluidCompatibility.status === "mismatch" || fluidCompatibility.status === "conflict")
|
||||
)
|
||||
? fluidCompatibility.message
|
||||
: "";
|
||||
const flowUnit = v?.unitPolicy?.output?.flow || this.config.general.unit || "m3/h";
|
||||
const pressureUnit = v?.unitPolicy?.output?.pressure || "mbar";
|
||||
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);
|
||||
if (deltaP !== null) {
|
||||
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" };
|
||||
}
|
||||
let deltaP = v.measurements.type("pressure").variant("predicted").position("delta").getCurrentValue(pressureUnit);
|
||||
if (deltaP !== null) {
|
||||
deltaP = parseFloat(deltaP.toFixed(0));
|
||||
}
|
||||
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.
|
||||
|
||||
@@ -703,7 +703,7 @@ class Valve {
|
||||
this.logger.debug(`Updating pressure: variant=${variant}, value=${value}, position=${position}`);
|
||||
|
||||
switch (variant) {
|
||||
case ("measured"):
|
||||
case ("measured"): {
|
||||
// put value in measurements container
|
||||
this._writeMeasurement("pressure", "measured", position, Number(value), unit);
|
||||
// get latest downstream pressure measurement
|
||||
@@ -712,18 +712,20 @@ class Valve {
|
||||
const predictedFlow = this._readMeasurement("flow", "predicted", "downstream", FORMULA_UNITS.flow);
|
||||
const activeFlow = Number.isFinite(predictedFlow) ? predictedFlow : measuredFlow;
|
||||
// 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;
|
||||
}
|
||||
|
||||
case ("predicted"):
|
||||
case ("predicted"): {
|
||||
// put value in measurements container
|
||||
this._writeMeasurement("pressure", "predicted", position, Number(value), unit);
|
||||
const predictedDownStreamP = this._readMeasurement("pressure", "predicted", "downstream", FORMULA_UNITS.pressure);
|
||||
const measuredFlowFromPred = this._readMeasurement("flow", "measured", "downstream", FORMULA_UNITS.flow);
|
||||
const predictedFlowFromPred = this._readMeasurement("flow", "predicted", "downstream", FORMULA_UNITS.flow);
|
||||
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;
|
||||
}
|
||||
|
||||
default:
|
||||
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}`);
|
||||
|
||||
switch (variant) {
|
||||
case ("measured"):
|
||||
case ("measured"): {
|
||||
// put value in measurements container
|
||||
this._writeMeasurement("flow", "measured", position, Number(value), unit);
|
||||
// get latest downstream pressure measurement
|
||||
const measuredDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
|
||||
const measuredFlow = this._readMeasurement("flow", "measured", position, FORMULA_UNITS.flow);
|
||||
// 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;
|
||||
}
|
||||
|
||||
case ("predicted"):
|
||||
case ("predicted"): {
|
||||
// put value in measurements container
|
||||
this._writeMeasurement("flow", "predicted", position, Number(value), unit);
|
||||
const predictedDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
|
||||
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;
|
||||
}
|
||||
|
||||
default:
|
||||
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
|
||||
|
||||
20
valve.html
20
valve.html
@@ -22,6 +22,8 @@
|
||||
|
||||
// Define specific properties
|
||||
speed: { value: 1, required: true },
|
||||
processOutputFormat: { value: "process" },
|
||||
dbaseOutputFormat: { value: "influxdb" },
|
||||
|
||||
//define asset properties
|
||||
uuid: { value: "" },
|
||||
@@ -113,6 +115,24 @@
|
||||
<input type="number" id="node-input-speed" style="width:60%;" />
|
||||
</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 -->
|
||||
<!-- Asset fields will be injected here -->
|
||||
<div id="asset-fields-placeholder"></div>
|
||||
|
||||
Reference in New Issue
Block a user