Compare commits

..

4 Commits

Author SHA1 Message Date
znetsixe
ae5bc750cd docs: add CLAUDE.md with S88 classification and superproject rule reference
References the flow-layout rule set in the EVOLV superproject
(.claude/rules/node-red-flow-layout.md) so Claude Code sessions working
in this repo know the S88 level, colour, and placement lane for this node.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:48:15 +02:00
Rene De Ren
548778c3f5 Expose output format selectors in editor 2026-03-12 16:39:25 +01:00
Rene De Ren
d594131cfc Migrate _loadConfig to use ConfigManager.buildConfig()
Replaces manual base config construction with shared buildConfig() method.
Node now only specifies domain-specific config sections.

Part of #1: Extract base config schema

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:59:35 +01:00
Rene De Ren
aaa88a7792 Fix ESLint errors and bugs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:39:57 +01:00
4 changed files with 160 additions and 134 deletions

23
CLAUDE.md Normal file
View 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).

View File

@@ -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.

View File

@@ -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.`);

View File

@@ -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>