forked from RnD/generalFunctions
license update and enhancements to measurement functionality + child parent relationship
This commit is contained in:
@@ -1,253 +1,94 @@
|
||||
// ChildRegistrationUtils.js
|
||||
class ChildRegistrationUtils {
|
||||
constructor(mainClass) {
|
||||
this.mainClass = mainClass; // Reference to the main class
|
||||
this.mainClass = mainClass;
|
||||
this.logger = mainClass.logger;
|
||||
this.registeredChildren = new Map();
|
||||
}
|
||||
|
||||
async registerChild(child, positionVsParent) {
|
||||
|
||||
this.logger.debug(`Registering child: ${child.id} with position=${positionVsParent}`);
|
||||
const { softwareType } = child.config.functionality;
|
||||
const { name, id, unit } = child.config.general;
|
||||
const { category = "", type = "" } = child.config.asset || {};
|
||||
console.log(`Registering child: ${name}, id: ${id}, softwareType: ${softwareType}, category: ${category}, type: ${type}, positionVsParent: ${positionVsParent}` );
|
||||
const emitter = child.emitter;
|
||||
const { name, id } = child.config.general;
|
||||
|
||||
this.logger.debug(`Registering child: ${name} (${id}) as ${softwareType} at ${positionVsParent}`);
|
||||
|
||||
//define position vs parent in child
|
||||
child.positionVsParent = positionVsParent;
|
||||
// Enhanced child setup
|
||||
child.parent = this.mainClass;
|
||||
child.positionVsParent = positionVsParent;
|
||||
|
||||
if (!this.mainClass.child) this.mainClass.child = {};
|
||||
if (!this.mainClass.child[softwareType])
|
||||
this.mainClass.child[softwareType] = {};
|
||||
if (!this.mainClass.child[softwareType][category])
|
||||
this.mainClass.child[softwareType][category] = {};
|
||||
if (!this.mainClass.child[softwareType][category][type])
|
||||
this.mainClass.child[softwareType][category][type] = {};
|
||||
|
||||
// Use an array to handle multiple categories
|
||||
if (!Array.isArray(this.mainClass.child[softwareType][category][type])) {
|
||||
this.mainClass.child[softwareType][category][type] = [];
|
||||
// Enhanced measurement container with rich context
|
||||
if (child.measurements) {
|
||||
child.measurements.setChildId(id);
|
||||
child.measurements.setChildName(name);
|
||||
child.measurements.setParentRef(this.mainClass);
|
||||
}
|
||||
|
||||
// Store child in your expected structure
|
||||
this._storeChild(child, softwareType);
|
||||
|
||||
// Push the new child to the array of the mainclass so we can track the childs
|
||||
this.mainClass.child[softwareType][category][type].push({
|
||||
name,
|
||||
id,
|
||||
unit,
|
||||
emitter,
|
||||
});
|
||||
|
||||
//then connect the child depending on the type type etc..
|
||||
this.connectChild(
|
||||
id,
|
||||
softwareType,
|
||||
emitter,
|
||||
category,
|
||||
// Track registration for utilities
|
||||
this.registeredChildren.set(id, {
|
||||
child,
|
||||
type,
|
||||
positionVsParent
|
||||
);
|
||||
}
|
||||
|
||||
connectChild(
|
||||
id,
|
||||
softwareType,
|
||||
emitter,
|
||||
category,
|
||||
child,
|
||||
type,
|
||||
positionVsParent
|
||||
) {
|
||||
this.logger.debug(
|
||||
`Connecting child id=${id}: desc=${softwareType}, category=${category},type=${type}, position=${positionVsParent}`
|
||||
);
|
||||
|
||||
switch (softwareType) {
|
||||
case "measurement":
|
||||
this.logger.debug(
|
||||
`Registering measurement child: ${id} with category=${category}`
|
||||
);
|
||||
this.connectMeasurement(child, type, positionVsParent);
|
||||
break;
|
||||
|
||||
case "machine":
|
||||
this.logger.debug(`Registering complete machine child: ${id}`);
|
||||
this.connectMachine(child);
|
||||
break;
|
||||
|
||||
case "valve":
|
||||
this.logger.debug(`Registering complete valve child: ${id}`);
|
||||
this.connectValve(child);
|
||||
break;
|
||||
|
||||
case "machineGroup":
|
||||
this.logger.debug(`Registering complete machineGroup child: ${id}`);
|
||||
this.connectMachineGroup(child);
|
||||
break;
|
||||
|
||||
case "actuator":
|
||||
this.logger.debug(`Registering linear actuator child: ${id}`);
|
||||
this.connectActuator(child,positionVsParent);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.error(`Child registration unrecognized desc: ${desc}`);
|
||||
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
||||
}
|
||||
}
|
||||
|
||||
connectMeasurement(child, type, position) {
|
||||
this.logger.debug(
|
||||
`Connecting measurement child: ${type} with position=${position}`
|
||||
);
|
||||
|
||||
// Check if type is valid
|
||||
if (!type) {
|
||||
this.logger.error(`Invalid type for measurement: ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize the measurement to a number - logging each step for debugging
|
||||
try {
|
||||
this.logger.debug(
|
||||
`Initializing measurement: ${type}, position: ${position} value: 0`
|
||||
);
|
||||
const typeResult = this.mainClass.measurements.type(type);
|
||||
const variantResult = typeResult.variant("measured");
|
||||
const positionResult = variantResult.position(position);
|
||||
positionResult.value(0);
|
||||
|
||||
this.logger.debug(
|
||||
`Subscribing on mAbs event for measurement: ${type}, position: ${position}`
|
||||
);
|
||||
// Listen for the mAbs event and update the measurement
|
||||
|
||||
this.logger.debug(
|
||||
`Successfully initialized measurement: ${type}, position: ${position}`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to initialize measurement: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
child.emitter.on("mAbs", (value) => {
|
||||
// Use the same method chaining approach that worked during initialization
|
||||
this.mainClass.measurements
|
||||
.type(type)
|
||||
.variant("measured")
|
||||
.position(position)
|
||||
.value(value);
|
||||
this.mainClass.updateMeasurement("measured", type, value, position);
|
||||
//this.logger.debug(`--------->>>>>>>>>Updated measurement: ${type}, value: ${value}, position: ${position}`);
|
||||
softwareType,
|
||||
position: positionVsParent,
|
||||
registeredAt: Date.now()
|
||||
});
|
||||
|
||||
// IMPORTANT: Only call parent registration - no automatic handling
|
||||
if (typeof this.mainClass.registerOnChildEvents === 'function') {
|
||||
this.mainClass.registerOnChildEvents();
|
||||
}
|
||||
|
||||
this.logger.info(`✅ Child ${name} registered successfully`);
|
||||
}
|
||||
|
||||
connectMachine(machine) {
|
||||
if (!machine) {
|
||||
this.logger.error("Invalid machine provided.");
|
||||
return;
|
||||
_storeChild(child, softwareType) {
|
||||
// Maintain your existing structure
|
||||
if (!this.mainClass.child) this.mainClass.child = {};
|
||||
if (!this.mainClass.child[softwareType]) this.mainClass.child[softwareType] = {};
|
||||
|
||||
const { category = "sensor" } = child.config.asset || {};
|
||||
if (!this.mainClass.child[softwareType][category]) {
|
||||
this.mainClass.child[softwareType][category] = [];
|
||||
}
|
||||
|
||||
const machineId = Object.keys(this.mainClass.machines).length + 1;
|
||||
this.mainClass.machines[machineId] = machine;
|
||||
|
||||
this.logger.info(
|
||||
`Setting up pressureChange listener for machine ${machineId}`
|
||||
);
|
||||
|
||||
machine.emitter.on("pressureChange", () =>
|
||||
this.mainClass.handlePressureChange(machine)
|
||||
);
|
||||
|
||||
//update of child triggers the handler
|
||||
this.mainClass.handleChildChange();
|
||||
|
||||
this.logger.info(`Machine ${machineId} registered successfully.`);
|
||||
|
||||
this.mainClass.child[softwareType][category].push(child);
|
||||
}
|
||||
|
||||
connectValve(valve) {
|
||||
if (!valve) {
|
||||
this.logger.warn("Invalid valve provided.");
|
||||
return;
|
||||
// NEW: Utility methods for parent to use
|
||||
getChildrenOfType(softwareType, category = null) {
|
||||
if (!this.mainClass.child[softwareType]) return [];
|
||||
|
||||
if (category) {
|
||||
return this.mainClass.child[softwareType][category] || [];
|
||||
}
|
||||
const valveId = Object.keys(this.mainClass.valves).length + 1;
|
||||
this.mainClass.valves[valveId] = valve; // Gooit valve object in de valves attribute met valve objects
|
||||
|
||||
valve.state.emitter.on("positionChange", (data) => {
|
||||
//ValveGroupController abboneren op klepstand verandering
|
||||
this.mainClass.logger.debug(`Position change of valve detected: ${data}`);
|
||||
this.mainClass.calcValveFlows();
|
||||
}); //bepaal nieuwe flow per valve
|
||||
valve.emitter.on("deltaPChange", () => {
|
||||
this.mainClass.logger.debug("DeltaP change of valve detected");
|
||||
this.mainClass.calcMaxDeltaP();
|
||||
}); //bepaal nieuwe max deltaP
|
||||
|
||||
this.logger.info(`Valve ${valveId} registered successfully.`);
|
||||
|
||||
// Return all children of this software type
|
||||
return Object.values(this.mainClass.child[softwareType]).flat();
|
||||
}
|
||||
|
||||
connectMachineGroup(machineGroup) {
|
||||
if (!machineGroup) {
|
||||
this.logger.warn("Invalid machineGroup provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const machineGroupId = Object.keys(this.mainClass.machineGroups).length + 1;
|
||||
this.mainClass.machineGroups[machineGroupId] = machineGroup;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Skip machinegroup connnection: ${error.message}`);
|
||||
}
|
||||
|
||||
machineGroup.emitter.on("totalFlowChange", (data) => {
|
||||
this.mainClass.logger.debug('Total flow change of machineGroup detected');
|
||||
this.mainClass.handleInput("parent", "totalFlowChange", data)}); //Geef nieuwe totale flow door aan valveGrouControl
|
||||
|
||||
this.logger.info(`MachineGroup ${machineGroup.config.general.name} registered successfully.`);
|
||||
}
|
||||
|
||||
connectActuator(actuator, positionVsParent) {
|
||||
if (!actuator) {
|
||||
this.logger.warn("Invalid actuator provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
//Special case gateGroupControl
|
||||
if (
|
||||
this.mainClass.config.functionality.softwareType == "gateGroupControl"
|
||||
) {
|
||||
if (Object.keys(this.mainClass.actuators).length < 2) {
|
||||
if (positionVsParent == "downstream") {
|
||||
this.mainClass.actuators[0] = actuator;
|
||||
}
|
||||
|
||||
if (positionVsParent == "upstream") {
|
||||
this.mainClass.actuators[1] = actuator;
|
||||
}
|
||||
//define emitters
|
||||
actuator.state.emitter.on("positionChange", (data) => {
|
||||
this.mainClass.logger.debug(`Position change of actuator detected: ${data}`);
|
||||
this.mainClass.eventUpdate();
|
||||
});
|
||||
|
||||
//define emitters
|
||||
actuator.state.emitter.on("stateChange", (data) => {
|
||||
this.mainClass.logger.debug(`State change of actuator detected: ${data}`);
|
||||
this.mainClass.eventUpdate();
|
||||
});
|
||||
|
||||
} else {
|
||||
this.logger.error(
|
||||
"Too many actuators registered. Only two are allowed."
|
||||
);
|
||||
}
|
||||
}
|
||||
getChildById(childId) {
|
||||
return this.registeredChildren.get(childId)?.child || null;
|
||||
}
|
||||
|
||||
//wanneer hij deze ontvangt is deltaP van een van de valves veranderd (kan ook zijn niet child zijn, maar dat maakt niet uit)
|
||||
getAllChildren() {
|
||||
return Array.from(this.registeredChildren.values()).map(r => r.child);
|
||||
}
|
||||
|
||||
// NEW: Debugging utilities
|
||||
logChildStructure() {
|
||||
this.logger.debug('Current child structure:', JSON.stringify(
|
||||
Object.keys(this.mainClass.child).reduce((acc, softwareType) => {
|
||||
acc[softwareType] = Object.keys(this.mainClass.child[softwareType]).reduce((catAcc, category) => {
|
||||
catAcc[category] = this.mainClass.child[softwareType][category].map(c => ({
|
||||
id: c.config.general.id,
|
||||
name: c.config.general.name
|
||||
}));
|
||||
return catAcc;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {}), null, 2
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChildRegistrationUtils;
|
||||
module.exports = ChildRegistrationUtils;
|
||||
260
src/helper/childRegistrationUtils_DEPRECATED.js
Normal file
260
src/helper/childRegistrationUtils_DEPRECATED.js
Normal file
@@ -0,0 +1,260 @@
|
||||
// ChildRegistrationUtils.js
|
||||
class ChildRegistrationUtils {
|
||||
constructor(mainClass) {
|
||||
this.mainClass = mainClass; // Reference to the main class
|
||||
this.logger = mainClass.logger;
|
||||
}
|
||||
|
||||
async registerChild(child, positionVsParent) {
|
||||
|
||||
this.logger.debug(`Registering child: ${child.id} with position=${positionVsParent}`);
|
||||
const { softwareType } = child.config.functionality;
|
||||
const { name, id, unit } = child.config.general;
|
||||
const { category = "", type = "" } = child.config.asset || {};
|
||||
console.log(`Registering child: ${name}, id: ${id}, softwareType: ${softwareType}, category: ${category}, type: ${type}, positionVsParent: ${positionVsParent}` );
|
||||
const emitter = child.emitter;
|
||||
|
||||
//define position vs parent in child
|
||||
child.positionVsParent = positionVsParent;
|
||||
child.parent = this.mainClass;
|
||||
|
||||
if (!this.mainClass.child) this.mainClass.child = {};
|
||||
if (!this.mainClass.child[softwareType])
|
||||
this.mainClass.child[softwareType] = {};
|
||||
if (!this.mainClass.child[softwareType][category])
|
||||
this.mainClass.child[softwareType][category] = {};
|
||||
if (!this.mainClass.child[softwareType][category][type])
|
||||
this.mainClass.child[softwareType][category][type] = {};
|
||||
|
||||
// Use an array to handle multiple categories
|
||||
if (!Array.isArray(this.mainClass.child[softwareType][category][type])) {
|
||||
this.mainClass.child[softwareType][category][type] = [];
|
||||
}
|
||||
|
||||
// Push the new child to the array of the mainclass so we can track the childs
|
||||
this.mainClass.child[softwareType][category][type].push({
|
||||
name,
|
||||
id,
|
||||
unit,
|
||||
emitter,
|
||||
});
|
||||
|
||||
//then connect the child depending on the type type etc..
|
||||
this.connectChild(
|
||||
id,
|
||||
softwareType,
|
||||
emitter,
|
||||
category,
|
||||
child,
|
||||
type,
|
||||
positionVsParent
|
||||
);
|
||||
}
|
||||
|
||||
connectChild(
|
||||
id,
|
||||
softwareType,
|
||||
emitter,
|
||||
category,
|
||||
child,
|
||||
type,
|
||||
positionVsParent
|
||||
) {
|
||||
this.logger.debug(
|
||||
`Connecting child id=${id}: desc=${softwareType}, category=${category},type=${type}, position=${positionVsParent}`
|
||||
);
|
||||
|
||||
switch (softwareType) {
|
||||
case "measurement":
|
||||
this.logger.debug(
|
||||
`Registering measurement child: ${id} with category=${category}`
|
||||
);
|
||||
this.connectMeasurement(child, type, positionVsParent);
|
||||
break;
|
||||
|
||||
case "machine":
|
||||
this.logger.debug(`Registering complete machine child: ${id}`);
|
||||
this.connectMachine(child);
|
||||
break;
|
||||
|
||||
case "valve":
|
||||
this.logger.debug(`Registering complete valve child: ${id}`);
|
||||
this.connectValve(child);
|
||||
break;
|
||||
|
||||
case "machineGroup":
|
||||
this.logger.debug(`Registering complete machineGroup child: ${id}`);
|
||||
this.connectMachineGroup(child);
|
||||
break;
|
||||
|
||||
case "actuator":
|
||||
this.logger.debug(`Registering linear actuator child: ${id}`);
|
||||
this.connectActuator(child,positionVsParent);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.error(`Child registration unrecognized desc: ${desc}`);
|
||||
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
||||
}
|
||||
}
|
||||
|
||||
connectMeasurement(child, type, position) {
|
||||
this.logger.debug(
|
||||
`Connecting measurement child: ${type} with position=${position}`
|
||||
);
|
||||
|
||||
// Check if type is valid
|
||||
if (!type) {
|
||||
this.logger.error(`Invalid type for measurement: ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize the measurement to a number - logging each step for debugging
|
||||
try {
|
||||
this.logger.debug(
|
||||
`Initializing measurement: ${type}, position: ${position} value: 0`
|
||||
);
|
||||
const typeResult = this.mainClass.measurements.type(type);
|
||||
const variantResult = typeResult.variant("measured");
|
||||
const positionResult = variantResult.position(position);
|
||||
positionResult.value(0);
|
||||
|
||||
this.logger.debug(
|
||||
`Subscribing on mAbs event for measurement: ${type}, position: ${position}`
|
||||
);
|
||||
// Listen for the mAbs event and update the measurement
|
||||
|
||||
this.logger.debug(
|
||||
`Successfully initialized measurement: ${type}, position: ${position}`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to initialize measurement: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
//testing new emitter strategy
|
||||
child.measurements.emitter.on("newValue", (data) => {
|
||||
this.logger.warn(
|
||||
`Value change event received for measurement: ${type}, position: ${position}, value: ${data.value}`
|
||||
);
|
||||
});
|
||||
|
||||
child.emitter.on("mAbs", (value) => {
|
||||
// Use the same method chaining approach that worked during initialization
|
||||
this.mainClass.measurements
|
||||
.type(type)
|
||||
.variant("measured")
|
||||
.position(position)
|
||||
.value(value);
|
||||
this.mainClass.updateMeasurement("measured", type, value, position);
|
||||
//this.logger.debug(`--------->>>>>>>>>Updated measurement: ${type}, value: ${value}, position: ${position}`);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
connectMachine(machine) {
|
||||
if (!machine) {
|
||||
this.logger.error("Invalid machine provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
const machineId = Object.keys(this.mainClass.machines).length + 1;
|
||||
this.mainClass.machines[machineId] = machine;
|
||||
|
||||
this.logger.info(
|
||||
`Setting up pressureChange listener for machine ${machineId}`
|
||||
);
|
||||
|
||||
machine.emitter.on("pressureChange", () =>
|
||||
this.mainClass.handlePressureChange(machine)
|
||||
);
|
||||
|
||||
//update of child triggers the handler
|
||||
this.mainClass.handleChildChange();
|
||||
|
||||
this.logger.info(`Machine ${machineId} registered successfully.`);
|
||||
}
|
||||
|
||||
connectValve(valve) {
|
||||
if (!valve) {
|
||||
this.logger.warn("Invalid valve provided.");
|
||||
return;
|
||||
}
|
||||
const valveId = Object.keys(this.mainClass.valves).length + 1;
|
||||
this.mainClass.valves[valveId] = valve; // Gooit valve object in de valves attribute met valve objects
|
||||
|
||||
valve.state.emitter.on("positionChange", (data) => {
|
||||
//ValveGroupController abboneren op klepstand verandering
|
||||
this.mainClass.logger.debug(`Position change of valve detected: ${data}`);
|
||||
this.mainClass.calcValveFlows();
|
||||
}); //bepaal nieuwe flow per valve
|
||||
valve.emitter.on("deltaPChange", () => {
|
||||
this.mainClass.logger.debug("DeltaP change of valve detected");
|
||||
this.mainClass.calcMaxDeltaP();
|
||||
}); //bepaal nieuwe max deltaP
|
||||
|
||||
this.logger.info(`Valve ${valveId} registered successfully.`);
|
||||
}
|
||||
|
||||
connectMachineGroup(machineGroup) {
|
||||
if (!machineGroup) {
|
||||
this.logger.warn("Invalid machineGroup provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const machineGroupId = Object.keys(this.mainClass.machineGroups).length + 1;
|
||||
this.mainClass.machineGroups[machineGroupId] = machineGroup;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Skip machinegroup connnection: ${error.message}`);
|
||||
}
|
||||
|
||||
machineGroup.emitter.on("totalFlowChange", (data) => {
|
||||
this.mainClass.logger.debug('Total flow change of machineGroup detected');
|
||||
this.mainClass.handleInput("parent", "totalFlowChange", data)}); //Geef nieuwe totale flow door aan valveGrouControl
|
||||
|
||||
this.logger.info(`MachineGroup ${machineGroup.config.general.name} registered successfully.`);
|
||||
}
|
||||
|
||||
connectActuator(actuator, positionVsParent) {
|
||||
if (!actuator) {
|
||||
this.logger.warn("Invalid actuator provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
//Special case gateGroupControl
|
||||
if (
|
||||
this.mainClass.config.functionality.softwareType == "gateGroupControl"
|
||||
) {
|
||||
if (Object.keys(this.mainClass.actuators).length < 2) {
|
||||
if (positionVsParent == "downstream") {
|
||||
this.mainClass.actuators[0] = actuator;
|
||||
}
|
||||
|
||||
if (positionVsParent == "upstream") {
|
||||
this.mainClass.actuators[1] = actuator;
|
||||
}
|
||||
//define emitters
|
||||
actuator.state.emitter.on("positionChange", (data) => {
|
||||
this.mainClass.logger.debug(`Position change of actuator detected: ${data}`);
|
||||
this.mainClass.eventUpdate();
|
||||
});
|
||||
|
||||
//define emitters
|
||||
actuator.state.emitter.on("stateChange", (data) => {
|
||||
this.mainClass.logger.debug(`State change of actuator detected: ${data}`);
|
||||
this.mainClass.eventUpdate();
|
||||
});
|
||||
|
||||
} else {
|
||||
this.logger.error(
|
||||
"Too many actuators registered. Only two are allowed."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//wanneer hij deze ontvangt is deltaP van een van de valves veranderd (kan ook zijn niet child zijn, maar dat maakt niet uit)
|
||||
}
|
||||
|
||||
module.exports = ChildRegistrationUtils;
|
||||
@@ -50,6 +50,8 @@ class MeasurementBuilder {
|
||||
this.position,
|
||||
this.windowSize
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const MeasurementBuilder = require('./MeasurementBuilder');
|
||||
const EventEmitter = require('events');
|
||||
const convertModule = require('../convert/index');
|
||||
|
||||
class MeasurementContainer {
|
||||
constructor(options = {}, logger) {
|
||||
this.logger = logger;
|
||||
constructor(options = {}) {
|
||||
this.emitter = new EventEmitter();
|
||||
this.measurements = {};
|
||||
this.windowSize = options.windowSize || 10; // Default window size
|
||||
|
||||
@@ -10,6 +12,63 @@ class MeasurementContainer {
|
||||
this._currentType = null;
|
||||
this._currentVariant = null;
|
||||
this._currentPosition = null;
|
||||
this._unit = null;
|
||||
|
||||
// Default units for each measurement type
|
||||
this.defaultUnits = {
|
||||
pressure: 'mbar',
|
||||
flow: 'm3/h',
|
||||
power: 'kW',
|
||||
temperature: 'C',
|
||||
volume: 'm3',
|
||||
length: 'm',
|
||||
...options.defaultUnits // Allow override
|
||||
};
|
||||
|
||||
// Auto-conversion settings
|
||||
this.autoConvert = options.autoConvert !== false; // Default to true
|
||||
this.preferredUnits = options.preferredUnits || {}; // Per-measurement overrides
|
||||
|
||||
// For chaining context
|
||||
this._currentType = null;
|
||||
this._currentVariant = null;
|
||||
this._currentPosition = null;
|
||||
this._unit = null;
|
||||
|
||||
// NEW: Enhanced child identification
|
||||
this.childId = null;
|
||||
this.childName = null;
|
||||
this.parentRef = null;
|
||||
|
||||
}
|
||||
|
||||
// NEW: Methods to set child context
|
||||
setChildId(childId) {
|
||||
this.childId = childId;
|
||||
return this;
|
||||
}
|
||||
|
||||
setChildName(childName) {
|
||||
this.childName = childName;
|
||||
return this;
|
||||
}
|
||||
|
||||
setParentRef(parent) {
|
||||
this.parentRef = parent;
|
||||
return this;
|
||||
}
|
||||
|
||||
// New method to set preferred units
|
||||
setPreferredUnit(measurementType, unit) {
|
||||
this.preferredUnits[measurementType] = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
// Get the target unit for a measurement type
|
||||
_getTargetUnit(measurementType) {
|
||||
return this.preferredUnits[measurementType] ||
|
||||
this.defaultUnits[measurementType] ||
|
||||
null;
|
||||
}
|
||||
|
||||
// Chainable methods
|
||||
@@ -37,20 +96,69 @@ class MeasurementContainer {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Core methods that complete the chain
|
||||
value(val, timestamp = Date.now()) {
|
||||
if (!this._ensureChainIsValid()) return this;
|
||||
|
||||
const measurement = this._getOrCreateMeasurement();
|
||||
measurement.setValue(val, timestamp);
|
||||
return this;
|
||||
// ENHANCED: Update your existing value method
|
||||
value(val, timestamp = Date.now(), sourceUnit = null) {
|
||||
if (!this._ensureChainIsValid()) return this;
|
||||
|
||||
const measurement = this._getOrCreateMeasurement();
|
||||
const targetUnit = this._getTargetUnit(this._currentType);
|
||||
|
||||
let convertedValue = val;
|
||||
let finalUnit = sourceUnit || targetUnit;
|
||||
|
||||
// Auto-convert if enabled and units are specified
|
||||
if (this.autoConvert && sourceUnit && targetUnit && sourceUnit !== targetUnit) {
|
||||
try {
|
||||
convertedValue = convertModule(val).from(sourceUnit).to(targetUnit);
|
||||
finalUnit = targetUnit;
|
||||
|
||||
if (this.logger) {
|
||||
this.logger.debug(`Auto-converted ${val} ${sourceUnit} to ${convertedValue} ${targetUnit}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
this.logger.warn(`Auto-conversion failed from ${sourceUnit} to ${targetUnit}: ${error.message}`);
|
||||
}
|
||||
convertedValue = val;
|
||||
finalUnit = sourceUnit;
|
||||
}
|
||||
}
|
||||
|
||||
measurement.setValue(convertedValue, timestamp);
|
||||
|
||||
if (finalUnit && !measurement.unit) {
|
||||
measurement.setUnit(finalUnit);
|
||||
}
|
||||
|
||||
// ✅ ENHANCED: Emit event with rich context
|
||||
const eventData = {
|
||||
value: convertedValue,
|
||||
originalValue: val,
|
||||
unit: finalUnit,
|
||||
sourceUnit: sourceUnit,
|
||||
timestamp,
|
||||
position: this._currentPosition,
|
||||
variant: this._currentVariant,
|
||||
type: this._currentType,
|
||||
// ✅ NEW: Enhanced context
|
||||
childId: this.childId,
|
||||
childName: this.childName,
|
||||
parentRef: this.parentRef
|
||||
};
|
||||
|
||||
// Emit the exact event your parent expects
|
||||
this.emitter.emit(`${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
unit(unitName) {
|
||||
if (!this._ensureChainIsValid()) return this;
|
||||
|
||||
const measurement = this._getOrCreateMeasurement();
|
||||
measurement.setUnit(unitName);
|
||||
this._unit = unitName;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -60,14 +168,52 @@ class MeasurementContainer {
|
||||
return this._getOrCreateMeasurement();
|
||||
}
|
||||
|
||||
getCurrentValue() {
|
||||
getCurrentValue(requestedUnit = null) {
|
||||
const measurement = this.get();
|
||||
return measurement ? measurement.getCurrentValue() : null;
|
||||
if (!measurement) return null;
|
||||
|
||||
const value = measurement.getCurrentValue();
|
||||
if (value === null) return null;
|
||||
|
||||
// Return as-is if no unit conversion requested
|
||||
if (!requestedUnit) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Convert if needed
|
||||
if (measurement.unit && requestedUnit !== measurement.unit) {
|
||||
try {
|
||||
return convertModule(value).from(measurement.unit).to(requestedUnit);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
this.logger.error(`Unit conversion failed: ${error.message}`);
|
||||
}
|
||||
return value; // Return original value if conversion fails
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
getAverage() {
|
||||
getAverage(requestedUnit = null) {
|
||||
const measurement = this.get();
|
||||
return measurement ? measurement.getAverage() : null;
|
||||
if (!measurement) return null;
|
||||
|
||||
const avgValue = measurement.getAverage();
|
||||
if (avgValue === null) return null;
|
||||
|
||||
if (!requestedUnit || !measurement.unit || requestedUnit === measurement.unit) {
|
||||
return avgValue;
|
||||
}
|
||||
|
||||
try {
|
||||
return convertModule(avgValue).from(measurement.unit).to(requestedUnit);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
this.logger.error(`Unit conversion failed: ${error.message}`);
|
||||
}
|
||||
return avgValue;
|
||||
}
|
||||
}
|
||||
|
||||
getMin() {
|
||||
@@ -85,47 +231,43 @@ class MeasurementContainer {
|
||||
return measurement ? measurement.getAllValues() : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Difference calculations between positions
|
||||
difference() {
|
||||
difference(requestedUnit = null) {
|
||||
if (!this._currentType || !this._currentVariant) {
|
||||
throw new Error('Type and variant must be specified for difference calculation');
|
||||
}
|
||||
|
||||
// Save position to restore chain state after operation
|
||||
const savedPosition = this._currentPosition;
|
||||
|
||||
// Get upstream measurement
|
||||
// Get upstream and downstream measurements
|
||||
this._currentPosition = 'upstream';
|
||||
const upstream = this.get();
|
||||
|
||||
// Get downstream measurement
|
||||
this._currentPosition = 'downstream';
|
||||
const downstream = this.get();
|
||||
|
||||
// Restore chain state
|
||||
this._currentPosition = savedPosition;
|
||||
|
||||
if (!upstream || !downstream || upstream.values.length === 0 || downstream.values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure units match
|
||||
let downstreamForCalc = downstream;
|
||||
if (upstream.unit && downstream.unit && upstream.unit !== downstream.unit) {
|
||||
try {
|
||||
downstreamForCalc = downstream.convertTo(upstream.unit);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
this.logger.error(`Unit conversion failed: ${error.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Get target unit for conversion
|
||||
const targetUnit = requestedUnit || upstream.unit || downstream.unit;
|
||||
|
||||
// Get values in the same unit
|
||||
const upstreamValue = this._convertValueToUnit(upstream.getCurrentValue(), upstream.unit, targetUnit);
|
||||
const downstreamValue = this._convertValueToUnit(downstream.getCurrentValue(), downstream.unit, targetUnit);
|
||||
|
||||
const upstreamAvg = this._convertValueToUnit(upstream.getAverage(), upstream.unit, targetUnit);
|
||||
const downstreamAvg = this._convertValueToUnit(downstream.getAverage(), downstream.unit, targetUnit);
|
||||
|
||||
return {
|
||||
value: downstreamForCalc.getCurrentValue() - upstream.getCurrentValue() ,
|
||||
avgDiff: downstreamForCalc.getAverage() - upstream.getAverage() ,
|
||||
unit: upstream.unit
|
||||
value: downstreamValue - upstreamValue,
|
||||
avgDiff: downstreamAvg - upstreamAvg,
|
||||
unit: targetUnit
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,6 +337,72 @@ class MeasurementContainer {
|
||||
this._currentVariant = null;
|
||||
this._currentPosition = null;
|
||||
}
|
||||
|
||||
// Helper method for value conversion
|
||||
_convertValueToUnit(value, fromUnit, toUnit) {
|
||||
if (!value || !fromUnit || !toUnit || fromUnit === toUnit) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return convertModule(value).from(fromUnit).to(toUnit);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
this.logger.warn(`Conversion failed from ${fromUnit} to ${toUnit}: ${error.message}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Get available units for a measurement type
|
||||
getAvailableUnits(measurementType = null) {
|
||||
const type = measurementType || this._currentType;
|
||||
if (!type) return [];
|
||||
|
||||
// Map measurement types to convert module measures
|
||||
const measureMap = {
|
||||
pressure: 'pressure',
|
||||
flow: 'volumeFlowRate',
|
||||
power: 'power',
|
||||
temperature: 'temperature',
|
||||
volume: 'volume',
|
||||
length: 'length',
|
||||
mass: 'mass',
|
||||
energy: 'energy'
|
||||
};
|
||||
|
||||
const convertMeasure = measureMap[type];
|
||||
if (!convertMeasure) return [];
|
||||
|
||||
try {
|
||||
return convertModule().possibilities(convertMeasure);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get best unit for current value
|
||||
getBestUnit(excludeUnits = []) {
|
||||
const measurement = this.get();
|
||||
if (!measurement || !measurement.unit) return null;
|
||||
|
||||
const currentValue = measurement.getCurrentValue();
|
||||
if (currentValue === null) return null;
|
||||
|
||||
try {
|
||||
const best = convertModule(currentValue)
|
||||
.from(measurement.unit)
|
||||
.toBest({ exclude: excludeUnits });
|
||||
|
||||
return best;
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
this.logger.error(`getBestUnit failed: ${error.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = MeasurementContainer;
|
||||
|
||||
@@ -1,58 +1,255 @@
|
||||
const { MeasurementContainer } = require('./index');
|
||||
|
||||
// Create a container
|
||||
const container = new MeasurementContainer({ windowSize: 20 });
|
||||
console.log('=== MEASUREMENT CONTAINER EXAMPLES ===\n');
|
||||
console.log('This guide shows how to use the MeasurementContainer for storing,');
|
||||
console.log('retrieving, and converting measurement data with automatic unit handling.\n');
|
||||
|
||||
// Example 1: Setting values with chaining
|
||||
console.log('--- Example 1: Setting values ---');
|
||||
container.type('pressure').variant('measured').position('upstream').value(100).unit('psi');
|
||||
container.type('pressure').variant('measured').position('downstream').value(95).unit('psi');
|
||||
container.type('pressure').variant('measured').position('downstream').value(80);
|
||||
// ====================================
|
||||
// BASIC SETUP EXAMPLES
|
||||
// ====================================
|
||||
console.log('--- Example 1: Basic Setup & Event Subscription ---');
|
||||
|
||||
// Example 2: Getting values with chaining
|
||||
console.log('--- Example 2: Getting values ---');
|
||||
const upstreamValue = container.type('pressure').variant('measured').position('upstream').getCurrentValue();
|
||||
const upstreamUnit = container.type('pressure').variant('measured').position('upstream').get().unit;
|
||||
// Create a basic container
|
||||
const basicContainer = new MeasurementContainer({ windowSize: 20 });
|
||||
|
||||
// Subscribe to flow events to monitor changes
|
||||
basicContainer.emitter.on('flow.predicted.upstream', (data) => {
|
||||
console.log(`📡 Event: Flow predicted upstream update: ${data.value} at ${new Date(data.timestamp).toLocaleTimeString()}`);
|
||||
});
|
||||
|
||||
//show all flow values from variant measured
|
||||
basicContainer.emitter.on('flow.measured.*', (data) => {
|
||||
console.log(`📡 Event---------- I DID IT: Flow measured ${data.position} update: ${data.value}`)
|
||||
});
|
||||
|
||||
// Basic value setting with chaining
|
||||
console.log('Setting basic pressure values...');
|
||||
basicContainer.type('pressure').variant('measured').position('upstream').value(100).unit('psi');
|
||||
basicContainer.type('pressure').variant('measured').position('downstream').value(95).unit('psi');
|
||||
basicContainer.type('pressure').variant('measured').position('downstream').value(80); // Additional value
|
||||
|
||||
console.log('✅ Basic setup complete\n');
|
||||
|
||||
// ====================================
|
||||
// AUTO-CONVERSION SETUP EXAMPLES
|
||||
// ====================================
|
||||
console.log('--- Example 2: Auto-Conversion Setup ---');
|
||||
console.log('Setting up a container with automatic unit conversion...\n');
|
||||
|
||||
// Create container with auto-conversion enabled
|
||||
const autoContainer = new MeasurementContainer({
|
||||
autoConvert: true,
|
||||
windowSize: 50,
|
||||
defaultUnits: {
|
||||
pressure: 'bar', // Default pressure unit
|
||||
flow: 'l/min', // Default flow unit
|
||||
power: 'kW', // Default power unit
|
||||
temperature: 'C' // Default temperature unit
|
||||
},
|
||||
preferredUnits: {
|
||||
pressure: 'psi' // Override: store pressure in PSI instead of bar
|
||||
}
|
||||
});
|
||||
|
||||
// Values are automatically converted to preferred units
|
||||
console.log('Adding pressure data with auto-conversion:');
|
||||
autoContainer.type('pressure').variant('measured').position('upstream')
|
||||
.value(1.5, Date.now(), 'bar'); // Input: 1.5 bar → Auto-stored as ~21.76 psi
|
||||
|
||||
autoContainer.type('pressure').variant('measured').position('downstream')
|
||||
.value(20, Date.now(), 'psi'); // Input: 20 psi → Stored as 20 psi (already in preferred unit)
|
||||
|
||||
// Check what was actually stored
|
||||
const storedPressure = autoContainer.type('pressure').variant('measured').position('upstream').get();
|
||||
console.log(` Stored upstream pressure: ${storedPressure.getCurrentValue()} ${storedPressure.unit}`);
|
||||
console.log(' Auto-conversion setup complete\n');
|
||||
|
||||
// ====================================
|
||||
// UNIT CONVERSION EXAMPLES
|
||||
// ====================================
|
||||
console.log('--- Example 3: Unit Conversion on Retrieval ---');
|
||||
console.log('Getting values in different units without changing stored data...\n');
|
||||
|
||||
// Add flow data in different units
|
||||
autoContainer.type('flow').variant('predicted').position('upstream')
|
||||
.value(100, Date.now(), 'l/min'); // Stored in l/min (default)
|
||||
|
||||
autoContainer.type('flow').variant('predicted').position('downstream')
|
||||
.value(6, Date.now(), 'm3/h'); // Auto-converted from m3/h to l/min
|
||||
|
||||
// Retrieve the same data in different units
|
||||
const flowLPM = autoContainer.type('flow').variant('predicted').position('upstream').getCurrentValue('l/min');
|
||||
const flowM3H = autoContainer.type('flow').variant('predicted').position('upstream').getCurrentValue('m3/h');
|
||||
const flowGPM = autoContainer.type('flow').variant('predicted').position('upstream').getCurrentValue('gal/min');
|
||||
|
||||
console.log(`Flow in l/min: ${flowLPM}`);
|
||||
console.log(`Flow in m³/h: ${flowM3H.toFixed(2)}`);
|
||||
console.log(`Flow in gal/min: ${flowGPM.toFixed(2)}`);
|
||||
console.log('Unit conversion examples complete\n');
|
||||
|
||||
// ====================================
|
||||
// SMART UNIT SELECTION
|
||||
// ====================================
|
||||
console.log('--- Example 4: Smart Unit Selection ---');
|
||||
console.log('Automatically finding the best unit for readability...\n');
|
||||
|
||||
// Add a very small pressure value
|
||||
autoContainer.type('pressure').variant('test').position('sensor')
|
||||
.value(0.001, Date.now(), 'bar');
|
||||
|
||||
// Get the best unit for this small value
|
||||
const bestUnit = autoContainer.type('pressure').variant('test').position('sensor').getBestUnit();
|
||||
if (bestUnit) {
|
||||
console.log(`Best unit representation: ${bestUnit.val} ${bestUnit.unit}`);
|
||||
}
|
||||
|
||||
// Get all available units for pressure
|
||||
const availableUnits = autoContainer.getAvailableUnits('pressure');
|
||||
console.log(`Available pressure units: ${availableUnits.slice(0, 8).join(', ')}... (${availableUnits.length} total)`);
|
||||
console.log('Smart unit selection complete\n');
|
||||
|
||||
// ====================================
|
||||
// BASIC RETRIEVAL AND CALCULATIONS
|
||||
// ====================================
|
||||
console.log('--- Example 5: Basic Value Retrieval ---');
|
||||
console.log('Getting individual values and their units...\n');
|
||||
|
||||
// Using basic container for clear examples
|
||||
const upstreamValue = basicContainer.type('pressure').variant('measured').position('upstream').getCurrentValue();
|
||||
const upstreamUnit = basicContainer.type('pressure').variant('measured').position('upstream').get().unit;
|
||||
console.log(`Upstream pressure: ${upstreamValue} ${upstreamUnit}`);
|
||||
const downstreamValue = container.type('pressure').variant('measured').position('downstream').getCurrentValue();
|
||||
const downstreamUnit = container.type('pressure').variant('measured').position('downstream').get().unit;
|
||||
|
||||
const downstreamValue = basicContainer.type('pressure').variant('measured').position('downstream').getCurrentValue();
|
||||
const downstreamUnit = basicContainer.type('pressure').variant('measured').position('downstream').get().unit;
|
||||
console.log(`Downstream pressure: ${downstreamValue} ${downstreamUnit}`);
|
||||
console.log('Basic retrieval complete\n');
|
||||
|
||||
// Example 3: Calculations using chained methods
|
||||
console.log('--- Example 3: Calculations ---');
|
||||
container.type('flow').variant('predicted').position('upstream').value(200).unit('gpm');
|
||||
container.type('flow').variant('predicted').position('downstream').value(195).unit('gpm');
|
||||
// ====================================
|
||||
// CALCULATIONS AND STATISTICS
|
||||
// ====================================
|
||||
console.log('--- Example 6: Calculations & Statistics ---');
|
||||
console.log('Using built-in calculation methods...\n');
|
||||
|
||||
const flowAvg = container.type('flow').variant('predicted').position('upstream').getAverage();
|
||||
// Add flow data for calculations
|
||||
basicContainer.type('flow').variant('predicted').position('upstream').value(200).unit('gpm');
|
||||
basicContainer.type('flow').variant('predicted').position('downstream').value(195).unit('gpm');
|
||||
|
||||
const flowAvg = basicContainer.type('flow').variant('predicted').position('upstream').getAverage();
|
||||
console.log(`Average upstream flow: ${flowAvg} gpm`);
|
||||
|
||||
// Example 4: Getting pressure difference
|
||||
console.log('--- Example 4: Difference calculations ---');
|
||||
const pressureDiff = container.type('pressure').variant('measured').difference();
|
||||
// Calculate pressure difference between upstream and downstream
|
||||
const pressureDiff = basicContainer.type('pressure').variant('measured').difference();
|
||||
console.log(`Pressure difference: ${pressureDiff.value} ${pressureDiff.unit}`);
|
||||
console.log('Calculations complete\n');
|
||||
|
||||
// Example 5: Adding multiple values to track history
|
||||
console.log('--- Example 5: Multiple values ---');
|
||||
// Add several values to upstream flow
|
||||
container.type('flow').variant('measured').position('upstream')
|
||||
.value(210).value(215).value(205).unit('gpm');
|
||||
// ====================================
|
||||
// ADVANCED STATISTICS
|
||||
// ====================================
|
||||
console.log('--- Example 7: Advanced Statistics & History ---');
|
||||
console.log('Adding multiple values and getting comprehensive statistics...\n');
|
||||
|
||||
// Then get statistics
|
||||
console.log('Flow statistics:');
|
||||
console.log(`- Current: ${container.type('flow').variant('measured').position('upstream').getCurrentValue()} gpm`);
|
||||
console.log(`- Average: ${container.type('flow').variant('measured').position('upstream').getAverage()} gpm`);
|
||||
console.log(`- Min: ${container.type('flow').variant('measured').position('upstream').getMin()} gpm`);
|
||||
console.log(`- Max: ${container.type('flow').variant('measured').position('upstream').getMax()} gpm`);
|
||||
console.log(`Show all values : ${JSON.stringify(container.type('flow').variant('measured').position('upstream').getAllValues())}`);
|
||||
// Add several flow measurements to build history
|
||||
basicContainer.type('flow').variant('measured').position('upstream')
|
||||
.value(210).value(215).value(205).value(220).value(200).unit('m3/h');
|
||||
basicContainer.type('flow').variant('measured').position('downstream')
|
||||
.value(190).value(195).value(185).value(200).value(180).unit('m3/h');
|
||||
|
||||
// Example 6: Listing available data
|
||||
console.log('--- Example 6: Listing available data ---');
|
||||
console.log('Types:', container.getTypes());
|
||||
console.log('Pressure variants:', container.type('pressure').getVariants());
|
||||
console.log('Measured pressure positions:', container.type('pressure').variant('measured').getPositions());
|
||||
// Get comprehensive statistics
|
||||
const measurement = basicContainer.type('flow').variant('measured').position('upstream');
|
||||
console.log('Flow Statistics:');
|
||||
console.log(`- Current value: ${measurement.getCurrentValue()} ${measurement.get().unit}`);
|
||||
console.log(`- Average: ${measurement.getAverage().toFixed(1)} ${measurement.get().unit}`);
|
||||
console.log(`- Minimum: ${measurement.getMin()} ${measurement.get().unit}`);
|
||||
console.log(`- Maximum: ${measurement.getMax()} ${measurement.get().unit}`);
|
||||
|
||||
// Show all values with timestamps
|
||||
const allValues = measurement.getAllValues();
|
||||
console.log(`- Total samples: ${allValues.values.length}`);
|
||||
console.log(`- Value history: [${allValues.values.join(', ')}]`);
|
||||
console.log('Advanced statistics complete\n');
|
||||
|
||||
// ====================================
|
||||
// DYNAMIC UNIT MANAGEMENT
|
||||
// ====================================
|
||||
console.log('--- Example 8: Dynamic Unit Management ---');
|
||||
console.log('Changing preferred units at runtime...\n');
|
||||
|
||||
// Change preferred unit for flow measurements
|
||||
autoContainer.setPreferredUnit('flow', 'm3/h');
|
||||
console.log('Changed preferred flow unit to m³/h');
|
||||
|
||||
// Add new flow data - will auto-convert to new preferred unit
|
||||
autoContainer.type('flow').variant('realtime').position('inlet')
|
||||
.value(150, Date.now(), 'l/min'); // Input in l/min, stored as m³/h
|
||||
|
||||
const realtimeFlow = autoContainer.type('flow').variant('realtime').position('inlet');
|
||||
console.log(`Stored as: ${realtimeFlow.getCurrentValue()} ${realtimeFlow.get().unit}`);
|
||||
console.log(`Original unit: ${realtimeFlow.getCurrentValue('l/min')} l/min`);
|
||||
console.log('Dynamic unit management complete\n');
|
||||
|
||||
// ====================================
|
||||
// DATA EXPLORATION
|
||||
// ====================================
|
||||
console.log('--- Example 9: Data Exploration ---');
|
||||
console.log('Discovering what data is available in the container...\n');
|
||||
|
||||
console.log('Available measurement types:', basicContainer.getTypes());
|
||||
console.log('Pressure variants:', basicContainer.type('pressure').getVariants());
|
||||
console.log('Measured pressure positions:', basicContainer.type('pressure').variant('measured').getPositions());
|
||||
|
||||
// Show data structure overview
|
||||
console.log('\nData Structure Overview:');
|
||||
basicContainer.getTypes().forEach(type => {
|
||||
console.log(`${type.toUpperCase()}:`);
|
||||
const variants = basicContainer.type(type).getVariants();
|
||||
variants.forEach(variant => {
|
||||
const positions = basicContainer.type(type).variant(variant).getPositions();
|
||||
positions.forEach(position => {
|
||||
const measurement = basicContainer.type(type).variant(variant).position(position).get();
|
||||
if (measurement && measurement.values.length > 0) {
|
||||
console.log(` └── ${variant}.${position}: ${measurement.values.length} values (${measurement.unit || 'no unit'})`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
console.log('Data exploration complete\n');
|
||||
|
||||
// ====================================
|
||||
// BEST PRACTICES SUMMARY
|
||||
// ====================================
|
||||
console.log('--- Best Practices Summary ---');
|
||||
console.log('BEST PRACTICES FOR NEW USERS:\n');
|
||||
|
||||
console.log('1. SETUP:');
|
||||
console.log(' • Enable auto-conversion for consistent units');
|
||||
console.log(' • Define default units for your measurement types');
|
||||
console.log(' • Set appropriate window size for your data needs\n');
|
||||
|
||||
console.log('2. STORING DATA:');
|
||||
console.log(' • Always use the full chain: type().variant().position().value()');
|
||||
console.log(' • Specify source unit when adding values: .value(100, timestamp, "psi")');
|
||||
console.log(' • Set units immediately after first value: .value(100).unit("psi")\n');
|
||||
|
||||
console.log('3. RETRIEVING DATA:');
|
||||
console.log(' • Use .getCurrentValue("unit") to get values in specific units');
|
||||
console.log(' • Use .getBestUnit() for automatic unit selection');
|
||||
console.log(' • Use .difference() for automatic upstream/downstream calculations\n');
|
||||
|
||||
console.log('4. MONITORING:');
|
||||
console.log(' • Subscribe to events for real-time updates');
|
||||
console.log(' • Use .emitter.on("type.variant.position", callback)');
|
||||
console.log(' • Explore available data with .getTypes(), .getVariants(), .getPositions()\n');
|
||||
|
||||
console.log('All examples complete! Ready to use MeasurementContainer');
|
||||
|
||||
// Export for programmatic use
|
||||
module.exports = {
|
||||
runExamples: () => {
|
||||
console.log('Examples of the measurement chainable API');
|
||||
}
|
||||
};
|
||||
console.log('Measurement Container Examples - Complete Guide for New Users');
|
||||
console.log('This file demonstrates all features with practical examples.');
|
||||
},
|
||||
|
||||
// Export containers for testing
|
||||
basicContainer,
|
||||
autoContainer
|
||||
};
|
||||
@@ -1,23 +1,26 @@
|
||||
const AssetMenu = require('./asset.js');
|
||||
const { TagcodeApp, DynamicAssetMenu } = require('./tagcodeApp.js');
|
||||
const LoggerMenu = require('./logger.js');
|
||||
const PhysicalPositionMenu = require('./physicalPosition.js');
|
||||
|
||||
class MenuManager {
|
||||
|
||||
constructor() {
|
||||
this.registeredMenus = new Map(); // Store menu type instances
|
||||
this.registerMenu('asset', new AssetMenu()); // Register asset menu by default
|
||||
this.registerMenu('logger', new LoggerMenu()); // Register logger menu by default
|
||||
this.registerMenu('position', new PhysicalPositionMenu()); // Register position menu by default
|
||||
this.registeredMenus = new Map();
|
||||
// Register factory functions
|
||||
this.registerMenu('asset', () => new AssetMenu()); // static menu to be replaced by dynamic one but later
|
||||
//this.registerMenu('asset', (nodeName) => new DynamicAssetMenu(nodeName, new TagcodeApp()));
|
||||
this.registerMenu('logger', () => new LoggerMenu());
|
||||
this.registerMenu('position', () => new PhysicalPositionMenu());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a menu type with its handler instance
|
||||
* Register a menu type with its handler factory function
|
||||
* @param {string} menuType - The type of menu (e.g., 'asset', 'logging')
|
||||
* @param {object} menuHandler - The menu handler instance
|
||||
* @param {function} menuFactory - The menu factory function
|
||||
*/
|
||||
registerMenu(menuType, menuHandler) {
|
||||
this.registeredMenus.set(menuType, menuHandler);
|
||||
registerMenu(menuType, menuFactory) {
|
||||
this.registeredMenus.set(menuType, menuFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,58 +30,145 @@ class MenuManager {
|
||||
* @returns {string} Complete JavaScript code to serve
|
||||
*/
|
||||
createEndpoint(nodeName, menuTypes) {
|
||||
// 1. Collect all menu data
|
||||
const menuData = {};
|
||||
menuTypes.forEach(menuType => {
|
||||
const handler = this.registeredMenus.get(menuType);
|
||||
if (handler && typeof handler.getAllMenuData === 'function') {
|
||||
menuData[menuType] = handler.getAllMenuData();
|
||||
try {
|
||||
// ✅ Create instances using factory functions with proper error handling
|
||||
const instantiatedMenus = new Map();
|
||||
|
||||
menuTypes.forEach(menuType => {
|
||||
try {
|
||||
const factory = this.registeredMenus.get(menuType);
|
||||
if (typeof factory === 'function') {
|
||||
const instance = factory(nodeName);
|
||||
instantiatedMenus.set(menuType, instance);
|
||||
} else {
|
||||
console.warn(`No factory function found for menu type: ${menuType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error creating instance for ${menuType}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Collect all menu data with error handling
|
||||
const menuData = {};
|
||||
menuTypes.forEach(menuType => {
|
||||
try {
|
||||
const handler = instantiatedMenus.get(menuType);
|
||||
if (handler && typeof handler.getAllMenuData === 'function') {
|
||||
menuData[menuType] = handler.getAllMenuData();
|
||||
} else {
|
||||
// Provide default empty data if method doesn't exist
|
||||
menuData[menuType] = {};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error getting menu data for ${menuType}:`, error);
|
||||
menuData[menuType] = {};
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Generate HTML injection code with error handling
|
||||
const htmlInjections = menuTypes.map(type => {
|
||||
try {
|
||||
const menu = instantiatedMenus.get(type);
|
||||
if (menu && typeof menu.getHtmlInjectionCode === 'function') {
|
||||
return menu.getHtmlInjectionCode(nodeName);
|
||||
}
|
||||
return '';
|
||||
} catch (error) {
|
||||
console.error(`Error generating HTML injection for ${type}:`, error);
|
||||
return `// Error generating HTML injection for ${type}: ${error.message}`;
|
||||
}
|
||||
}).join('\n');
|
||||
|
||||
// ✅ Collect all client initialization code with error handling
|
||||
const initFunctions = [];
|
||||
menuTypes.forEach(menuType => {
|
||||
try {
|
||||
const handler = instantiatedMenus.get(menuType);
|
||||
if (handler && typeof handler.getClientInitCode === 'function') {
|
||||
initFunctions.push(handler.getClientInitCode(nodeName));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error generating init code for ${menuType}:`, error);
|
||||
initFunctions.push(`// Error in ${menuType} initialization: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert menu data to JSON
|
||||
const menuDataJSON = JSON.stringify(menuData, null, 2);
|
||||
|
||||
// ✅ Assemble the complete script with comprehensive error handling
|
||||
return `
|
||||
try {
|
||||
// Create the namespace structure with safety checks
|
||||
window.EVOLV = window.EVOLV || {};
|
||||
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||
|
||||
// Initialize menu namespaces
|
||||
${menuTypes.map(type => `window.EVOLV.nodes.${nodeName}.${type}Menu = window.EVOLV.nodes.${nodeName}.${type}Menu || {};`).join('\n ')}
|
||||
|
||||
// Inject the pre-loaded menu data directly into the namespace
|
||||
window.EVOLV.nodes.${nodeName}.menuData = ${menuDataJSON};
|
||||
|
||||
// HTML injections with error handling
|
||||
try {
|
||||
${htmlInjections}
|
||||
} catch (htmlError) {
|
||||
console.error('Error in HTML injections for ${nodeName}:', htmlError);
|
||||
}
|
||||
|
||||
// Initialize functions with error handling
|
||||
try {
|
||||
${initFunctions.join('\n\n ')}
|
||||
} catch (initError) {
|
||||
console.error('Error in initialization functions for ${nodeName}:', initError);
|
||||
}
|
||||
|
||||
// Main initialization function that calls all menu initializers
|
||||
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||
try {
|
||||
${menuTypes.map(type => `
|
||||
try {
|
||||
if (window.EVOLV.nodes.${nodeName}.${type}Menu && window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor) {
|
||||
window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor(node);
|
||||
}
|
||||
} catch (${type}Error) {
|
||||
console.error('Error initializing ${type} menu for ${nodeName}:', ${type}Error);
|
||||
}`).join('')}
|
||||
} catch (editorError) {
|
||||
console.error('Error in main editor initialization for ${nodeName}:', editorError);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('${nodeName} menu data and initializers loaded for: ${menuTypes.join(', ')}');
|
||||
|
||||
} catch (globalError) {
|
||||
console.error('Critical error in ${nodeName} menu initialization:', globalError);
|
||||
|
||||
// Fallback initialization
|
||||
window.EVOLV = window.EVOLV || {};
|
||||
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||
console.warn('Using fallback editor initialization for ${nodeName}');
|
||||
};
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
// Generate HTML injection code
|
||||
const htmlInjections = menuTypes.map(type => {
|
||||
const menu = this.registeredMenus.get(type);
|
||||
if (menu && menu.getHtmlInjectionCode) {
|
||||
return menu.getHtmlInjectionCode(nodeName);
|
||||
}
|
||||
return '';
|
||||
}).join('\n');
|
||||
|
||||
// 2. Collect all client initialization code
|
||||
const initFunctions = [];
|
||||
menuTypes.forEach(menuType => {
|
||||
const handler = this.registeredMenus.get(menuType);
|
||||
if (handler && typeof handler.getClientInitCode === 'function') {
|
||||
initFunctions.push(handler.getClientInitCode(nodeName));
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Convert menu data to JSON
|
||||
const menuDataJSON = JSON.stringify(menuData, null, 2);
|
||||
|
||||
// 4. Assemble the complete script
|
||||
return `
|
||||
// Create the namespace structure
|
||||
window.EVOLV = window.EVOLV || {};
|
||||
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||
|
||||
// Inject the pre-loaded menu data directly into the namespace
|
||||
window.EVOLV.nodes.${nodeName}.menuData = ${menuDataJSON};
|
||||
|
||||
${initFunctions.join('\n\n')}
|
||||
|
||||
// Main initialization function that calls all menu initializers
|
||||
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||
${menuTypes.map(type => `
|
||||
if (window.EVOLV.nodes.${nodeName}.${type}Menu && window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor) {
|
||||
window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor(node);
|
||||
}`).join('')}
|
||||
};
|
||||
|
||||
console.log('${nodeName} menu data and initializers loaded for: ${menuTypes.join(', ')}');
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error(`Critical error creating endpoint for ${nodeName}:`, error);
|
||||
|
||||
// Return minimal fallback script
|
||||
return `
|
||||
window.EVOLV = window.EVOLV || {};
|
||||
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||
console.error('Menu system failed to initialize for ${nodeName}');
|
||||
};
|
||||
console.error('Menu system failed for ${nodeName}:', '${error.message}');
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
606
src/menu/tagcodeApp.js
Normal file
606
src/menu/tagcodeApp.js
Normal file
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* taggcodeApp.js
|
||||
* Dynamische AssetMenu implementatie met TagcodeApp API
|
||||
* Vervangt de statische assetData met calls naar REST-endpoints.
|
||||
*/
|
||||
|
||||
class TagcodeApp {
|
||||
constructor(baseURL = 'https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api') {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
async fetchData(path, params = {}) {
|
||||
const url = new URL(`${this.baseURL}/${path}`);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
const json = await response.json();
|
||||
if (!json.success) throw new Error(json.error || json.message);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
// Asset endpoints
|
||||
getAllAssets() {
|
||||
return this.fetchData('asset/get_all_assets.php');
|
||||
}
|
||||
|
||||
getAssetDetail(tag_code) {
|
||||
return this.fetchData('asset/get_detail_asset.php', { tag_code });
|
||||
}
|
||||
|
||||
getAssetHistory(asset_tag_number) {
|
||||
return this.fetchData('asset/get_history_asset.php', { asset_tag_number });
|
||||
}
|
||||
|
||||
getAssetHierarchy(asset_tag_number) {
|
||||
return this.fetchData('asset/get_asset_hierarchy.php', { asset_tag_number });
|
||||
}
|
||||
|
||||
createOrUpdateAsset(params) {
|
||||
return this.fetchData('asset/create_asset.php', params);
|
||||
}
|
||||
|
||||
// Product & vendor endpoints
|
||||
getVendors() {
|
||||
return this.fetchData('vendor/get_vendors.php');
|
||||
}
|
||||
|
||||
getSubtypes(vendor_name) {
|
||||
return this.fetchData('product/get_subtypesFromVendor.php', { vendor_name });
|
||||
}
|
||||
|
||||
getSubtypesForCategory(vendor_name, category) {
|
||||
return this.fetchData('product/get_subtypesFromVendorAndCategory.php', {
|
||||
vendor_name,
|
||||
category
|
||||
});
|
||||
}
|
||||
|
||||
getProductModels(vendor_name, product_subtype_name) {
|
||||
return this.fetchData('product/get_product_models.php', { vendor_name, product_subtype_name });
|
||||
}
|
||||
|
||||
getLocations() {
|
||||
return this.fetchData('location/get_locations.php');
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicAssetMenu {
|
||||
constructor(nodeName, api = new TagcodeApp()) {
|
||||
|
||||
this.nodeName = nodeName;
|
||||
this.api = api;
|
||||
|
||||
//temp translation table for nodeName to API
|
||||
// Mapping van nodeName naar softwareType
|
||||
this.softwareTypeMapping = {
|
||||
'measurement': 'Sensor',
|
||||
'rotatingMachine': 'machine',
|
||||
'valve': 'valve',
|
||||
'pump': 'machine',
|
||||
'heatExchanger': 'machine',
|
||||
// Voeg meer mappings toe als nodig
|
||||
};
|
||||
|
||||
// Bepaal automatisch de softwareType
|
||||
this.softwareType = this.softwareTypeMapping[nodeName] || nodeName;
|
||||
|
||||
|
||||
this.data = {
|
||||
vendors: [],
|
||||
subtypes: {},
|
||||
models: {}
|
||||
};
|
||||
}
|
||||
|
||||
//Added missing getAllMenuData method
|
||||
|
||||
getAllMenuData() {
|
||||
return {
|
||||
vendors: this.data.vendors || [],
|
||||
locations: this.data.locations || [],
|
||||
htmlTemplate: this.getHtmlTemplate()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialiseer: haal alleen de vendor-lijst en locaties op
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
this.data.suppliers = await this.api.getVendors();
|
||||
this.data.locations = await this.api.getLocations();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize DynamicAssetMenu:', error);
|
||||
this.data.suppliers = [];
|
||||
this.data.locations = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Complete getClientInitCode method with full TagcodeApp definition
|
||||
|
||||
getClientInitCode(nodeName) {
|
||||
return `
|
||||
// --- DynamicAssetMenu voor ${nodeName} ---
|
||||
|
||||
// ✅ Define COMPLETE TagcodeApp class in browser context
|
||||
window.TagcodeApp = window.TagcodeApp || class {
|
||||
constructor(baseURL = 'https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api') {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
async fetchData(path, params = {}) {
|
||||
const url = new URL(this.baseURL + '/' + path);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||
const json = await response.json();
|
||||
if (!json.success) throw new Error(json.error || json.message);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
// ✅ ALL API methods defined here
|
||||
getAllAssets() {
|
||||
return this.fetchData('asset/get_all_assets.php');
|
||||
}
|
||||
|
||||
getAssetDetail(tag_code) {
|
||||
return this.fetchData('asset/get_detail_asset.php', { tag_code });
|
||||
}
|
||||
|
||||
getVendors() {
|
||||
return this.fetchData('vendor/get_vendors.php');
|
||||
}
|
||||
|
||||
getSubtypes(vendor_name, category = null) {
|
||||
const params = { vendor_name };
|
||||
if (category) params.category = category;
|
||||
return this.fetchData('product/get_subtypesFromVendor.php', params);
|
||||
}
|
||||
|
||||
getProductModels(vendor_name, product_subtype_name) {
|
||||
return this.fetchData('product/get_product_models.php', { vendor_name, product_subtype_name });
|
||||
}
|
||||
|
||||
getLocations() {
|
||||
return this.fetchData('location/get_locations.php');
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Initialize the API instance BEFORE it's needed
|
||||
window.assetAPI = window.assetAPI || new window.TagcodeApp();
|
||||
|
||||
// Helper populate function
|
||||
function populate(el, opts, sel) {
|
||||
if (!el) return;
|
||||
const old = el.value;
|
||||
el.innerHTML = '<option value="">Select…</option>';
|
||||
(opts||[]).forEach(o=>{
|
||||
const opt = document.createElement('option');
|
||||
opt.value = o;
|
||||
opt.textContent = o;
|
||||
el.appendChild(opt);
|
||||
});
|
||||
el.value = sel || '';
|
||||
if (el.value !== old) el.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
// ✅ Ensure namespace exists and initialize properly
|
||||
if (!window.EVOLV.nodes.${nodeName}.assetMenu) {
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu = {};
|
||||
}
|
||||
|
||||
// ✅ Complete initEditor function
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.initEditor = async function(node) {
|
||||
try {
|
||||
console.log('🚀 Starting asset menu initialization for ${nodeName}');
|
||||
console.log('🎯 Automatic softwareType: ${this.softwareType}');
|
||||
|
||||
// ✅ Verify API is available
|
||||
if (!window.assetAPI) {
|
||||
console.error('❌ window.assetAPI not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Wait for DOM to be ready and inject HTML with retry
|
||||
const waitForDialogAndInject = () => {
|
||||
return new Promise((resolve) => {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 20;
|
||||
|
||||
const tryInject = () => {
|
||||
attempts++;
|
||||
console.log('Injection attempt ' + attempts + '/' + maxAttempts);
|
||||
|
||||
const injectionSuccess = this.injectHtml ? this.injectHtml() : false;
|
||||
|
||||
if (injectionSuccess) {
|
||||
console.log('✅ HTML injection successful on attempt:', attempts);
|
||||
resolve(true);
|
||||
} else if (attempts < maxAttempts) {
|
||||
setTimeout(tryInject, 100);
|
||||
} else {
|
||||
console.warn('⚠️ HTML injection failed after ' + maxAttempts + ' attempts');
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(tryInject, 200);
|
||||
});
|
||||
};
|
||||
|
||||
// Wait for HTML injection
|
||||
const htmlReady = await waitForDialogAndInject();
|
||||
|
||||
if (!htmlReady) {
|
||||
console.error('❌ Could not inject HTML, continuing without asset menu');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔧 Setting up asset menu functionality');
|
||||
|
||||
// ✅ Load vendor list with error handling
|
||||
try {
|
||||
console.log('📡 Loading vendors...');
|
||||
const vendors = await window.assetAPI.getVendors();
|
||||
console.log('✅ Vendors loaded:', vendors.length);
|
||||
|
||||
// ✅ Handle both string arrays and object arrays
|
||||
const vendorNames = vendors.map(v => v.name || v);
|
||||
populate(document.getElementById('node-input-supplier'), vendorNames, node.supplier);
|
||||
} catch (vendorError) {
|
||||
console.error('❌ Error loading vendors:', vendorError);
|
||||
}
|
||||
|
||||
// ✅ Get form elements
|
||||
const elems = {
|
||||
supplier: document.getElementById('node-input-supplier'),
|
||||
category: document.getElementById('node-input-category'),
|
||||
type: document.getElementById('node-input-assetType'),
|
||||
model: document.getElementById('node-input-model'),
|
||||
unit: document.getElementById('node-input-unit')
|
||||
};
|
||||
|
||||
// ✅ Set automatic category value
|
||||
if (elems.category) {
|
||||
elems.category.value = '${this.softwareType}';
|
||||
console.log('✅ Automatic category set to:', elems.category.value);
|
||||
}
|
||||
|
||||
// ✅ Supplier change: load subtypes for automatic category
|
||||
if (elems.supplier) {
|
||||
elems.supplier.addEventListener('change', async () => {
|
||||
const vendor = elems.supplier.value;
|
||||
const category = '${this.softwareType}';
|
||||
|
||||
if (!vendor) {
|
||||
populate(elems.type, [], '');
|
||||
populate(elems.model, [], '');
|
||||
populate(elems.unit, [], '');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('📡 Loading subtypes for vendor:', vendor, 'category:', category);
|
||||
const subtypes = await window.assetAPI.getSubtypes(vendor, category);
|
||||
console.log('✅ Subtypes loaded:', subtypes.length);
|
||||
|
||||
const subtypeNames = subtypes.map(s => s.name || s.subtype_name || s);
|
||||
populate(elems.type, subtypeNames, node.assetType);
|
||||
|
||||
populate(elems.model, [], '');
|
||||
populate(elems.unit, [], '');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading subtypes:', error);
|
||||
populate(elems.type, [], '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Type change: load models for vendor + selected subtype
|
||||
if (elems.type) {
|
||||
elems.type.addEventListener('change', async () => {
|
||||
const vendor = elems.supplier.value;
|
||||
const selectedSubtype = elems.type.value;
|
||||
|
||||
if (!vendor || !selectedSubtype) {
|
||||
populate(elems.model, [], '');
|
||||
populate(elems.unit, [], '');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('📡 Loading models for vendor:', vendor, 'subtype:', selectedSubtype);
|
||||
const models = await window.assetAPI.getProductModels(vendor, selectedSubtype);
|
||||
console.log('✅ Models loaded:', models.length);
|
||||
|
||||
window._currentModels = models;
|
||||
const modelNames = models.map(m => m.name || m.model_name || m);
|
||||
populate(elems.model, modelNames, node.model);
|
||||
|
||||
populate(elems.unit, [], '');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading models:', error);
|
||||
populate(elems.model, [], '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Model change: show units for selected model
|
||||
if (elems.model) {
|
||||
elems.model.addEventListener('change', () => {
|
||||
const selectedModelName = elems.model.value;
|
||||
const models = window._currentModels || [];
|
||||
const selectedModel = models.find(m =>
|
||||
(m.name || m.model_name) === selectedModelName
|
||||
);
|
||||
|
||||
const units = selectedModel && selectedModel.product_model_meta ?
|
||||
Object.keys(selectedModel.product_model_meta) : [];
|
||||
populate(elems.unit, units, node.unit);
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Trigger supplier change if there's a saved value
|
||||
if (node.supplier && elems.supplier) {
|
||||
setTimeout(() => {
|
||||
elems.supplier.dispatchEvent(new Event('change'));
|
||||
}, 100);
|
||||
}
|
||||
|
||||
console.log('✅ Asset menu initialization complete for ${nodeName}');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in asset menu initialization:', error);
|
||||
}
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
getHtmlTemplate() {
|
||||
return `
|
||||
<!-- Asset Properties -->
|
||||
<hr />
|
||||
<h3>Asset selection (${this.softwareType})</h3>
|
||||
<div class="form-row">
|
||||
<label for="node-input-supplier"><i class="fa fa-industry"></i> Supplier</label>
|
||||
<select id="node-input-supplier" style="width:70%;"></select>
|
||||
</div>
|
||||
<!-- ✅ Toon softwareType als readonly info -->
|
||||
<div class="form-row">
|
||||
<label><i class="fa fa-sitemap"></i> Category</label>
|
||||
<input type="text" value="${this.softwareType}" readonly style="width:70%; background-color: #f5f5f5;" />
|
||||
<input type="hidden" id="node-input-category" value="${this.softwareType}" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-assetType"><i class="fa fa-puzzle-piece"></i> Type</label>
|
||||
<select id="node-input-assetType" style="width:70%;"></select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
|
||||
<select id="node-input-model" style="width:70%;"></select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
|
||||
<select id="node-input-unit" style="width:70%;"></select>
|
||||
</div>
|
||||
<hr />
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed getHtmlInjectionCode method
|
||||
*/
|
||||
/**
|
||||
* Fixed getHtmlInjectionCode method with better element detection
|
||||
*/
|
||||
getHtmlInjectionCode(nodeName) {
|
||||
const htmlTemplate = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\${/g, '\\${');
|
||||
|
||||
return `
|
||||
// Enhanced HTML injection with multiple fallback strategies
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.injectHtml = function() {
|
||||
try {
|
||||
// Strategy 1: Find the dialog form container
|
||||
let targetContainer = document.querySelector('#red-ui-editor-dialog .red-ui-editDialog-content');
|
||||
|
||||
// Strategy 2: Fallback to the main dialog form
|
||||
if (!targetContainer) {
|
||||
targetContainer = document.querySelector('#dialog-form');
|
||||
}
|
||||
|
||||
// Strategy 3: Fallback to any form in the editor dialog
|
||||
if (!targetContainer) {
|
||||
targetContainer = document.querySelector('#red-ui-editor-dialog form');
|
||||
}
|
||||
|
||||
// Strategy 4: Find by Red UI classes
|
||||
if (!targetContainer) {
|
||||
targetContainer = document.querySelector('.red-ui-editor-dialog .editor-tray-content');
|
||||
}
|
||||
|
||||
if (targetContainer) {
|
||||
// Remove any existing asset menu to prevent duplicates
|
||||
const existingAssetMenu = targetContainer.querySelector('.asset-menu-section');
|
||||
if (existingAssetMenu) {
|
||||
existingAssetMenu.remove();
|
||||
}
|
||||
|
||||
// Create container div
|
||||
const assetMenuDiv = document.createElement('div');
|
||||
assetMenuDiv.className = 'asset-menu-section';
|
||||
assetMenuDiv.innerHTML = \`${htmlTemplate}\`;
|
||||
|
||||
// Insert at the beginning of the form
|
||||
targetContainer.insertBefore(assetMenuDiv, targetContainer.firstChild);
|
||||
|
||||
console.log(' Asset menu HTML injected successfully into:', targetContainer.className || targetContainer.tagName);
|
||||
return true;
|
||||
} else {
|
||||
console.warn('⚠️ Could not find dialog form container. Available elements:');
|
||||
console.log('Available dialogs:', document.querySelectorAll('[id*="dialog"], [class*="dialog"]'));
|
||||
console.log('Available forms:', document.querySelectorAll('form'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error injecting HTML:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Exporteer voor gebruik in Node-RED
|
||||
module.exports = { TagcodeApp, DynamicAssetMenu };
|
||||
|
||||
/*
|
||||
// --- Test CLI ---
|
||||
// Voer deze test uit met `node tagcodeApp.js` om de API-client en menu-init logica te controleren
|
||||
if (require.main === module) {
|
||||
(async () => {
|
||||
const api = new TagcodeApp();
|
||||
console.log('=== Test: getVendors() ===');
|
||||
let vendors;
|
||||
try {
|
||||
vendors = await api.getVendors();
|
||||
console.log('Vendors:', vendors);
|
||||
} catch (e) {
|
||||
console.error('getVendors() error:', e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('=== Test: getLocations() ===');
|
||||
try {
|
||||
const locations = await api.getLocations();
|
||||
console.log('Locations:', locations);
|
||||
} catch (e) {
|
||||
console.error('getLocations() error:', e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Test verschillende nodeNames met automatische softwareType mapping
|
||||
const testNodes = [
|
||||
{ nodeName: 'measurement', expectedSoftwareType: 'Sensor' },
|
||||
{ nodeName: 'rotatingMachine', expectedSoftwareType: 'machine' },
|
||||
{ nodeName: 'valve', expectedSoftwareType: 'valve' }
|
||||
];
|
||||
|
||||
for (const testNode of testNodes) {
|
||||
console.log(`\n=== Test: ${testNode.nodeName} → ${testNode.expectedSoftwareType} ===`);
|
||||
|
||||
// Initialize DynamicAssetMenu met automatische softwareType
|
||||
const menu = new DynamicAssetMenu(testNode.nodeName, api);
|
||||
console.log(`✅ Automatic softwareType for ${testNode.nodeName}:`, menu.softwareType);
|
||||
|
||||
try {
|
||||
await menu.init();
|
||||
console.log('Preloaded suppliers:', menu.data.suppliers.map(v=>v.name || v));
|
||||
} catch (e) {
|
||||
console.error(`DynamicAssetMenu.init() error for ${testNode.nodeName}:`, e.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`=== Sequential dropdown simulation for ${testNode.nodeName} ===`);
|
||||
|
||||
// 1. Select supplier
|
||||
const supplier = menu.data.suppliers[0];
|
||||
const supplierName = supplier.name || supplier;
|
||||
console.log('Selected supplier:', supplierName);
|
||||
|
||||
// 2. ✅ Gebruik automatische softwareType in plaats van dropdown
|
||||
const automaticCategory = menu.softwareType;
|
||||
console.log('Automatic category (softwareType):', automaticCategory);
|
||||
|
||||
// 3. ✅ Direct naar models met supplier + automatische category
|
||||
let models;
|
||||
try {
|
||||
console.log(`📡 Loading models for supplier: "${supplierName}", category: "${automaticCategory}"`);
|
||||
models = await api.getProductModels(supplierName, automaticCategory);
|
||||
console.log('Fetched models:', models.map(m=>m.name || m));
|
||||
|
||||
if (models.length === 0) {
|
||||
console.warn(`⚠️ No models found for ${supplierName} + ${automaticCategory}`);
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`getProductModels error for ${supplierName} + ${automaticCategory}:`, e.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Extract unique types from models
|
||||
const types = Array.from(new Set(models.map(m => m.product_model_type || m.type || 'Unknown')));
|
||||
console.log('Available types:', types);
|
||||
|
||||
if (types.length === 0) {
|
||||
console.warn('⚠️ No types found in models');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Choose first type
|
||||
const selectedType = types[0];
|
||||
console.log('Selected type:', selectedType);
|
||||
|
||||
// 6. Filter models by type
|
||||
const filteredModels = models.filter(m =>
|
||||
(m.product_model_type || m.type) === selectedType
|
||||
);
|
||||
console.log('Models for selected type:', filteredModels.map(m => m.name || m));
|
||||
|
||||
if (filteredModels.length === 0) {
|
||||
console.warn('⚠️ No models found for selected type');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 7. Choose first model and show units
|
||||
const model = filteredModels[0];
|
||||
console.log('Selected model:', model.name || model);
|
||||
|
||||
const units = model.product_model_meta ? Object.keys(model.product_model_meta) : [];
|
||||
console.log('Available units:', units);
|
||||
const unit = units[0] || 'N/A';
|
||||
console.log('Selected unit:', unit);
|
||||
|
||||
console.log(`✅ Complete flow for ${testNode.nodeName}:`);
|
||||
console.log(` Supplier: ${supplierName}`);
|
||||
console.log(` Category: ${automaticCategory} (automatic)`);
|
||||
console.log(` Type: ${selectedType}`);
|
||||
console.log(` Model: ${model.name || model}`);
|
||||
console.log(` Unit: ${unit}`);
|
||||
}
|
||||
|
||||
console.log('\n=== Test verschillende softwareTypes ===');
|
||||
|
||||
// Test of de API verschillende categories ondersteunt
|
||||
const testCategories = ['Sensor', 'machine', 'valve', 'pump'];
|
||||
const testSupplier = 'Vega'; // Bijvoorbeeld
|
||||
|
||||
for (const category of testCategories) {
|
||||
try {
|
||||
console.log(`\n📡 Testing category: ${category} with supplier: ${testSupplier}`);
|
||||
const models = await api.getProductModels(testSupplier, category);
|
||||
console.log(`✅ Found ${models.length} models for ${testSupplier} + ${category}`);
|
||||
|
||||
if (models.length > 0) {
|
||||
const sampleModel = models[0];
|
||||
console.log(` Sample model:`, sampleModel.name || sampleModel);
|
||||
|
||||
const types = Array.from(new Set(models.map(m => m.product_model_type || m.type)));
|
||||
console.log(` Available types:`, types);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`⚠️ No models found for ${testSupplier} + ${category}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Klaar met alle tests ===');
|
||||
})();
|
||||
}
|
||||
*/
|
||||
207
src/menu/tagcodeAsset.js
Normal file
207
src/menu/tagcodeAsset.js
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* taggcodeApp.js
|
||||
* Dynamische AssetMenu implementatie met TagcodeApp API
|
||||
* Vervangt de statische assetData met calls naar REST-endpoints.
|
||||
*/
|
||||
|
||||
class TagcodeApp {
|
||||
constructor(baseURL = 'https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api') {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
async fetchData(path, params = {}) {
|
||||
const url = new URL(`${this.baseURL}/${path}`);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
const json = await response.json();
|
||||
if (!json.success) throw new Error(json.error || json.message);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
// Asset endpoints
|
||||
getAllAssets() {
|
||||
return this.fetchData('asset/get_all_assets.php');
|
||||
}
|
||||
|
||||
getAssetDetail(tag_code) {
|
||||
return this.fetchData('asset/get_detail_asset.php', { tag_code });
|
||||
}
|
||||
|
||||
getAssetHistory(asset_tag_number) {
|
||||
return this.fetchData('asset/get_history_asset.php', { asset_tag_number });
|
||||
}
|
||||
|
||||
getAssetHierarchy(asset_tag_number) {
|
||||
return this.fetchData('asset/get_asset_hierarchy.php', { asset_tag_number });
|
||||
}
|
||||
|
||||
createOrUpdateAsset(params) {
|
||||
// Bij create/update worden alle velden via query params meegegeven
|
||||
return this.fetchData('asset/create_asset.php', params);
|
||||
}
|
||||
|
||||
// Product & vendor endpoints
|
||||
getVendors() {
|
||||
return this.fetchData('vendor/get_vendors.php');
|
||||
}
|
||||
|
||||
getSubtypes(vendor_name) {
|
||||
return this.fetchData('product/get_subtypesFromVendor.php', { vendor_name });
|
||||
}
|
||||
|
||||
getProductModels(vendor_name, product_subtype_name) {
|
||||
return this.fetchData('product/get_product_models.php', { vendor_name, product_subtype_name });
|
||||
}
|
||||
|
||||
getLocations() {
|
||||
return this.fetchData('location/get_locations.php');
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicAssetMenu {
|
||||
constructor(nodeName, api = new TagcodeApp()) {
|
||||
this.nodeName = nodeName;
|
||||
this.api = api;
|
||||
this.data = {
|
||||
vendors: [],
|
||||
subtypes: {}, // per vendor
|
||||
models: {} // per vendor+subtype
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialiseer: haal vendors en locaties eenmalig op
|
||||
*/
|
||||
async init() {
|
||||
this.data.vendors = await this.api.getVendors();
|
||||
this.data.locations = await this.api.getLocations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injecteer HTML, data en events
|
||||
*/
|
||||
getClientInitCode() {
|
||||
const node = this.nodeName;
|
||||
return `
|
||||
// --- DynamicAssetMenu voor ${node} ---
|
||||
window.TagcodeApp = window.TagcodeApp || ${TagcodeApp.toString()};
|
||||
window.assetAPI = window.assetAPI || new TagcodeApp();
|
||||
|
||||
// Helper populate
|
||||
function populate(el, opts, sel) {
|
||||
const old = el.value;
|
||||
el.innerHTML = '<option value="">Select…</option>';
|
||||
(opts||[]).forEach(o=>{
|
||||
const opt = document.createElement('option');
|
||||
opt.value = o; opt.textContent = o;
|
||||
el.appendChild(opt);
|
||||
});
|
||||
el.value = sel || '';
|
||||
if (el.value !== old) el.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
// InitEditor
|
||||
window.EVOLV.nodes.${node}.assetMenu.initEditor = async function(node) {
|
||||
this.injectHtml();
|
||||
// eerst: laad vendor-lijst
|
||||
const vendors = await window.assetAPI.getVendors();
|
||||
const vendorNames = vendors.map(v=>v.name);
|
||||
populate(document.getElementById('node-input-supplier'), vendorNames, node.supplier);
|
||||
|
||||
// wire events
|
||||
const elems = {
|
||||
supplier: document.getElementById('node-input-supplier'),
|
||||
category: document.getElementById('node-input-category'),
|
||||
type: document.getElementById('node-input-assetType'),
|
||||
model: document.getElementById('node-input-model'),
|
||||
unit: document.getElementById('node-input-unit')
|
||||
};
|
||||
|
||||
elems.supplier.addEventListener('change', async ()=>{
|
||||
const v = elems.supplier.value;
|
||||
if (!v) return populate(elems.category, [], '');
|
||||
const subs = await window.assetAPI.getSubtypes(v);
|
||||
const names = subs.map(s=>s.name);
|
||||
populate(elems.category, names, node.category);
|
||||
});
|
||||
|
||||
elems.category.addEventListener('change', async ()=>{
|
||||
const v = elems.supplier.value, c = elems.category.value;
|
||||
if (!v||!c) return populate(elems.type, [], '');
|
||||
const models = await window.assetAPI.getProductModels(v, c);
|
||||
window._currentModels = models; // tijdelijk cachen
|
||||
const types = Array.from(new Set(models.map(m=>m.product_model_type)));
|
||||
populate(elems.type, types, node.assetType);
|
||||
});
|
||||
|
||||
elems.type.addEventListener('change', ()=>{
|
||||
const t = elems.type.value;
|
||||
const models = window._currentModels || [];
|
||||
const filtered = models.filter(m=>m.product_model_type===t);
|
||||
const names = filtered.map(m=>m.name);
|
||||
window._filteredModels = filtered;
|
||||
populate(elems.model, names, node.model);
|
||||
});
|
||||
|
||||
elems.model.addEventListener('change', ()=>{
|
||||
const m = elems.model.value;
|
||||
const models = window._filteredModels || [];
|
||||
const entry = models.find(x=>x.name===m);
|
||||
const units = entry && entry.product_model_meta ? Object.keys(entry.product_model_meta) : [];
|
||||
populate(elems.unit, units, node.unit);
|
||||
});
|
||||
|
||||
// laadt opgeslagen waarden
|
||||
if (node.supplier) elems.supplier.dispatchEvent(new Event('change'));
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
getHtmlTemplate() {
|
||||
return `
|
||||
<!-- Asset Properties -->
|
||||
<hr />
|
||||
<h3>Asset selection</h3>
|
||||
<div class="form-row">
|
||||
<label for="node-input-supplier"><i class="fa fa-industry"></i> Supplier</label>
|
||||
<select id="node-input-supplier" style="width:70%;"></select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-category"><i class="fa fa-sitemap"></i> Category</label>
|
||||
<select id="node-input-category" style="width:70%;"></select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-assetType"><i class="fa fa-puzzle-piece"></i> Type</label>
|
||||
<select id="node-input-assetType" style="width:70%;"></select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
|
||||
<select id="node-input-model" style="width:70%;"></select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
|
||||
<select id="node-input-unit" style="width:70%;"></select>
|
||||
</div>
|
||||
<hr />
|
||||
`;
|
||||
}
|
||||
|
||||
getHtmlInjectionCode() {
|
||||
const tmpl = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\$/g, '\\\$');
|
||||
return `
|
||||
// Asset HTML injection voor ${this.nodeName}
|
||||
window.EVOLV.nodes.${this.nodeName}.assetMenu.injectHtml = function() {
|
||||
const placeholder = document.getElementById('asset-fields-placeholder');
|
||||
if (placeholder && !placeholder.hasChildNodes()) {
|
||||
placeholder.innerHTML = \`${tmpl}\`;
|
||||
}
|
||||
};
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Exporteer voor gebruik in Node-RED
|
||||
module.exports = { TagcodeApp, DynamicAssetMenu };
|
||||
Reference in New Issue
Block a user