Complete general functions
This commit is contained in:
406
configs/machineConfig.json
Normal file
406
configs/machineConfig.json
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "Rotating Machine",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this machine configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration. If not provided, defaults to null."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "m3/h",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The default measurement unit for this configuration (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"logLevel": {
|
||||||
|
"default": "info",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "debug",
|
||||||
|
"description": "Log messages are printed for debugging purposes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "info",
|
||||||
|
"description": "Informational messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warn",
|
||||||
|
"description": "Warning messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "error",
|
||||||
|
"description": "Error messages are printed."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether logging is active. If true, log messages will be generated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"functionality": {
|
||||||
|
"softwareType": {
|
||||||
|
"default": "machine",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specified software type for this configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "RotationalDeviceController",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Indicates the role this configuration plays within the system."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asset": {
|
||||||
|
"uuid": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A universally unique identifier for this asset. May be null if not assigned."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geoLocation": {
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "An object representing the asset's physical coordinates or location.",
|
||||||
|
"schema": {
|
||||||
|
"x": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "X coordinate of the asset's location."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Y coordinate of the asset's location."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"z": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Z coordinate of the asset's location."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The supplier or manufacturer of the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"default": "pump",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subType": {
|
||||||
|
"default": "Centrifugal",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A more specific classification within 'type'. For example, 'centrifugal' for a centrifugal pump."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accuracy": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The accuracy of the machine or sensor, typically as a percentage or absolute value."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"machineCurve": {
|
||||||
|
"default": {
|
||||||
|
"nq": {
|
||||||
|
"1": {
|
||||||
|
"x": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
30,
|
||||||
|
40,
|
||||||
|
50
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"np": {
|
||||||
|
"1": {
|
||||||
|
"x": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
30,
|
||||||
|
40,
|
||||||
|
50
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "machineCurve",
|
||||||
|
"description": "All machine curves must have a 'nq' and 'np' curve. nq stands for the flow curve, np stands for the power curve. Together they form the efficiency curve."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"current": {
|
||||||
|
"default": "auto",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "auto",
|
||||||
|
"description": "Machine accepts setpoints from a parent controller and runs autonomously."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "virtualControl",
|
||||||
|
"description": "Controlled via GUI setpoints; ignores parent commands."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "fysicalControl",
|
||||||
|
"description": "Controlled via physical buttons or switches; ignores external automated commands."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "maintenance",
|
||||||
|
"description": "No active control from auto, virtual, or fysical sources."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The operational mode of the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedActions":{
|
||||||
|
"default":{},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema":{
|
||||||
|
"auto": {
|
||||||
|
"default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in auto mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"virtualControl": {
|
||||||
|
"default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in virtualControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fysicalControl": {
|
||||||
|
"default": ["statusCheck", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in fysicalControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maintenance": {
|
||||||
|
"default": ["statusCheck"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in maintenance mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Information about valid command sources recognized by the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedSources":{
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema":{
|
||||||
|
"auto": {
|
||||||
|
"default": ["parent", "GUI", "fysical"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sources allowed in auto mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"virtualControl": {
|
||||||
|
"default": ["GUI", "fysical"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sources allowed in virtualControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fysicalControl": {
|
||||||
|
"default": ["fysical"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sources allowed in fysicalControl mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Information about valid command sources recognized by the machine."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"default": "parent",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "parent",
|
||||||
|
"description": "Commands are received from a parent controller."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "GUI",
|
||||||
|
"description": "Commands are received from a graphical user interface."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "fysical",
|
||||||
|
"description": "Commands are received from physical buttons or switches."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Information about valid command sources recognized by the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default": "statusCheck",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "statusCheck",
|
||||||
|
"description": "Checks the machine's state (mode, submode, operational status)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "execMovement",
|
||||||
|
"description": "Allows control through auto or GUI setpoints."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "execSequence",
|
||||||
|
"description": "Allows execution of sequences through auto or GUI controls."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "emergencyStop",
|
||||||
|
"description": "Overrides all commands and stops the machine immediately (safety scenarios)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the possible actions that can be performed on the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sequences":{
|
||||||
|
"default":{},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"startup": {
|
||||||
|
"default": ["starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for starting up the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shutdown": {
|
||||||
|
"default": ["stopping","coolingdown","idle"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for shutting down the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emergencystop": {
|
||||||
|
"default": ["emergencystop","off"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for an emergency stop."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boot": {
|
||||||
|
"default": ["idle","starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for booting up the machine."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Predefined sequences of states for the machine."
|
||||||
|
|
||||||
|
},
|
||||||
|
"calculationMode": {
|
||||||
|
"default": "medium",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "low",
|
||||||
|
"description": "Calculations run at fixed intervals (time-based)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "medium",
|
||||||
|
"description": "Calculations run when new setpoints arrive or measured changes occur (event-driven)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "high",
|
||||||
|
"description": "Calculations run on all event-driven info, including every movement."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The frequency at which calculations are performed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
349
configs/measurementConfig.json
Normal file
349
configs/measurementConfig.json
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "Measurement Configuration",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this measurement configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration. If not provided, defaults to null."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit of measurement for this configuration (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"logLevel": {
|
||||||
|
"default": "info",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "debug",
|
||||||
|
"description": "Log messages are printed for debugging purposes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "info",
|
||||||
|
"description": "Informational messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warn",
|
||||||
|
"description": "Warning messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "error",
|
||||||
|
"description": "Error messages are printed."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether logging is active. If true, log messages will be generated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"functionality": {
|
||||||
|
"softwareType": {
|
||||||
|
"default": "measurement",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specified software type for this configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "Sensor",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Indicates the role this configuration plays (e.g., sensor, controller, etc.)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asset": {
|
||||||
|
"uuid": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Asset tag number which is a universally unique identifier for this asset. May be null if not assigned."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geoLocation": {
|
||||||
|
"default": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "An object representing the asset's physical coordinates or location.",
|
||||||
|
"schema": {
|
||||||
|
"x": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "X coordinate of the asset's location."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Y coordinate of the asset's location."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"z": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Z coordinate of the asset's location."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The supplier or manufacturer of the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"default": "sensor",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "sensor",
|
||||||
|
"description": "A device that detects or measures a physical property and responds to it (e.g. temperature sensor)."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subType": {
|
||||||
|
"default": "pressure",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A more specific classification within 'type'. For example, 'pressure' for a pressure sensor."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accuracy": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The accuracy of the sensor, typically represented as a percentage or absolute value."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repeatability": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The repeatability of the sensor, typically represented as a percentage or absolute value."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scaling": {
|
||||||
|
"enabled": {
|
||||||
|
"default": false,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether input scaling is active. If true, input values will be scaled according to the parameters below."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inputMin": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The minimum expected input value before scaling."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inputMax": {
|
||||||
|
"default": 1,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The maximum expected input value before scaling."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"absMin": {
|
||||||
|
"default": 50,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The absolute minimum value that can be read or displayed after scaling."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"absMax": {
|
||||||
|
"default": 100,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The absolute maximum value that can be read or displayed after scaling."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "A constant offset to apply to the scaled output (e.g., to calibrate zero-points)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"smoothing": {
|
||||||
|
"smoothWindow": {
|
||||||
|
"default": 10,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 1,
|
||||||
|
"description": "Determines the size of the data window (number of samples) used for smoothing operations."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"smoothMethod": {
|
||||||
|
"default": "mean",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "none",
|
||||||
|
"description": "No smoothing is applied; raw data is passed through."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "mean",
|
||||||
|
"description": "Calculates the simple arithmetic mean (average) of the data points in a window."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "min",
|
||||||
|
"description": "Selects the smallest (minimum) value among the data points in a window."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "max",
|
||||||
|
"description": "Selects the largest (maximum) value among the data points in a window."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "sd",
|
||||||
|
"description": "Computes the standard deviation to measure the variation or spread of the data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "lowPass",
|
||||||
|
"description": "Filters out high-frequency components, allowing only lower frequencies to pass."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "highPass",
|
||||||
|
"description": "Filters out low-frequency components, allowing only higher frequencies to pass."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "weightedMovingAverage",
|
||||||
|
"description": "Applies varying weights to each data point in a window before averaging."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "bandPass",
|
||||||
|
"description": "Filters the signal to allow only frequencies within a specific range to pass."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "median",
|
||||||
|
"description": "Selects the median (middle) value in a window, minimizing the effect of outliers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "kalman",
|
||||||
|
"description": "Applies a Kalman filter to combine noisy measurements over time for more accurate estimates."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "savitzkyGolay",
|
||||||
|
"description": "Uses a polynomial smoothing filter on a moving window, which can also provide derivative estimates."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"simulation": {
|
||||||
|
"enabled": {
|
||||||
|
"default": false,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, the system operates in simulation mode, generating simulated values instead of using real inputs."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"safeCalibrationTime": {
|
||||||
|
"default": 100,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 100,
|
||||||
|
"description": "Time to wait before finalizing calibration in simulation mode (in milliseconds or appropriate unit)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interpolation": {
|
||||||
|
"percentMin": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Minimum percentage for interpolation or data scaling operations."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"percentMax": {
|
||||||
|
"default": 100,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"max": 100,
|
||||||
|
"description": "Maximum percentage for interpolation or data scaling operations."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outlierDetection": {
|
||||||
|
"enabled": {
|
||||||
|
"default": false,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether outlier detection is enabled. If true, outliers will be identified and handled according to the method specified."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"default": "zScore",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "zScore",
|
||||||
|
"description": "Uses the Z-score method to identify outliers based on standard deviations from the mean."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "iqr",
|
||||||
|
"description": "Uses the Interquartile Range (IQR) method to identify outliers based on the spread of the middle 50% of the data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "modifiedZScore",
|
||||||
|
"description": "Uses a modified Z-score method that is more robust to small sample sizes."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"default": 3,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The threshold value used by the selected outlier detection method. For example, a Z-score threshold of 3.0."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
datasets/assetData/pumps/DAB/centrifugal pumps/models.json
Normal file
36
datasets/assetData/pumps/DAB/centrifugal pumps/models.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "DAB Evosta 2 20-75",
|
||||||
|
"description": "N/A",
|
||||||
|
"machineCurve": {
|
||||||
|
"np": {
|
||||||
|
"200": {
|
||||||
|
"x": [0, 20, 40, 60, 80, 100],
|
||||||
|
"y": [5, 8, 12, 15, 17, 18]
|
||||||
|
},
|
||||||
|
"300": {
|
||||||
|
"x": [0, 20, 40, 60, 80, 100],
|
||||||
|
"y": [20, 28, 32, 34, 35, 35]
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"x": [0, 20, 40, 60, 80, 100],
|
||||||
|
"y": [35, 38, 42, 45, 47, 48]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nq": {
|
||||||
|
"200": {
|
||||||
|
"x": [0, 20, 40, 60, 80, 100],
|
||||||
|
"y": [0, 0.4, 0.8, 1.2, 1.6, 2.0]
|
||||||
|
},
|
||||||
|
"300": {
|
||||||
|
"x": [0, 20, 40, 60, 80, 100],
|
||||||
|
"y": [0, 0.72, 1.44, 2.16, 2.88, 3.6]
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"x": [0, 20, 40, 60, 80, 100],
|
||||||
|
"y": [0, 0.8, 1.6, 2.4, 3.2, 4.0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
6
datasets/assetData/pumps/DAB/subtypes.json
Normal file
6
datasets/assetData/pumps/DAB/subtypes.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "centrifugal pumps"
|
||||||
|
}
|
||||||
|
]
|
||||||
1068
datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json
Normal file
1068
datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json
Normal file
File diff suppressed because it is too large
Load Diff
6
datasets/assetData/pumps/hydrostal/subtypes.json
Normal file
6
datasets/assetData/pumps/hydrostal/subtypes.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "centrifugal pumps"
|
||||||
|
}
|
||||||
|
]
|
||||||
10
datasets/assetData/pumps/suppliers.json
Normal file
10
datasets/assetData/pumps/suppliers.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "hydrostal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "DAB"
|
||||||
|
}
|
||||||
|
]
|
||||||
7
datasets/assetData/sensors/eastron/power/models.json
Normal file
7
datasets/assetData/sensors/eastron/power/models.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "SDM120MODBUS",
|
||||||
|
"description": "De SDM120MODBUS kWh meter is een kWh meter die geschikt is voor het meten van zowel verbruik als teruglevering van stroom. Dat maakt deze meter ook zeer geschikt in combinatie met zonnepanelen."
|
||||||
|
}
|
||||||
|
]
|
||||||
6
datasets/assetData/sensors/eastron/subTypes.json
Normal file
6
datasets/assetData/sensors/eastron/subTypes.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "power"
|
||||||
|
}
|
||||||
|
]
|
||||||
7
datasets/assetData/sensors/eh/flow/models.json
Normal file
7
datasets/assetData/sensors/eh/flow/models.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Proline Promag W 400",
|
||||||
|
"description": "A flow meter used for measuring the flow of liquids in various industrial applications."
|
||||||
|
}
|
||||||
|
]
|
||||||
6
datasets/assetData/sensors/eh/subTypes.json
Normal file
6
datasets/assetData/sensors/eh/subTypes.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "flow"
|
||||||
|
}
|
||||||
|
]
|
||||||
14
datasets/assetData/sensors/suppliers.json
Normal file
14
datasets/assetData/sensors/suppliers.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "vega"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "eh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"name": "eastron"
|
||||||
|
}
|
||||||
|
]
|
||||||
6
datasets/assetData/sensors/vega/pressure/models.json
Normal file
6
datasets/assetData/sensors/vega/pressure/models.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Vegabar 14",
|
||||||
|
"description": "N/A"
|
||||||
|
}
|
||||||
|
]
|
||||||
14
datasets/assetData/sensors/vega/subtypes.json
Normal file
14
datasets/assetData/sensors/vega/subtypes.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "pressure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "flow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"name": "temperature"
|
||||||
|
}
|
||||||
|
]
|
||||||
30
datasets/assetData/sensors/vega/temperature/models.json
Normal file
30
datasets/assetData/sensors/vega/temperature/models.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "VegaTemp 10",
|
||||||
|
"description": "Low cost sensor for general purpose applications."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaTemp 20",
|
||||||
|
"description": "High accuracy sensor for laboratory applications."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaTemp 30",
|
||||||
|
"description": "High accuracy sensor for industrial applications."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaTemp 40",
|
||||||
|
"description": "High accuracy sensor for environmental monitoring."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaTemp 50",
|
||||||
|
"description": "High accuracy temperature sensor for industrial applications."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaTherm 22",
|
||||||
|
"description": "Compact sensor ideal for environmental monitoring."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaTemp 100",
|
||||||
|
"description": "Robust sensor designed for high temperature ranges."
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
datasets/lstmData/tfjs_model/group1-shard1of1.bin
Normal file
BIN
datasets/lstmData/tfjs_model/group1-shard1of1.bin
Normal file
Binary file not shown.
297
datasets/lstmData/tfjs_model/model.json
Normal file
297
datasets/lstmData/tfjs_model/model.json
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
{
|
||||||
|
"format": "layers-model",
|
||||||
|
"generatedBy": "keras v3.8.0",
|
||||||
|
"convertedBy": "TensorFlow.js Converter v4.22.0",
|
||||||
|
"modelTopology": {
|
||||||
|
"keras_version": "3.8.0",
|
||||||
|
"backend": "tensorflow",
|
||||||
|
"model_config": {
|
||||||
|
"class_name": "Sequential",
|
||||||
|
"config": {
|
||||||
|
"name": "sequential_8",
|
||||||
|
"trainable": true,
|
||||||
|
"dtype": {
|
||||||
|
"module": "keras",
|
||||||
|
"class_name": "DTypePolicy",
|
||||||
|
"config": {
|
||||||
|
"name": "float32"
|
||||||
|
},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"class_name": "InputLayer",
|
||||||
|
"config": {
|
||||||
|
"batch_shape": [
|
||||||
|
null,
|
||||||
|
24,
|
||||||
|
166
|
||||||
|
],
|
||||||
|
"dtype": "float32",
|
||||||
|
"sparse": false,
|
||||||
|
"name": "input_layer_8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class_name": "LSTM",
|
||||||
|
"config": {
|
||||||
|
"name": "lstm_16",
|
||||||
|
"trainable": true,
|
||||||
|
"dtype": {
|
||||||
|
"module": "keras",
|
||||||
|
"class_name": "DTypePolicy",
|
||||||
|
"config": {
|
||||||
|
"name": "float32"
|
||||||
|
},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"return_sequences": true,
|
||||||
|
"return_state": false,
|
||||||
|
"go_backwards": false,
|
||||||
|
"stateful": false,
|
||||||
|
"unroll": false,
|
||||||
|
"zero_output_for_mask": false,
|
||||||
|
"units": 5,
|
||||||
|
"activation": "tanh",
|
||||||
|
"recurrent_activation": "sigmoid",
|
||||||
|
"use_bias": true,
|
||||||
|
"kernel_initializer": {
|
||||||
|
"module": "keras.initializers",
|
||||||
|
"class_name": "GlorotUniform",
|
||||||
|
"config": {
|
||||||
|
"seed": null
|
||||||
|
},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"recurrent_initializer": {
|
||||||
|
"module": "keras.initializers",
|
||||||
|
"class_name": "Orthogonal",
|
||||||
|
"config": {
|
||||||
|
"seed": null,
|
||||||
|
"gain": 1
|
||||||
|
},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"bias_initializer": {
|
||||||
|
"module": "keras.initializers",
|
||||||
|
"class_name": "Zeros",
|
||||||
|
"config": {},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"unit_forget_bias": true,
|
||||||
|
"kernel_regularizer": null,
|
||||||
|
"recurrent_regularizer": null,
|
||||||
|
"bias_regularizer": null,
|
||||||
|
"activity_regularizer": null,
|
||||||
|
"kernel_constraint": null,
|
||||||
|
"recurrent_constraint": null,
|
||||||
|
"bias_constraint": null,
|
||||||
|
"dropout": 0,
|
||||||
|
"recurrent_dropout": 0,
|
||||||
|
"seed": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class_name": "LSTM",
|
||||||
|
"config": {
|
||||||
|
"name": "lstm_17",
|
||||||
|
"trainable": true,
|
||||||
|
"dtype": {
|
||||||
|
"module": "keras",
|
||||||
|
"class_name": "DTypePolicy",
|
||||||
|
"config": {
|
||||||
|
"name": "float32"
|
||||||
|
},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"return_sequences": false,
|
||||||
|
"return_state": false,
|
||||||
|
"go_backwards": false,
|
||||||
|
"stateful": false,
|
||||||
|
"unroll": false,
|
||||||
|
"zero_output_for_mask": false,
|
||||||
|
"units": 5,
|
||||||
|
"activation": "tanh",
|
||||||
|
"recurrent_activation": "sigmoid",
|
||||||
|
"use_bias": true,
|
||||||
|
"kernel_initializer": {
|
||||||
|
"module": "keras.initializers",
|
||||||
|
"class_name": "GlorotUniform",
|
||||||
|
"config": {
|
||||||
|
"seed": null
|
||||||
|
},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"recurrent_initializer": {
|
||||||
|
"module": "keras.initializers",
|
||||||
|
"class_name": "Orthogonal",
|
||||||
|
"config": {
|
||||||
|
"seed": null,
|
||||||
|
"gain": 1
|
||||||
|
},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"bias_initializer": {
|
||||||
|
"module": "keras.initializers",
|
||||||
|
"class_name": "Zeros",
|
||||||
|
"config": {},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"unit_forget_bias": true,
|
||||||
|
"kernel_regularizer": null,
|
||||||
|
"recurrent_regularizer": null,
|
||||||
|
"bias_regularizer": null,
|
||||||
|
"activity_regularizer": null,
|
||||||
|
"kernel_constraint": null,
|
||||||
|
"recurrent_constraint": null,
|
||||||
|
"bias_constraint": null,
|
||||||
|
"dropout": 0,
|
||||||
|
"recurrent_dropout": 0,
|
||||||
|
"seed": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class_name": "Dense",
|
||||||
|
"config": {
|
||||||
|
"name": "dense_8",
|
||||||
|
"trainable": true,
|
||||||
|
"dtype": {
|
||||||
|
"module": "keras",
|
||||||
|
"class_name": "DTypePolicy",
|
||||||
|
"config": {
|
||||||
|
"name": "float32"
|
||||||
|
},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"units": 1,
|
||||||
|
"activation": "linear",
|
||||||
|
"use_bias": true,
|
||||||
|
"kernel_initializer": {
|
||||||
|
"module": "keras.initializers",
|
||||||
|
"class_name": "GlorotUniform",
|
||||||
|
"config": {
|
||||||
|
"seed": null
|
||||||
|
},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"bias_initializer": {
|
||||||
|
"module": "keras.initializers",
|
||||||
|
"class_name": "Zeros",
|
||||||
|
"config": {},
|
||||||
|
"registered_name": null
|
||||||
|
},
|
||||||
|
"kernel_regularizer": null,
|
||||||
|
"bias_regularizer": null,
|
||||||
|
"kernel_constraint": null,
|
||||||
|
"bias_constraint": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build_input_shape": [
|
||||||
|
null,
|
||||||
|
24,
|
||||||
|
166
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"training_config": {
|
||||||
|
"loss": "mse",
|
||||||
|
"loss_weights": null,
|
||||||
|
"metrics": null,
|
||||||
|
"weighted_metrics": null,
|
||||||
|
"run_eagerly": false,
|
||||||
|
"steps_per_execution": 1,
|
||||||
|
"jit_compile": false,
|
||||||
|
"optimizer_config": {
|
||||||
|
"class_name": "Adam",
|
||||||
|
"config": {
|
||||||
|
"name": "adam",
|
||||||
|
"learning_rate": 0.00009999999747378752,
|
||||||
|
"weight_decay": null,
|
||||||
|
"clipnorm": null,
|
||||||
|
"global_clipnorm": null,
|
||||||
|
"clipvalue": null,
|
||||||
|
"use_ema": false,
|
||||||
|
"ema_momentum": 0.99,
|
||||||
|
"ema_overwrite_frequency": null,
|
||||||
|
"loss_scale_factor": null,
|
||||||
|
"gradient_accumulation_steps": null,
|
||||||
|
"beta_1": 0.9,
|
||||||
|
"beta_2": 0.999,
|
||||||
|
"epsilon": 1e-7,
|
||||||
|
"amsgrad": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"weightsManifest": [
|
||||||
|
{
|
||||||
|
"paths": [
|
||||||
|
"group1-shard1of1.bin"
|
||||||
|
],
|
||||||
|
"weights": [
|
||||||
|
{
|
||||||
|
"name": "dense_8/kernel",
|
||||||
|
"shape": [
|
||||||
|
5,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"dtype": "float32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dense_8/bias",
|
||||||
|
"shape": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"dtype": "float32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lstm_16/lstm_cell/kernel",
|
||||||
|
"shape": [
|
||||||
|
166,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"dtype": "float32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lstm_16/lstm_cell/recurrent_kernel",
|
||||||
|
"shape": [
|
||||||
|
5,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"dtype": "float32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lstm_16/lstm_cell/bias",
|
||||||
|
"shape": [
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"dtype": "float32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lstm_17/lstm_cell/kernel",
|
||||||
|
"shape": [
|
||||||
|
5,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"dtype": "float32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lstm_17/lstm_cell/recurrent_kernel",
|
||||||
|
"shape": [
|
||||||
|
5,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"dtype": "float32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lstm_17/lstm_cell/bias",
|
||||||
|
"shape": [
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"dtype": "float32"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
datasets/lstmData/tfjs_model/model_weights.weights.h5
Normal file
BIN
datasets/lstmData/tfjs_model/model_weights.weights.h5
Normal file
Binary file not shown.
66
datasets/unitData.json
Normal file
66
datasets/unitData.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"category": "flow",
|
||||||
|
"values": [
|
||||||
|
{ "value": "m3/h", "description": "Cubic meters per hour" },
|
||||||
|
{ "value": "l/s", "description": "Liters per second" },
|
||||||
|
{ "value": "l/min", "description": "Liters per minute" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "pressure",
|
||||||
|
"values": [
|
||||||
|
{ "value": "bar", "description": "Pressure in bars" },
|
||||||
|
{ "value": "mbar", "description": "Pressure in millibars" },
|
||||||
|
{ "value": "psi", "description": "Pounds per square inch" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "temperature",
|
||||||
|
"values": [
|
||||||
|
{ "value": "°C", "description": "Temperature in Celsius" },
|
||||||
|
{ "value": "°F", "description": "Temperature in Fahrenheit" },
|
||||||
|
{ "value": "K", "description": "Temperature in Kelvin" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "percentage",
|
||||||
|
"values": [
|
||||||
|
{ "value": "%", "description": "Percentage" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "power",
|
||||||
|
"values": [
|
||||||
|
{ "value": "kW", "description": "Kilowatt" },
|
||||||
|
{ "value": "W", "description": "Watt" },
|
||||||
|
{ "value": "kWh", "description": "Kilowatt hour" },
|
||||||
|
{ "value": "Wh", "description": "Watt hour" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "energy",
|
||||||
|
"values": [
|
||||||
|
{ "value": "kWh", "description": "Kilowatt hour" },
|
||||||
|
{ "value": "Wh", "description": "Watt hour" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "distance",
|
||||||
|
"values": [
|
||||||
|
{ "value": "m", "description": "Meters" },
|
||||||
|
{ "value": "cm", "description": "Centimeters" },
|
||||||
|
{ "value": "mm", "description": "Millimeters" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "centrifugal pumps",
|
||||||
|
"values": [
|
||||||
|
{ "value": "m3/h", "description": "Cubic meters"},
|
||||||
|
{ "value": "l/s", "description": "Liters" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
3
helper/assetUtils.js
Normal file
3
helper/assetUtils.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function getAssetVariables() {
|
||||||
|
|
||||||
|
}
|
||||||
243
helper/childRegistrationUtils.js
Normal file
243
helper/childRegistrationUtils.js
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// ChildRegistrationUtils.js
|
||||||
|
class ChildRegistrationUtils {
|
||||||
|
constructor(mainClass) {
|
||||||
|
this.mainClass = mainClass; // Reference to the main class
|
||||||
|
this.logger = mainClass.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerChild(child, positionVsParent) {
|
||||||
|
const { softwareType } = child.config.functionality;
|
||||||
|
const { name, id, unit } = child.config.general;
|
||||||
|
const { type = "", subType = "" } = child.config.asset || {};
|
||||||
|
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][type])
|
||||||
|
this.mainClass.child[softwareType][type] = {};
|
||||||
|
if (!this.mainClass.child[softwareType][type][subType])
|
||||||
|
this.mainClass.child[softwareType][type][subType] = {};
|
||||||
|
|
||||||
|
// Use an array to handle multiple subtypes
|
||||||
|
if (!Array.isArray(this.mainClass.child[softwareType][type][subType])) {
|
||||||
|
this.mainClass.child[softwareType][type][subType] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the child in the cloud when available and supply the new child on base of tagcode OLIFANT WE NEED TO FIX THIS SO ITS DYNAMIC!
|
||||||
|
/*
|
||||||
|
try{
|
||||||
|
const url = "https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api/asset/create_asset.php?";
|
||||||
|
const TagCode = child.config.asset.tagCode;
|
||||||
|
//console.log(`Register child => ${TagCode}`);
|
||||||
|
const completeURL = url + `asset_product_model_id=1&asset_product_model_uuid=123456789&asset_name=AssetNaam&asset_description=Beschrijving&asset_status=actief&asset_profile_id=1&asset_location_id=1&asset_process_id=11&asset_tag_number=${TagCode}&child_assets=[L6616]`;
|
||||||
|
|
||||||
|
await fetch(completeURL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(e){
|
||||||
|
console.log("Error saving assetID and tagnumber", e);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Push the new child to the array of the mainclass so we can track the childs
|
||||||
|
this.mainClass.child[softwareType][type][subType].push({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
unit,
|
||||||
|
emitter,
|
||||||
|
});
|
||||||
|
|
||||||
|
//then connect the child depending on the type subtype etc..
|
||||||
|
this.connectChild(
|
||||||
|
id,
|
||||||
|
softwareType,
|
||||||
|
emitter,
|
||||||
|
type,
|
||||||
|
child,
|
||||||
|
subType,
|
||||||
|
positionVsParent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectChild(
|
||||||
|
id,
|
||||||
|
softwareType,
|
||||||
|
emitter,
|
||||||
|
type,
|
||||||
|
child,
|
||||||
|
subType,
|
||||||
|
positionVsParent
|
||||||
|
) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Connecting child id=${id}: desc=${softwareType}, type=${type},subType=${subType}, position=${positionVsParent}`
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (softwareType) {
|
||||||
|
case "measurement":
|
||||||
|
this.logger.debug(
|
||||||
|
`Registering measurement child: ${id} with type=${type}`
|
||||||
|
);
|
||||||
|
this.connectMeasurement(child, subType, 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 "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, subType, position) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Connecting measurement child: ${subType} with position=${position}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if subType is valid
|
||||||
|
if (!subType) {
|
||||||
|
this.logger.error(`Invalid subType for measurement: ${subType}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the measurement to a number - logging each step for debugging
|
||||||
|
try {
|
||||||
|
this.logger.debug(
|
||||||
|
`Initializing measurement: ${subType}, position: ${position} value: 0`
|
||||||
|
);
|
||||||
|
const typeResult = this.mainClass.measurements.type(subType);
|
||||||
|
const variantResult = typeResult.variant("measured");
|
||||||
|
const positionResult = variantResult.position(position);
|
||||||
|
positionResult.value(0);
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Subscribing on mAbs event for measurement: ${subType}, position: ${position}`
|
||||||
|
);
|
||||||
|
// Listen for the mAbs event and update the measurement
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Successfully initialized measurement: ${subType}, 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(subType)
|
||||||
|
.variant("measured")
|
||||||
|
.position(position)
|
||||||
|
.value(value);
|
||||||
|
this.mainClass.updateMeasurement("measured", subType, value, position);
|
||||||
|
//this.logger.debug(`--------->>>>>>>>>Updated measurement: ${subType}, 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.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
94
helper/configUtils.js
Normal file
94
helper/configUtils.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* @file configUtils.js
|
||||||
|
*
|
||||||
|
* Permission is hereby granted to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to use it for personal
|
||||||
|
* or non-commercial purposes, with the following restrictions:
|
||||||
|
*
|
||||||
|
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
|
||||||
|
* be copied, merged, distributed, sublicensed, or sold without explicit
|
||||||
|
* prior written permission from the author.
|
||||||
|
*
|
||||||
|
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
|
||||||
|
* a valid license, obtainable only with the explicit consent of the author.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Ownership of this code remains solely with the original author. Unauthorized
|
||||||
|
* use of this Software is strictly prohibited.
|
||||||
|
*
|
||||||
|
* @summary Utility for managing and validating configuration values.
|
||||||
|
* @description Utility for managing and validating configuration values.
|
||||||
|
* @module ConfigUtils
|
||||||
|
* @requires ValidationUtils
|
||||||
|
* @requires Logger
|
||||||
|
* @exports ConfigUtils
|
||||||
|
* @version 0.1.0
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
const ValidationUtils = require("./validationUtils");
|
||||||
|
const Logger = require("./logger");
|
||||||
|
|
||||||
|
class ConfigUtils {
|
||||||
|
constructor(defaultConfig, IloggerEnabled , IloggerLevel) {
|
||||||
|
const loggerEnabled = IloggerEnabled || true;
|
||||||
|
const loggerLevel = IloggerLevel || "warn";
|
||||||
|
this.logger = new Logger(loggerEnabled, loggerLevel, 'ConfigUtils');
|
||||||
|
this.defaultConfig = defaultConfig;
|
||||||
|
this.validationUtils = new ValidationUtils(loggerEnabled, loggerLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize configuration
|
||||||
|
initConfig(config) {
|
||||||
|
this.logger.info("Initializing configuration...");
|
||||||
|
|
||||||
|
// Validate the provided configuration
|
||||||
|
const validatedConfig = this.validationUtils.validateSchema(config, this.defaultConfig, this.defaultConfig.functionality.softwareType.default);
|
||||||
|
|
||||||
|
this.logger.info("Configuration initialized successfully.");
|
||||||
|
|
||||||
|
return validatedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Update configuration
|
||||||
|
updateConfig(currentConfig, newConfig) {
|
||||||
|
this.logger.info("Updating configuration...");
|
||||||
|
|
||||||
|
// Merge 2 configs and validate the result
|
||||||
|
const mergedConfig = this.mergeObjects(currentConfig, newConfig);
|
||||||
|
|
||||||
|
// Merge current configuration with new values
|
||||||
|
const updatedConfig = this.validationUtils.validateSchema(mergedConfig, this.defaultConfig, this.defaultConfig.functionality.softwareType.default);
|
||||||
|
|
||||||
|
this.logger.info("Configuration updated successfully.");
|
||||||
|
return updatedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through objects and merge them obj1 will be updated with obj2 values
|
||||||
|
mergeObjects(obj1, obj2) {
|
||||||
|
for (let key in obj2) {
|
||||||
|
if (obj2.hasOwnProperty(key)) {
|
||||||
|
if (typeof obj2[key] === 'object') {
|
||||||
|
if (!obj1[key]) {
|
||||||
|
obj1[key] = {};
|
||||||
|
}
|
||||||
|
this.mergeObjects(obj1[key], obj2[key]);
|
||||||
|
} else {
|
||||||
|
obj1[key] = obj2[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ConfigUtils;
|
||||||
57
helper/logger.js
Normal file
57
helper/logger.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
class Logger {
|
||||||
|
constructor(logging = true, logLevel = 'debug', nameModule = 'N/A') {
|
||||||
|
this.logging = logging; // Boolean flag to enable/disable logging
|
||||||
|
this.logLevel = logLevel; // Default log level: 'debug', 'info', 'warn', 'error'
|
||||||
|
this.levels = ['debug', 'info', 'warn', 'error']; // Log levels in order of severity
|
||||||
|
this.nameModule = nameModule; // Name of the module that uses the logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a log message should be displayed based on current log level
|
||||||
|
shouldLog(level) {
|
||||||
|
return this.levels.indexOf(level) >= this.levels.indexOf(this.logLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log a debug message
|
||||||
|
debug(message) {
|
||||||
|
if (this.logging && this.shouldLog('debug')) {
|
||||||
|
console.debug(`[DEBUG] -> ${this.nameModule}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log an info message
|
||||||
|
info(message) {
|
||||||
|
if (this.logging && this.shouldLog('info')) {
|
||||||
|
console.info(`[INFO] -> ${this.nameModule}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log a warning message
|
||||||
|
warn(message) {
|
||||||
|
if (this.logging && this.shouldLog('warn')) {
|
||||||
|
console.warn(`[WARN] -> ${this.nameModule}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log an error message
|
||||||
|
error(message) {
|
||||||
|
if (this.logging && this.shouldLog('error')) {
|
||||||
|
console.error(`[ERROR] -> ${this.nameModule}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the log level dynamically
|
||||||
|
setLogLevel(level) {
|
||||||
|
if (this.levels.includes(level)) {
|
||||||
|
this.logLevel = level;
|
||||||
|
} else {
|
||||||
|
console.error(`[ERROR ${nameModule}]: Invalid log level: ${level}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the logging on or off
|
||||||
|
toggleLogging() {
|
||||||
|
this.logging = !this.logging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Logger;
|
||||||
187
helper/measurements/Measurement.js
Normal file
187
helper/measurements/Measurement.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// Add unit conversion support
|
||||||
|
const convertModule = require('../../../convert/dependencies/index');
|
||||||
|
|
||||||
|
class Measurement {
|
||||||
|
constructor(type, variant, position, windowSize) {
|
||||||
|
this.type = type; // e.g. 'pressure', 'flow', etc.
|
||||||
|
this.variant = variant; // e.g. 'predicted' or 'measured', etc..
|
||||||
|
this.position = position; // Downstream or upstream of parent object
|
||||||
|
this.windowSize = windowSize; // Rolling window size
|
||||||
|
|
||||||
|
// Place all data inside an array
|
||||||
|
this.values = []; // Array to store all values
|
||||||
|
this.timestamps = []; // Array to store all timestamps
|
||||||
|
|
||||||
|
// Unit tracking
|
||||||
|
this.unit = null; // Current unit of measurement
|
||||||
|
|
||||||
|
// For tracking differences if this is a calculated difference measurement
|
||||||
|
this.isDifference = false;
|
||||||
|
this.sourcePositions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Updater methods --
|
||||||
|
setType(type) {
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariant(variant) {
|
||||||
|
this.variant = variant;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition(position) {
|
||||||
|
this.position = position;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value, timestamp = Date.now()) {
|
||||||
|
/*
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
value = null ;
|
||||||
|
//throw new Error('Value cannot be null or undefined');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
//shift the oldest value
|
||||||
|
if(this.values.length >= this.windowSize){
|
||||||
|
this.values.shift();
|
||||||
|
this.timestamps.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
//push the new value
|
||||||
|
this.values.push(value);
|
||||||
|
this.timestamps.push(timestamp);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUnit(unit) {
|
||||||
|
this.unit = unit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Getter methods --
|
||||||
|
getCurrentValue() {
|
||||||
|
if (this.values.length === 0) return null;
|
||||||
|
return this.values[this.values.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
getAverage() {
|
||||||
|
if (this.values.length === 0) return null;
|
||||||
|
const sum = this.values.reduce((acc, val) => acc + val, 0);
|
||||||
|
return sum / this.values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMin() {
|
||||||
|
if (this.values.length === 0) return null;
|
||||||
|
return Math.min(...this.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMax() {
|
||||||
|
if (this.values.length === 0) return null;
|
||||||
|
return Math.max(...this.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllValues() {
|
||||||
|
return {
|
||||||
|
values: [...this.values],
|
||||||
|
timestamps: [...this.timestamps],
|
||||||
|
unit: this.unit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Position-based methods --
|
||||||
|
|
||||||
|
// Create a new measurement that is the difference between two positions
|
||||||
|
static createDifference(upstreamMeasurement, downstreamMeasurement) {
|
||||||
|
console.log('hello:');
|
||||||
|
if (upstreamMeasurement.type !== downstreamMeasurement.type ||
|
||||||
|
upstreamMeasurement.variant !== downstreamMeasurement.variant) {
|
||||||
|
throw new Error('Cannot calculate difference between different measurement types or variants');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure units match
|
||||||
|
let downstream = downstreamMeasurement;
|
||||||
|
if (upstreamMeasurement.unit && downstream.unit &&
|
||||||
|
upstreamMeasurement.unit !== downstream.unit) {
|
||||||
|
downstream = downstream.convertTo(upstreamMeasurement.unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new difference measurement
|
||||||
|
const diffMeasurement = new Measurement(
|
||||||
|
upstreamMeasurement.type,
|
||||||
|
upstreamMeasurement.variant,
|
||||||
|
'difference',
|
||||||
|
Math.min(upstreamMeasurement.windowSize, downstream.windowSize)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark as a difference measurement and keep track of sources
|
||||||
|
diffMeasurement.isDifference = true;
|
||||||
|
diffMeasurement.sourcePositions = ['upstream', 'downstream'];
|
||||||
|
|
||||||
|
// Calculate all differences where timestamps match
|
||||||
|
const upValues = upstreamMeasurement.getAllValues();
|
||||||
|
const downValues = downstream.getAllValues();
|
||||||
|
|
||||||
|
// Match timestamps and calculate differences
|
||||||
|
for (let i = 0; i < upValues.timestamps.length; i++) {
|
||||||
|
const upTimestamp = upValues.timestamps[i];
|
||||||
|
const downIndex = downValues.timestamps.indexOf(upTimestamp);
|
||||||
|
|
||||||
|
if (downIndex !== -1) {
|
||||||
|
|
||||||
|
const diff = upValues.values[i] - downValues.values[downIndex];
|
||||||
|
diffMeasurement.setValue(diff, upTimestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diffMeasurement.setUnit(upstreamMeasurement.unit);
|
||||||
|
|
||||||
|
return diffMeasurement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Additional getter methods --
|
||||||
|
getLatestTimestamp() {
|
||||||
|
if (this.timestamps.length === 0) return null;
|
||||||
|
return this.timestamps[this.timestamps.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
getValueAtTimestamp(timestamp) {
|
||||||
|
const index = this.timestamps.indexOf(timestamp);
|
||||||
|
if (index === -1) return null;
|
||||||
|
return this.values[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Unit conversion methods --
|
||||||
|
convertTo(targetUnit) {
|
||||||
|
if (!this.unit) {
|
||||||
|
throw new Error('Current unit not set, cannot convert');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const convertedValues = this.values.map(value =>
|
||||||
|
convertModule.convert(value).from(this.unit).to(targetUnit)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newMeasurement = new Measurement(
|
||||||
|
this.type,
|
||||||
|
this.variant,
|
||||||
|
this.position,
|
||||||
|
this.windowSize
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy values and timestamps
|
||||||
|
newMeasurement.values = convertedValues;
|
||||||
|
newMeasurement.timestamps = [...this.timestamps];
|
||||||
|
newMeasurement.unit = targetUnit;
|
||||||
|
|
||||||
|
return newMeasurement;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Unit conversion failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Measurement;
|
||||||
56
helper/measurements/MeasurementBuilder.js
Normal file
56
helper/measurements/MeasurementBuilder.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
const Measurement = require('./Measurement');
|
||||||
|
|
||||||
|
class MeasurementBuilder {
|
||||||
|
constructor() {
|
||||||
|
this.type = null;
|
||||||
|
this.variant = null;
|
||||||
|
this.position = null;
|
||||||
|
this.windowSize = 10; // Default window size
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. 'pressure', 'flow', etc.
|
||||||
|
setType(type) {
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. 'predicted' or 'measured', etc..
|
||||||
|
setVariant(variant) {
|
||||||
|
this.variant = variant;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downstream or upstream of parent object
|
||||||
|
setPosition(position) {
|
||||||
|
this.position = position;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// default size of the data that gets tracked
|
||||||
|
setWindowSize(windowSize) {
|
||||||
|
this.windowSize = windowSize;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
// Validate required fields
|
||||||
|
if (!this.type) {
|
||||||
|
throw new Error('Measurement type is required');
|
||||||
|
}
|
||||||
|
if (!this.variant) {
|
||||||
|
throw new Error('Measurement variant is required');
|
||||||
|
}
|
||||||
|
if (!this.position) {
|
||||||
|
throw new Error('Measurement position is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Measurement(
|
||||||
|
this.type,
|
||||||
|
this.variant,
|
||||||
|
this.position,
|
||||||
|
this.windowSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MeasurementBuilder;
|
||||||
200
helper/measurements/MeasurementContainer.js
Normal file
200
helper/measurements/MeasurementContainer.js
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
const MeasurementBuilder = require('./MeasurementBuilder');
|
||||||
|
|
||||||
|
class MeasurementContainer {
|
||||||
|
constructor(options = {}, logger) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.measurements = {};
|
||||||
|
this.windowSize = options.windowSize || 10; // Default window size
|
||||||
|
|
||||||
|
// For chaining context
|
||||||
|
this._currentType = null;
|
||||||
|
this._currentVariant = null;
|
||||||
|
this._currentPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chainable methods
|
||||||
|
type(typeName) {
|
||||||
|
this._currentType = typeName;
|
||||||
|
this._currentVariant = null;
|
||||||
|
this._currentPosition = null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
variant(variantName) {
|
||||||
|
if (!this._currentType) {
|
||||||
|
throw new Error('Type must be specified before variant');
|
||||||
|
}
|
||||||
|
this._currentVariant = variantName;
|
||||||
|
this._currentPosition = null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
position(positionName) {
|
||||||
|
if (!this._currentVariant) {
|
||||||
|
throw new Error('Variant must be specified before position');
|
||||||
|
}
|
||||||
|
this._currentPosition = positionName;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
unit(unitName) {
|
||||||
|
if (!this._ensureChainIsValid()) return this;
|
||||||
|
|
||||||
|
const measurement = this._getOrCreateMeasurement();
|
||||||
|
measurement.setUnit(unitName);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal operations - get data out
|
||||||
|
get() {
|
||||||
|
if (!this._ensureChainIsValid()) return null;
|
||||||
|
return this._getOrCreateMeasurement();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentValue() {
|
||||||
|
const measurement = this.get();
|
||||||
|
return measurement ? measurement.getCurrentValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAverage() {
|
||||||
|
const measurement = this.get();
|
||||||
|
return measurement ? measurement.getAverage() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMin() {
|
||||||
|
const measurement = this.get();
|
||||||
|
return measurement ? measurement.getMin() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMax() {
|
||||||
|
const measurement = this.get();
|
||||||
|
return measurement ? measurement.getMax() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllValues() {
|
||||||
|
const measurement = this.get();
|
||||||
|
return measurement ? measurement.getAllValues() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difference calculations between positions
|
||||||
|
difference() {
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: downstreamForCalc.getCurrentValue() - upstream.getCurrentValue() ,
|
||||||
|
avgDiff: downstreamForCalc.getAverage() - upstream.getAverage() ,
|
||||||
|
unit: upstream.unit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
_ensureChainIsValid() {
|
||||||
|
if (!this._currentType || !this._currentVariant || !this._currentPosition) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error('Incomplete measurement chain, required: type, variant, and position');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getOrCreateMeasurement() {
|
||||||
|
// Initialize nested structure if needed
|
||||||
|
if (!this.measurements[this._currentType]) {
|
||||||
|
this.measurements[this._currentType] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.measurements[this._currentType][this._currentVariant]) {
|
||||||
|
this.measurements[this._currentType][this._currentVariant] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.measurements[this._currentType][this._currentVariant][this._currentPosition]) {
|
||||||
|
this.measurements[this._currentType][this._currentVariant][this._currentPosition] =
|
||||||
|
new MeasurementBuilder()
|
||||||
|
.setType(this._currentType)
|
||||||
|
.setVariant(this._currentVariant)
|
||||||
|
.setPosition(this._currentPosition)
|
||||||
|
.setWindowSize(this.windowSize)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.measurements[this._currentType][this._currentVariant][this._currentPosition];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional utility methods
|
||||||
|
getTypes() {
|
||||||
|
return Object.keys(this.measurements);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariants() {
|
||||||
|
if (!this._currentType) {
|
||||||
|
throw new Error('Type must be specified before listing variants');
|
||||||
|
}
|
||||||
|
return this.measurements[this._currentType] ?
|
||||||
|
Object.keys(this.measurements[this._currentType]) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getPositions() {
|
||||||
|
if (!this._currentType || !this._currentVariant) {
|
||||||
|
throw new Error('Type and variant must be specified before listing positions');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.measurements[this._currentType] ||
|
||||||
|
!this.measurements[this._currentType][this._currentVariant]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(this.measurements[this._currentType][this._currentVariant]);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.measurements = {};
|
||||||
|
this._currentType = null;
|
||||||
|
this._currentVariant = null;
|
||||||
|
this._currentPosition = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MeasurementContainer;
|
||||||
89
helper/measurements/README.md
Normal file
89
helper/measurements/README.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Measurement System Documentation
|
||||||
|
|
||||||
|
This system provides a flexible way to store, retrieve, and analyze measurement data using a chainable API.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { MeasurementContainer } = require('./index');
|
||||||
|
const container = new MeasurementContainer({ windowSize: 20 });
|
||||||
|
|
||||||
|
// Set values
|
||||||
|
container.type('pressure').variant('measured').position('upstream').value(100).unit('psi');
|
||||||
|
|
||||||
|
// Get values
|
||||||
|
const upstreamPressure = container.type('pressure').variant('measured').position('upstream').getCurrentValue();
|
||||||
|
console.log(`Upstream pressure: ${upstreamPressure}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chainable API Methods
|
||||||
|
|
||||||
|
### Setting Context
|
||||||
|
- `type(typeName)` - Set the measurement type (pressure, flow, etc.)
|
||||||
|
- `variant(variantName)` - Set the variant (measured, predicted, etc.)
|
||||||
|
- `position(positionName)` - Set the position (upstream, downstream, etc.)
|
||||||
|
|
||||||
|
### Setting Data
|
||||||
|
- `value(val, [timestamp])` - Add a value with optional timestamp
|
||||||
|
- `unit(unitName)` - Set the measurement unit
|
||||||
|
|
||||||
|
### Getting Data
|
||||||
|
- `get()` - Get the measurement object
|
||||||
|
- `getCurrentValue()` - Get the most recent value
|
||||||
|
- `getAverage()` - Calculate average of all values
|
||||||
|
- `getMin()` - Get minimum value
|
||||||
|
- `getMax()` - Get maximum value
|
||||||
|
|
||||||
|
### Calculations
|
||||||
|
- `difference()` - Calculate difference between upstream and downstream positions
|
||||||
|
|
||||||
|
### Listing Available Data
|
||||||
|
- `getTypes()` - Get all measurement types
|
||||||
|
- `listVariants()` - List variants for current type
|
||||||
|
- `listPositions()` - List positions for current type and variant
|
||||||
|
|
||||||
|
## Example Workflows
|
||||||
|
|
||||||
|
### Setting and retrieving values
|
||||||
|
```javascript
|
||||||
|
// Set a measurement
|
||||||
|
container.type('flow')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream')
|
||||||
|
.value(120)
|
||||||
|
.unit('gpm');
|
||||||
|
|
||||||
|
// Retrieve the same measurement
|
||||||
|
const flow = container.type('flow')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream')
|
||||||
|
.getCurrentValue();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Calculating differences
|
||||||
|
```javascript
|
||||||
|
// Set upstream and downstream measurements
|
||||||
|
container.type('pressure').variant('measured').position('upstream').value(100).unit('psi');
|
||||||
|
container.type('pressure').variant('measured').position('downstream').value(95).unit('psi');
|
||||||
|
|
||||||
|
// Calculate the difference
|
||||||
|
const diff = container.type('pressure').variant('measured').difference();
|
||||||
|
console.log(`Pressure drop: ${diff.currentDiff} ${diff.unit}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with historical data
|
||||||
|
```javascript
|
||||||
|
// Add multiple values
|
||||||
|
container.type('temperature')
|
||||||
|
.variant('measured')
|
||||||
|
.position('outlet')
|
||||||
|
.value(72)
|
||||||
|
.value(74)
|
||||||
|
.value(73)
|
||||||
|
.unit('F');
|
||||||
|
|
||||||
|
// Get statistics
|
||||||
|
const avg = container.type('temperature').variant('measured').position('outlet').getAverage();
|
||||||
|
const min = container.type('temperature').variant('measured').position('outlet').getMin();
|
||||||
|
const max = container.type('temperature').variant('measured').position('outlet').getMax();
|
||||||
|
```
|
||||||
58
helper/measurements/examples.js
Normal file
58
helper/measurements/examples.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const { MeasurementContainer } = require('./index');
|
||||||
|
|
||||||
|
// Create a container
|
||||||
|
const container = new MeasurementContainer({ windowSize: 20 });
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
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;
|
||||||
|
console.log(`Downstream pressure: ${downstreamValue} ${downstreamUnit}`);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
const flowAvg = container.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();
|
||||||
|
console.log(`Pressure difference: ${pressureDiff.value} ${pressureDiff.unit}`);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
// 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())}`);
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
runExamples: () => {
|
||||||
|
console.log('Examples of the measurement chainable API');
|
||||||
|
}
|
||||||
|
};
|
||||||
9
helper/measurements/index.js
Normal file
9
helper/measurements/index.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const MeasurementContainer = require('./MeasurementContainer');
|
||||||
|
const Measurement = require('./Measurement');
|
||||||
|
const MeasurementBuilder = require('./MeasurementBuilder');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
MeasurementContainer,
|
||||||
|
Measurement,
|
||||||
|
MeasurementBuilder
|
||||||
|
};
|
||||||
484
helper/menuUtils.js
Normal file
484
helper/menuUtils.js
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
export function initBasicToggles(elements) {
|
||||||
|
// Toggle visibility for log level
|
||||||
|
elements.logCheckbox.addEventListener("change", function () {
|
||||||
|
elements.rowLogLevel.style.display = this.checked ? "block" : "none";
|
||||||
|
});
|
||||||
|
elements.rowLogLevel.style.display = elements.logCheckbox.checked
|
||||||
|
? "block"
|
||||||
|
: "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the initialize toggles function within scope
|
||||||
|
export function initMeasurementToggles(elements) {
|
||||||
|
// Toggle visibility for scaling inputs
|
||||||
|
elements.scalingCheckbox.addEventListener("change", function () {
|
||||||
|
elements.rowInputMin.style.display = this.checked ? "block" : "none";
|
||||||
|
elements.rowInputMax.style.display = this.checked ? "block" : "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set initial states
|
||||||
|
elements.rowInputMin.style.display = elements.scalingCheckbox.checked
|
||||||
|
? "block"
|
||||||
|
: "none";
|
||||||
|
elements.rowInputMax.style.display = elements.scalingCheckbox.checked
|
||||||
|
? "block"
|
||||||
|
: "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initTensionToggles(elements, node) {
|
||||||
|
const currentMethod = node.interpolationMethod;
|
||||||
|
elements.rowTension.style.display =
|
||||||
|
currentMethod === "monotone_cubic_spline" ? "block" : "none";
|
||||||
|
console.log(
|
||||||
|
"Initial tension row display: ",
|
||||||
|
elements.rowTension.style.display
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.interpolationMethodInput.addEventListener("change", function () {
|
||||||
|
const selectedMethod = this.value;
|
||||||
|
console.log(`Interpolation method changed: ${selectedMethod}`);
|
||||||
|
node.interpolationMethod = selectedMethod;
|
||||||
|
|
||||||
|
// Toggle visibility for tension input
|
||||||
|
elements.rowTension.style.display =
|
||||||
|
selectedMethod === "monotone_cubic_spline" ? "block" : "none";
|
||||||
|
console.log("Tension row display: ", elements.rowTension.style.display);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Define the smoothing methods population function within scope
|
||||||
|
export function populateSmoothingMethods(configUrls, elements, node) {
|
||||||
|
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const smoothingMethods =
|
||||||
|
configData.smoothing?.smoothMethod?.rules?.values?.map(
|
||||||
|
(o) => o.value
|
||||||
|
) || [];
|
||||||
|
populateDropdown(
|
||||||
|
elements.smoothMethod,
|
||||||
|
smoothingMethods,
|
||||||
|
node,
|
||||||
|
"smooth_method"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error loading smoothing methods", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function populateInterpolationMethods(configUrls, elements, node) {
|
||||||
|
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const interpolationMethods =
|
||||||
|
configData?.interpolation?.type?.rules?.values.map((m) => m.value) ||
|
||||||
|
[];
|
||||||
|
populateDropdown(
|
||||||
|
elements.interpolationMethodInput,
|
||||||
|
interpolationMethods,
|
||||||
|
node,
|
||||||
|
"interpolationMethod"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the selected method and use it to spawn 1 more field to fill in tension
|
||||||
|
//const selectedMethod = interpolationMethods.find(m => m === node.interpolationMethod);
|
||||||
|
initTensionToggles(elements, node);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error loading interpolation methods", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function populateLogLevelOptions(logLevelSelect, configData, node) {
|
||||||
|
// debug log level
|
||||||
|
//console.log("Displaying configData => ", configData) ;
|
||||||
|
|
||||||
|
const logLevels =
|
||||||
|
configData?.general?.logging?.logLevel?.rules?.values?.map(
|
||||||
|
(l) => l.value
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
//console.log("Displaying logLevels => ", logLevels);
|
||||||
|
|
||||||
|
// Reuse your existing generic populateDropdown helper
|
||||||
|
populateDropdown(logLevelSelect, logLevels, node.logLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
//cascade dropdowns for asset type, supplier, subType, model, unit
|
||||||
|
export function fetchAndPopulateDropdowns(configUrls, elements, node) {
|
||||||
|
|
||||||
|
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const assetType = configData.asset?.type?.default;
|
||||||
|
const localSuppliersUrl = constructUrl(configUrls.local.taggcodeAPI,`${assetType}s`,"suppliers.json");
|
||||||
|
const cloudSuppliersUrl = constructCloudURL(configUrls.cloud.taggcodeAPI, "/vendor/get_vendors.php");
|
||||||
|
|
||||||
|
return fetchData(cloudSuppliersUrl, localSuppliersUrl)
|
||||||
|
.then((supplierData) => {
|
||||||
|
|
||||||
|
const suppliers = supplierData.map((supplier) => supplier.name);
|
||||||
|
|
||||||
|
// Populate suppliers dropdown and set up its change handler
|
||||||
|
return populateDropdown(
|
||||||
|
elements.supplier,
|
||||||
|
suppliers,
|
||||||
|
node,
|
||||||
|
"supplier",
|
||||||
|
function (selectedSupplier) {
|
||||||
|
if (selectedSupplier) {
|
||||||
|
populateSubTypes(configUrls, elements, node, selectedSupplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// If we have a saved supplier, trigger subTypes population
|
||||||
|
if (node.supplier) {
|
||||||
|
populateSubTypes(configUrls, elements, node, node.supplier);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error in initial dropdown population:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSpecificConfigUrl(nodeName,cloudAPI) {
|
||||||
|
|
||||||
|
const cloudConfigURL = cloudAPI + "/config/" + nodeName + ".json";
|
||||||
|
const localConfigURL = "http://localhost:1880/"+ nodeName + "/dependencies/"+ nodeName + "/" + nodeName + "Config.json";
|
||||||
|
|
||||||
|
return { cloudConfigURL, localConfigURL };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes to API
|
||||||
|
export async function apiCall(node) {
|
||||||
|
try{
|
||||||
|
// OLFIANT when a browser refreshes the tag code is lost!!! fix this later!!!!!
|
||||||
|
// FIX UUID ALSO LATER
|
||||||
|
|
||||||
|
if(node.assetTagCode !== "" || node.assetTagCode !== null){ }
|
||||||
|
// API call to register or check asset in central database
|
||||||
|
let assetregisterAPI = node.configUrls.cloud.taggcodeAPI + "/asset/create_asset.php";
|
||||||
|
|
||||||
|
const assetModelId = node.modelMetadata.id; //asset_product_model_id
|
||||||
|
const uuid = node.uuid; //asset_product_model_uuid
|
||||||
|
const assetName = node.assetType; //asset_name / type?
|
||||||
|
const description = node.name; // asset_description
|
||||||
|
const assetStatus = "actief"; //asset_status -> koppel aan enable / disable node ? or make dropdown ?
|
||||||
|
const assetProfileId = 1; //asset_profile_id these are the rules to check if the childs are valid under this node (parent / child id?)
|
||||||
|
const child_assets = ["63247"]; //child_assets tagnummer of id?
|
||||||
|
const assetProcessId = node.processId; //asset_process_id
|
||||||
|
const assetLocationId = node.locationId; //asset_location_id
|
||||||
|
const tagCode = node.assetTagCode; // if already exists in the node information use it to tell the api it exists and it will update else we will get it from the api call
|
||||||
|
//console.log(`this is my tagCode: ${tagCode}`);
|
||||||
|
|
||||||
|
// Build base URL with required parameters
|
||||||
|
let apiUrl = `?asset_product_model_id=${assetModelId}&asset_product_model_uuid=${uuid}&asset_name=${assetName}&asset_description=${description}&asset_status=${assetStatus}&asset_profile_id=${assetProfileId}&asset_location_id=${assetLocationId}&asset_process_id=${assetProcessId}&child_assets=${child_assets}`;
|
||||||
|
|
||||||
|
// Only add tagCode to URL if it exists
|
||||||
|
if (tagCode) {
|
||||||
|
apiUrl += `&asset_tag_number=${tagCode}`;
|
||||||
|
console.log('hello there');
|
||||||
|
}
|
||||||
|
|
||||||
|
assetregisterAPI += apiUrl;
|
||||||
|
console.log("API call to register asset in central database", assetregisterAPI);
|
||||||
|
|
||||||
|
const response = await fetch(assetregisterAPI, {
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the response text first
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log("Raw API response:", responseText);
|
||||||
|
|
||||||
|
// Try to parse the JSON, handling potential parsing errors
|
||||||
|
let jsonResponse;
|
||||||
|
try {
|
||||||
|
jsonResponse = JSON.parse(responseText);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error("JSON Parsing Error:", parseError);
|
||||||
|
console.error("Response that could not be parsed:", responseText);
|
||||||
|
throw new Error("Failed to parse API response");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(jsonResponse);
|
||||||
|
|
||||||
|
if(jsonResponse.success){
|
||||||
|
console.log(`${jsonResponse.message}, tag number: ${jsonResponse.asset_tag_number}, asset id: ${jsonResponse.asset_id}`);
|
||||||
|
// Save the asset tag number and id to the node
|
||||||
|
} else {
|
||||||
|
console.log("Asset not registered in central database");
|
||||||
|
}
|
||||||
|
return jsonResponse;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error saving changes to asset register API", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function fetchData(url, fallbackUrl) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
const responsData = await response.json();
|
||||||
|
//responsData
|
||||||
|
const data = responsData.data;
|
||||||
|
/* .map(item => {
|
||||||
|
const { vendor_name, ...rest } = item;
|
||||||
|
return {
|
||||||
|
name: vendor_name,
|
||||||
|
...rest
|
||||||
|
};
|
||||||
|
}); */
|
||||||
|
console.log(url);
|
||||||
|
console.log("Response Data: ", data);
|
||||||
|
return data;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`Primary URL failed: ${url}. Trying fallback URL: ${fallbackUrl}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const response = await fetch(fallbackUrl);
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
return await response.json();
|
||||||
|
} catch (fallbackErr) {
|
||||||
|
console.error("Both primary and fallback URLs failed:", fallbackErr);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchProjectData(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
const responsData = await response.json();
|
||||||
|
console.log("Response Data: ", responsData);
|
||||||
|
return responsData;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function populateDropdown(
|
||||||
|
htmlElement,
|
||||||
|
options,
|
||||||
|
node,
|
||||||
|
property,
|
||||||
|
callback
|
||||||
|
) {
|
||||||
|
generateHtml(htmlElement, options, node[property]);
|
||||||
|
|
||||||
|
htmlElement.addEventListener("change", async (e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
console.log(`Dropdown changed: ${property} = ${newValue}`);
|
||||||
|
node[property] = newValue;
|
||||||
|
|
||||||
|
RED.nodes.dirty(true);
|
||||||
|
if (callback) await callback(newValue); // Ensure async callback completion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to construct a URL from a base and path internal
|
||||||
|
function constructUrl(base, ...paths) {
|
||||||
|
|
||||||
|
// Remove trailing slash from base and leading slashes from paths
|
||||||
|
const sanitizedBase = (base || "").replace(/\/+$/, "");
|
||||||
|
const sanitizedPaths = paths.map((path) => path.replace(/^\/+|\/+$/g, ""));
|
||||||
|
|
||||||
|
// Join sanitized base and paths
|
||||||
|
const url = `${sanitizedBase}/${sanitizedPaths.join("/")}`;
|
||||||
|
console.log("Base:", sanitizedBase);
|
||||||
|
console.log("Paths:", sanitizedPaths);
|
||||||
|
console.log("Constructed URL:", url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Adjust for API Gateway
|
||||||
|
function constructCloudURL(base, ...paths) {
|
||||||
|
// Remove trailing slash from base and leading slashes from paths
|
||||||
|
const sanitizedBase = base.replace(/\/+$/, "");
|
||||||
|
const sanitizedPaths = paths.map((path) => path.replace(/^\/+|\/+$/g, ""));
|
||||||
|
// Join sanitized base and paths
|
||||||
|
const url = `${sanitizedBase}/${sanitizedPaths.join("/")}`;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateSubTypes(configUrls, elements, node, selectedSupplier) {
|
||||||
|
|
||||||
|
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const assetType = configData.asset?.type?.default;
|
||||||
|
const supplierFolder = constructUrl( configUrls.local.taggcodeAPI, `${assetType}s`, selectedSupplier );
|
||||||
|
|
||||||
|
const localSubTypesUrl = constructUrl(supplierFolder, "subtypes.json");
|
||||||
|
const cloudSubTypesUrl = constructCloudURL(configUrls.cloud.taggcodeAPI, "/product/get_subtypesFromVendor.php?vendor_name=" + selectedSupplier);
|
||||||
|
|
||||||
|
return fetchData(cloudSubTypesUrl, localSubTypesUrl)
|
||||||
|
.then((subTypeData) => {
|
||||||
|
const subTypes = subTypeData.map((subType) => subType.name);
|
||||||
|
|
||||||
|
return populateDropdown(
|
||||||
|
elements.subType,
|
||||||
|
subTypes,
|
||||||
|
node,
|
||||||
|
"subType",
|
||||||
|
function (selectedSubType) {
|
||||||
|
if (selectedSubType) {
|
||||||
|
// When subType changes, update both models and units
|
||||||
|
populateModels(
|
||||||
|
configUrls,
|
||||||
|
elements,
|
||||||
|
node,
|
||||||
|
selectedSupplier,
|
||||||
|
selectedSubType
|
||||||
|
);
|
||||||
|
populateUnitsForSubType(
|
||||||
|
configUrls,
|
||||||
|
elements,
|
||||||
|
node,
|
||||||
|
selectedSubType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// If we have a saved subType, trigger both models and units population
|
||||||
|
if (node.subType) {
|
||||||
|
populateModels(
|
||||||
|
configUrls,
|
||||||
|
elements,
|
||||||
|
node,
|
||||||
|
selectedSupplier,
|
||||||
|
node.subType
|
||||||
|
);
|
||||||
|
populateUnitsForSubType(configUrls, elements, node, node.subType);
|
||||||
|
}
|
||||||
|
//console.log("In fetch part of subtypes ");
|
||||||
|
// Store all data from selected model
|
||||||
|
/* node["modelMetadata"] = modelData.find(
|
||||||
|
(model) => model.name === node.model
|
||||||
|
);
|
||||||
|
console.log("Model Metadata: ", node["modelMetadata"]); */
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error populating subtypes:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateUnitsForSubType(configUrls, elements, node, selectedSubType) {
|
||||||
|
// Fetch the units data
|
||||||
|
fetchData(configUrls.cloud.units, configUrls.local.units)
|
||||||
|
.then((unitsData) => {
|
||||||
|
// Find the category that matches the subType name
|
||||||
|
const categoryData = unitsData.units.find(
|
||||||
|
(category) =>
|
||||||
|
category.category.toLowerCase() === selectedSubType.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (categoryData) {
|
||||||
|
// Extract just the unit values and descriptions
|
||||||
|
const units = categoryData.values.map((unit) => ({
|
||||||
|
value: unit.value,
|
||||||
|
description: unit.description,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create the options array with descriptions as labels
|
||||||
|
const options = units.map((unit) => ({
|
||||||
|
value: unit.value,
|
||||||
|
label: `${unit.value} - ${unit.description}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Populate the units dropdown
|
||||||
|
populateDropdown(
|
||||||
|
elements.unit,
|
||||||
|
options.map((opt) => opt.value),
|
||||||
|
node,
|
||||||
|
"unit"
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there's no currently selected unit but we have options, select the first one
|
||||||
|
if (!node.unit && options.length > 0) {
|
||||||
|
node.unit = options[0].value;
|
||||||
|
elements.unit.value = options[0].value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no matching category is found, provide a default % option
|
||||||
|
const defaultUnits = [{ value: "%", description: "Percentage" }];
|
||||||
|
populateDropdown(
|
||||||
|
elements.unit,
|
||||||
|
defaultUnits.map((unit) => unit.value),
|
||||||
|
node,
|
||||||
|
"unit"
|
||||||
|
);
|
||||||
|
console.warn(
|
||||||
|
`No matching unit category found for subType: ${selectedSubType}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching units:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateModels(
|
||||||
|
configUrls,
|
||||||
|
elements,
|
||||||
|
node,
|
||||||
|
selectedSupplier,
|
||||||
|
selectedSubType
|
||||||
|
) {
|
||||||
|
|
||||||
|
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const assetType = configData.asset?.type?.default;
|
||||||
|
// save assetType to fetch later
|
||||||
|
node.assetType = assetType;
|
||||||
|
|
||||||
|
const supplierFolder = constructUrl( configUrls.local.taggcodeAPI,`${assetType}s`,selectedSupplier);
|
||||||
|
const subTypeFolder = constructUrl(supplierFolder, selectedSubType);
|
||||||
|
const localModelsUrl = constructUrl(subTypeFolder, "models.json");
|
||||||
|
const cloudModelsUrl = constructCloudURL(configUrls.cloud.taggcodeAPI, "/product/get_product_models.php?vendor_name=" + selectedSupplier + "&product_subtype_name=" + selectedSubType);
|
||||||
|
|
||||||
|
return fetchData(cloudModelsUrl, localModelsUrl).then((modelData) => {
|
||||||
|
const models = modelData.map((model) => model.name); // use this to populate the dropdown
|
||||||
|
|
||||||
|
// If a model is already selected, store its metadata immediately
|
||||||
|
if (node.model) {
|
||||||
|
node["modelMetadata"] = modelData.find((model) => model.name === node.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
populateDropdown(elements.model, models, node, "model", (selectedModel) => {
|
||||||
|
// Store only the metadata for the selected model
|
||||||
|
node["modelMetadata"] = modelData.find((model) => model.name === selectedModel);
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
console.log('hello here I am:');
|
||||||
|
console.log(node["modelMetadata"]);
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error populating models:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateHtml(htmlElement, options, savedValue) {
|
||||||
|
htmlElement.innerHTML = options.length
|
||||||
|
? `<option value="">Select...</option>${options
|
||||||
|
.map((opt) => `<option value="${opt}">${opt}</option>`)
|
||||||
|
.join("")}`
|
||||||
|
: "<option value=''>No options available</option>";
|
||||||
|
|
||||||
|
if (savedValue && options.includes(savedValue)) {
|
||||||
|
htmlElement.value = savedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
297
helper/nrmse/errorMetric.test.js
Normal file
297
helper/nrmse/errorMetric.test.js
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
const ErrorMetrics = require('./errorMetrics');
|
||||||
|
|
||||||
|
// Dummy logger for tests
|
||||||
|
const logger = {
|
||||||
|
error: console.error,
|
||||||
|
debug: console.log,
|
||||||
|
info: console.log
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
thresholds: {
|
||||||
|
NRMSE_LOW: 0.05,
|
||||||
|
NRMSE_MEDIUM: 0.10,
|
||||||
|
NRMSE_HIGH: 0.15,
|
||||||
|
LONG_TERM_LOW: 0.02,
|
||||||
|
LONG_TERM_MEDIUM: 0.04,
|
||||||
|
LONG_TERM_HIGH: 0.06
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ErrorMetricsTester {
|
||||||
|
constructor() {
|
||||||
|
this.totalTests = 0;
|
||||||
|
this.passedTests = 0;
|
||||||
|
this.failedTests = 0;
|
||||||
|
this.errorMetrics = new ErrorMetrics(config, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(condition, message) {
|
||||||
|
this.totalTests++;
|
||||||
|
if (condition) {
|
||||||
|
console.log(`✓ PASS: ${message}`);
|
||||||
|
this.passedTests++;
|
||||||
|
} else {
|
||||||
|
console.log(`✗ FAIL: ${message}`);
|
||||||
|
this.failedTests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testMeanSquaredError() {
|
||||||
|
console.log("\nTesting Mean Squared Error...");
|
||||||
|
const predicted = [1, 2, 3];
|
||||||
|
const measured = [1, 3, 5];
|
||||||
|
const mse = this.errorMetrics.meanSquaredError(predicted, measured);
|
||||||
|
this.assert(Math.abs(mse - 1.67) < 0.1, "MSE correctly calculated");
|
||||||
|
}
|
||||||
|
|
||||||
|
testRootMeanSquaredError() {
|
||||||
|
console.log("\nTesting Root Mean Squared Error...");
|
||||||
|
const predicted = [1, 2, 3];
|
||||||
|
const measured = [1, 3, 5];
|
||||||
|
const rmse = this.errorMetrics.rootMeanSquaredError(predicted, measured);
|
||||||
|
this.assert(Math.abs(rmse - 1.29) < 0.1, "RMSE correctly calculated");
|
||||||
|
}
|
||||||
|
|
||||||
|
testNormalizedRMSE() {
|
||||||
|
console.log("\nTesting Normalized RMSE...");
|
||||||
|
const predicted = [100, 102, 104];
|
||||||
|
const measured = [98, 103, 107];
|
||||||
|
const processMin = 90, processMax = 110;
|
||||||
|
const nrmse = this.errorMetrics.normalizedRootMeanSquaredError(predicted, measured, processMin, processMax);
|
||||||
|
this.assert(typeof nrmse === 'number' && nrmse > 0, "Normalized RMSE calculated correctly");
|
||||||
|
}
|
||||||
|
|
||||||
|
testNormalizeUsingRealtime() {
|
||||||
|
console.log("\nTesting Normalize Using Realtime...");
|
||||||
|
const predicted = [100, 102, 104];
|
||||||
|
const measured = [98, 103, 107];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nrmse = this.errorMetrics.normalizeUsingRealtime(predicted, measured);
|
||||||
|
this.assert(typeof nrmse === 'number' && nrmse > 0, "Normalize using realtime calculated correctly");
|
||||||
|
} catch (error) {
|
||||||
|
this.assert(false, `Normalize using realtime failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with identical values to check error handling
|
||||||
|
const sameValues = [100, 100, 100];
|
||||||
|
try {
|
||||||
|
this.errorMetrics.normalizeUsingRealtime(sameValues, sameValues);
|
||||||
|
this.assert(false, "Should throw error with identical values");
|
||||||
|
} catch (error) {
|
||||||
|
this.assert(true, "Correctly throws error when min/max are the same");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testLongTermNRMSD() {
|
||||||
|
console.log("\nTesting Long Term NRMSD Accumulation...");
|
||||||
|
// Reset the accumulation values
|
||||||
|
this.errorMetrics.cumNRMSD = 0;
|
||||||
|
this.errorMetrics.cumCount = 0;
|
||||||
|
|
||||||
|
let lastValue = 0;
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
lastValue = this.errorMetrics.longTermNRMSD(0.1 + i * 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assert(
|
||||||
|
this.errorMetrics.cumCount === 100 &&
|
||||||
|
this.errorMetrics.cumNRMSD !== 0 &&
|
||||||
|
lastValue !== 0,
|
||||||
|
"Long term NRMSD accumulates over 100 iterations"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test that values are returned only after accumulating 100 samples
|
||||||
|
this.errorMetrics.cumNRMSD = 0;
|
||||||
|
this.errorMetrics.cumCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < 99; i++) {
|
||||||
|
const result = this.errorMetrics.longTermNRMSD(0.1);
|
||||||
|
this.assert(result === 0, "No longTermNRMSD returned before 100 samples");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a different value for the 100th sample to ensure a non-zero result
|
||||||
|
const result = this.errorMetrics.longTermNRMSD(0.2);
|
||||||
|
this.assert(result !== 0, "longTermNRMSD returned after 100 samples");
|
||||||
|
}
|
||||||
|
|
||||||
|
testDetectImmediateDrift() {
|
||||||
|
console.log("\nTesting Immediate Drift Detection...");
|
||||||
|
|
||||||
|
// Test high drift
|
||||||
|
let drift = this.errorMetrics.detectImmediateDrift(config.thresholds.NRMSE_HIGH + 0.01);
|
||||||
|
this.assert(drift.level === 3, "Detects high immediate drift correctly");
|
||||||
|
|
||||||
|
// Test medium drift
|
||||||
|
drift = this.errorMetrics.detectImmediateDrift(config.thresholds.NRMSE_MEDIUM + 0.01);
|
||||||
|
this.assert(drift.level === 2, "Detects medium immediate drift correctly");
|
||||||
|
|
||||||
|
// Test low drift
|
||||||
|
drift = this.errorMetrics.detectImmediateDrift(config.thresholds.NRMSE_LOW + 0.01);
|
||||||
|
this.assert(drift.level === 1, "Detects low immediate drift correctly");
|
||||||
|
|
||||||
|
// Test no drift
|
||||||
|
drift = this.errorMetrics.detectImmediateDrift(config.thresholds.NRMSE_LOW - 0.01);
|
||||||
|
this.assert(drift.level === 0, "Detects no immediate drift correctly");
|
||||||
|
}
|
||||||
|
|
||||||
|
testDetectLongTermDrift() {
|
||||||
|
console.log("\nTesting Long Term Drift Detection...");
|
||||||
|
|
||||||
|
// Test high drift
|
||||||
|
let drift = this.errorMetrics.detectLongTermDrift(config.thresholds.LONG_TERM_HIGH + 0.01);
|
||||||
|
this.assert(drift.level === 3, "Detects high long-term drift correctly");
|
||||||
|
|
||||||
|
// Test medium drift
|
||||||
|
drift = this.errorMetrics.detectLongTermDrift(config.thresholds.LONG_TERM_MEDIUM + 0.01);
|
||||||
|
this.assert(drift.level === 2, "Detects medium long-term drift correctly");
|
||||||
|
|
||||||
|
// Test low drift
|
||||||
|
drift = this.errorMetrics.detectLongTermDrift(config.thresholds.LONG_TERM_LOW + 0.01);
|
||||||
|
this.assert(drift.level === 1, "Detects low long-term drift correctly");
|
||||||
|
|
||||||
|
// Test no drift
|
||||||
|
drift = this.errorMetrics.detectLongTermDrift(config.thresholds.LONG_TERM_LOW - 0.01);
|
||||||
|
this.assert(drift.level === 0, "Detects no long-term drift correctly");
|
||||||
|
|
||||||
|
// Test negative drift values
|
||||||
|
drift = this.errorMetrics.detectLongTermDrift(-config.thresholds.LONG_TERM_HIGH - 0.01);
|
||||||
|
this.assert(drift.level === 3, "Detects negative high long-term drift correctly");
|
||||||
|
}
|
||||||
|
|
||||||
|
testDriftDetection() {
|
||||||
|
console.log("\nTesting Combined Drift Detection...");
|
||||||
|
|
||||||
|
let nrmseHigh = config.thresholds.NRMSE_HIGH + 0.01;
|
||||||
|
let ltNRMSD = 0;
|
||||||
|
|
||||||
|
let result = this.errorMetrics.detectDrift(nrmseHigh, ltNRMSD);
|
||||||
|
|
||||||
|
this.assert(
|
||||||
|
result !== null &&
|
||||||
|
result.ImmDrift &&
|
||||||
|
result.ImmDrift.level === 3 &&
|
||||||
|
result.LongTermDrift.level === 0,
|
||||||
|
"Detects high immediate drift with no long-term drift"
|
||||||
|
);
|
||||||
|
|
||||||
|
nrmseHigh = config.thresholds.NRMSE_LOW - 0.01;
|
||||||
|
ltNRMSD = config.thresholds.LONG_TERM_MEDIUM + 0.01;
|
||||||
|
result = this.errorMetrics.detectDrift(nrmseHigh, ltNRMSD);
|
||||||
|
this.assert(
|
||||||
|
result !== null &&
|
||||||
|
result.ImmDrift.level === 0 &&
|
||||||
|
result.LongTermDrift &&
|
||||||
|
result.LongTermDrift.level === 2,
|
||||||
|
"Detects medium long-term drift with no immediate drift"
|
||||||
|
);
|
||||||
|
|
||||||
|
nrmseHigh = config.thresholds.NRMSE_MEDIUM + 0.01;
|
||||||
|
ltNRMSD = config.thresholds.LONG_TERM_MEDIUM + 0.01;
|
||||||
|
result = this.errorMetrics.detectDrift(nrmseHigh, ltNRMSD);
|
||||||
|
this.assert(
|
||||||
|
result.ImmDrift.level === 2 &&
|
||||||
|
result.LongTermDrift.level === 2,
|
||||||
|
"Detects both medium immediate and medium long-term drift"
|
||||||
|
);
|
||||||
|
|
||||||
|
nrmseHigh = config.thresholds.NRMSE_LOW - 0.01;
|
||||||
|
ltNRMSD = config.thresholds.LONG_TERM_LOW - 0.01;
|
||||||
|
result = this.errorMetrics.detectDrift(nrmseHigh, ltNRMSD);
|
||||||
|
this.assert(
|
||||||
|
result.ImmDrift.level === 0 &&
|
||||||
|
result.LongTermDrift.level === 0,
|
||||||
|
"No significant drift detected when under thresholds"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testAssessDrift() {
|
||||||
|
console.log("\nTesting assessDrift function...");
|
||||||
|
|
||||||
|
// Reset accumulation for testing
|
||||||
|
this.errorMetrics.cumNRMSD = 0;
|
||||||
|
this.errorMetrics.cumCount = 0;
|
||||||
|
|
||||||
|
const predicted = [100, 101, 102, 103];
|
||||||
|
const measured = [90, 91, 92, 93];
|
||||||
|
const processMin = 90, processMax = 110;
|
||||||
|
|
||||||
|
let result = this.errorMetrics.assessDrift(predicted, measured, processMin, processMax);
|
||||||
|
|
||||||
|
this.assert(
|
||||||
|
result !== null &&
|
||||||
|
typeof result.nrmse === 'number' &&
|
||||||
|
typeof result.longTermNRMSD === 'number' &&
|
||||||
|
typeof result.immediateLevel === 'number' &&
|
||||||
|
typeof result.immediateFeedback === 'string' &&
|
||||||
|
typeof result.longTermLevel === 'number' &&
|
||||||
|
typeof result.longTermFeedback === 'string',
|
||||||
|
"assessDrift returns complete result structure"
|
||||||
|
);
|
||||||
|
|
||||||
|
this.assert(
|
||||||
|
result.immediateLevel > 0,
|
||||||
|
"assessDrift detects immediate drift with significant difference"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test with identical values
|
||||||
|
result = this.errorMetrics.assessDrift(predicted, predicted, processMin, processMax);
|
||||||
|
this.assert(
|
||||||
|
result.nrmse === 0 &&
|
||||||
|
result.immediateLevel === 0,
|
||||||
|
"assessDrift indicates no immediate drift when predicted equals measured"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test with slight drift
|
||||||
|
const measuredSlight = [100, 100.5, 101, 101.5];
|
||||||
|
result = this.errorMetrics.assessDrift(predicted, measuredSlight, processMin, processMax);
|
||||||
|
|
||||||
|
this.assert(
|
||||||
|
result !== null &&
|
||||||
|
result.nrmse < 0.05 &&
|
||||||
|
(result.immediateLevel < 2),
|
||||||
|
"assessDrift returns appropriate levels for slight drift"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test long-term drift accumulation
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
this.errorMetrics.assessDrift(
|
||||||
|
predicted,
|
||||||
|
measured.map(m => m + (Math.random() * 2 - 1)), // Add small random fluctuation
|
||||||
|
processMin,
|
||||||
|
processMax
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = this.errorMetrics.assessDrift(predicted, measured, processMin, processMax);
|
||||||
|
this.assert(
|
||||||
|
result.longTermNRMSD !== 0,
|
||||||
|
"Long-term drift accumulates over multiple assessments"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAllTests() {
|
||||||
|
console.log("\nStarting Error Metrics Tests...\n");
|
||||||
|
this.testMeanSquaredError();
|
||||||
|
this.testRootMeanSquaredError();
|
||||||
|
this.testNormalizedRMSE();
|
||||||
|
this.testNormalizeUsingRealtime();
|
||||||
|
this.testLongTermNRMSD();
|
||||||
|
this.testDetectImmediateDrift();
|
||||||
|
this.testDetectLongTermDrift();
|
||||||
|
this.testDriftDetection();
|
||||||
|
this.testAssessDrift();
|
||||||
|
|
||||||
|
console.log("\nTest Summary:");
|
||||||
|
console.log(`Total Tests: ${this.totalTests}`);
|
||||||
|
console.log(`Passed: ${this.passedTests}`);
|
||||||
|
console.log(`Failed: ${this.failedTests}`);
|
||||||
|
|
||||||
|
process.exit(this.failedTests > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all tests
|
||||||
|
const tester = new ErrorMetricsTester();
|
||||||
|
tester.runAllTests().catch(console.error);
|
||||||
154
helper/nrmse/errorMetrics.js
Normal file
154
helper/nrmse/errorMetrics.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
//load local dependencies
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
//load all config modules
|
||||||
|
const defaultConfig = require('./nrmseConfig.json');
|
||||||
|
const ConfigUtils = require('../configUtils');
|
||||||
|
|
||||||
|
class ErrorMetrics {
|
||||||
|
constructor(config = {}, logger) {
|
||||||
|
|
||||||
|
this.emitter = new EventEmitter(); // Own EventEmitter
|
||||||
|
this.configUtils = new ConfigUtils(defaultConfig);
|
||||||
|
this.config = this.configUtils.initConfig(config);
|
||||||
|
|
||||||
|
// Init after config is set
|
||||||
|
this.logger = logger;
|
||||||
|
|
||||||
|
// For long-term NRMSD accumulation
|
||||||
|
this.cumNRMSD = 0;
|
||||||
|
this.cumCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//INCLUDE timestamps in the next update OLIFANT
|
||||||
|
meanSquaredError(predicted, measured) {
|
||||||
|
if (predicted.length !== measured.length) {
|
||||||
|
this.logger.error("Comparing MSE Arrays must have the same length.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let sumSqError = 0;
|
||||||
|
for (let i = 0; i < predicted.length; i++) {
|
||||||
|
const err = predicted[i] - measured[i];
|
||||||
|
sumSqError += err * err;
|
||||||
|
}
|
||||||
|
return sumSqError / predicted.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
rootMeanSquaredError(predicted, measured) {
|
||||||
|
return Math.sqrt(this.meanSquaredError(predicted, measured));
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedRootMeanSquaredError(predicted, measured, processMin, processMax) {
|
||||||
|
const range = processMax - processMin;
|
||||||
|
if (range <= 0) {
|
||||||
|
this.logger.error("Invalid process range: processMax must be greater than processMin.");
|
||||||
|
}
|
||||||
|
const rmse = this.rootMeanSquaredError(predicted, measured);
|
||||||
|
return rmse / range;
|
||||||
|
}
|
||||||
|
|
||||||
|
longTermNRMSD(input) {
|
||||||
|
|
||||||
|
const storedNRMSD = this.cumNRMSD;
|
||||||
|
const storedCount = this.cumCount;
|
||||||
|
const newCount = storedCount + 1;
|
||||||
|
|
||||||
|
// Update cumulative values
|
||||||
|
this.cumCount = newCount;
|
||||||
|
|
||||||
|
// Calculate new running average
|
||||||
|
if (storedCount === 0) {
|
||||||
|
this.cumNRMSD = input; // First value
|
||||||
|
} else {
|
||||||
|
// Running average formula: newAvg = oldAvg + (newValue - oldAvg) / newCount
|
||||||
|
this.cumNRMSD = storedNRMSD + (input - storedNRMSD) / newCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(newCount >= 100) {
|
||||||
|
// Return the current NRMSD value, not just the contribution from this sample
|
||||||
|
return this.cumNRMSD;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeUsingRealtime(predicted, measured) {
|
||||||
|
const realtimeMin = Math.min(Math.min(...predicted), Math.min(...measured));
|
||||||
|
const realtimeMax = Math.max(Math.max(...predicted), Math.max(...measured));
|
||||||
|
const range = realtimeMax - realtimeMin;
|
||||||
|
if (range <= 0) {
|
||||||
|
throw new Error("Invalid process range: processMax must be greater than processMin.");
|
||||||
|
}
|
||||||
|
const rmse = this.rootMeanSquaredError(predicted, measured);
|
||||||
|
return rmse / range;
|
||||||
|
}
|
||||||
|
|
||||||
|
detectImmediateDrift(nrmse) {
|
||||||
|
let ImmDrift = {};
|
||||||
|
this.logger.debug(`checking immediate drift with thresholds : ${this.config.thresholds.NRMSE_HIGH} ${this.config.thresholds.NRMSE_MEDIUM} ${this.config.thresholds.NRMSE_LOW}`);
|
||||||
|
switch (true) {
|
||||||
|
case( nrmse > this.config.thresholds.NRMSE_HIGH ) :
|
||||||
|
ImmDrift = {level : 3 , feedback : "High immediate drift detected"};
|
||||||
|
break;
|
||||||
|
case( nrmse > this.config.thresholds.NRMSE_MEDIUM ) :
|
||||||
|
ImmDrift = {level : 2 , feedback : "Medium immediate drift detected"};
|
||||||
|
break;
|
||||||
|
case(nrmse > this.config.thresholds.NRMSE_LOW ):
|
||||||
|
ImmDrift = {level : 1 , feedback : "Low immediate drift detected"};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ImmDrift = {level : 0 , feedback : "No drift detected"};
|
||||||
|
}
|
||||||
|
return ImmDrift;
|
||||||
|
}
|
||||||
|
|
||||||
|
detectLongTermDrift(longTermNRMSD) {
|
||||||
|
let LongTermDrift = {};
|
||||||
|
this.logger.debug(`checking longterm drift with thresholds : ${this.config.thresholds.LONG_TERM_HIGH} ${this.config.thresholds.LONG_TERM_MEDIUM} ${this.config.thresholds.LONG_TERM_LOW}`);
|
||||||
|
switch (true) {
|
||||||
|
case(Math.abs(longTermNRMSD) > this.config.thresholds.LONG_TERM_HIGH) :
|
||||||
|
LongTermDrift = {level : 3 , feedback : "High long-term drift detected"};
|
||||||
|
break;
|
||||||
|
case (Math.abs(longTermNRMSD) > this.config.thresholds.LONG_TERM_MEDIUM) :
|
||||||
|
LongTermDrift = {level : 2 , feedback : "Medium long-term drift detected"};
|
||||||
|
break;
|
||||||
|
case ( Math.abs(longTermNRMSD) > this.config.thresholds.LONG_TERM_LOW ) :
|
||||||
|
LongTermDrift = {level : 1 , feedback : "Low long-term drift detected"};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LongTermDrift = {level : 0 , feedback : "No drift detected"};
|
||||||
|
}
|
||||||
|
return LongTermDrift;
|
||||||
|
}
|
||||||
|
|
||||||
|
detectDrift(nrmse, longTermNRMSD) {
|
||||||
|
const ImmDrift = this.detectImmediateDrift(nrmse);
|
||||||
|
const LongTermDrift = this.detectLongTermDrift(longTermNRMSD);
|
||||||
|
return { ImmDrift, LongTermDrift };
|
||||||
|
}
|
||||||
|
|
||||||
|
// asses the drift
|
||||||
|
assessDrift(predicted, measured, processMin, processMax) {
|
||||||
|
// Compute NRMSE and check for immediate drift
|
||||||
|
const nrmse = this.normalizedRootMeanSquaredError(predicted, measured, processMin, processMax);
|
||||||
|
this.logger.debug(`NRMSE: ${nrmse}`);
|
||||||
|
// cmopute long-term NRMSD and add result to cumalitve NRMSD
|
||||||
|
const longTermNRMSD = this.longTermNRMSD(nrmse);
|
||||||
|
// return the drift
|
||||||
|
// Return the drift assessment object
|
||||||
|
const driftAssessment = this.detectDrift(nrmse, longTermNRMSD);
|
||||||
|
return {
|
||||||
|
nrmse,
|
||||||
|
longTermNRMSD,
|
||||||
|
immediateLevel: driftAssessment.ImmDrift.level,
|
||||||
|
immediateFeedback: driftAssessment.ImmDrift.feedback,
|
||||||
|
longTermLevel: driftAssessment.LongTermDrift.level,
|
||||||
|
longTermFeedback: driftAssessment.LongTermDrift.feedback
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ErrorMetrics;
|
||||||
138
helper/nrmse/nrmseConfig.json
Normal file
138
helper/nrmse/nrmseConfig.json
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "ErrorMetrics",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name for the configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration, assigned dynamically when needed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit used for the state values (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"logLevel": {
|
||||||
|
"default": "info",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "debug",
|
||||||
|
"description": "Log messages are printed for debugging purposes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "info",
|
||||||
|
"description": "Informational messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warn",
|
||||||
|
"description": "Warning messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "error",
|
||||||
|
"description": "Error messages are printed."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether logging is active. If true, log messages will be generated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"functionality": {
|
||||||
|
"softwareType": {
|
||||||
|
"default": "errorMetrics",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Logical name identifying the software type."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "error calculation",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Functional role within the system."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"current": {
|
||||||
|
"default": "active",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "active",
|
||||||
|
"description": "The error metrics calculation is active."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "inactive",
|
||||||
|
"description": "The error metrics calculation is inactive."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The operational mode of the error metrics calculation."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"NRMSE_LOW": {
|
||||||
|
"default": 0.05,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Low threshold for normalized root mean squared error."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"NRMSE_MEDIUM": {
|
||||||
|
"default": 0.10,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Medium threshold for normalized root mean squared error."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"NRMSE_HIGH": {
|
||||||
|
"default": 0.15,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "High threshold for normalized root mean squared error."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LONG_TERM_LOW": {
|
||||||
|
"default": 0.02,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Low threshold for long-term normalized root mean squared deviation."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LONG_TERM_MEDIUM": {
|
||||||
|
"default": 0.04,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Medium threshold for long-term normalized root mean squared deviation."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LONG_TERM_HIGH": {
|
||||||
|
"default": 0.06,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "High threshold for long-term normalized root mean squared deviation."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
helper/outliers/outlierDetection.js
Normal file
89
helper/outliers/outlierDetection.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
class DynamicClusterDeviation {
|
||||||
|
constructor() {
|
||||||
|
this.clusters = []; // Stores clusters as { center, spread, count }
|
||||||
|
}
|
||||||
|
|
||||||
|
update(value) {
|
||||||
|
console.log(`\nProcessing value: ${value}`);
|
||||||
|
|
||||||
|
// If no clusters exist, create the first one
|
||||||
|
if (this.clusters.length === 0) {
|
||||||
|
this.clusters.push({ center: value, spread: 0, count: 1 });
|
||||||
|
console.log(` → First cluster created at ${value}`);
|
||||||
|
return { value, isOutlier: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Find the closest cluster
|
||||||
|
let bestMatch = null;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
for (const cluster of this.clusters) {
|
||||||
|
const distance = Math.abs(value - cluster.center);
|
||||||
|
console.log(` Checking against cluster at ${cluster.center} (spread: ${cluster.spread}, count: ${cluster.count}) → distance: ${distance}`);
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
bestMatch = cluster;
|
||||||
|
minDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Closest cluster found at ${bestMatch.center} with distance: ${minDistance}`);
|
||||||
|
|
||||||
|
// Step 2: Compute dynamic threshold
|
||||||
|
const dynamicThreshold = 1 + 5 / Math.sqrt(bestMatch.count + 1);
|
||||||
|
const allowedDeviation = dynamicThreshold * (bestMatch.spread || 1);
|
||||||
|
|
||||||
|
console.log(` Dynamic threshold: ${dynamicThreshold.toFixed(2)}, Allowed deviation: ${allowedDeviation.toFixed(2)}`);
|
||||||
|
|
||||||
|
// Step 3: Check if value fits within the dynamically adjusted cluster spread
|
||||||
|
if (minDistance <= allowedDeviation) {
|
||||||
|
// Update cluster dynamically
|
||||||
|
const newCenter = (bestMatch.center * bestMatch.count + value) / (bestMatch.count + 1);
|
||||||
|
const newSpread = Math.max(bestMatch.spread, minDistance);
|
||||||
|
bestMatch.center = newCenter;
|
||||||
|
bestMatch.spread = newSpread;
|
||||||
|
bestMatch.count += 1;
|
||||||
|
|
||||||
|
console.log(` ✅ Value fits in cluster! Updating cluster:`);
|
||||||
|
console.log(` → New center: ${newCenter.toFixed(2)}`);
|
||||||
|
console.log(` → New spread: ${newSpread.toFixed(2)}`);
|
||||||
|
console.log(` → New count: ${bestMatch.count}`);
|
||||||
|
|
||||||
|
return { value, isOutlier: false };
|
||||||
|
} else {
|
||||||
|
// If too far, create a new cluster
|
||||||
|
this.clusters.push({ center: value, spread: 0, count: 1 });
|
||||||
|
|
||||||
|
console.log(` ❌ Outlier detected! New cluster created at ${value}`);
|
||||||
|
|
||||||
|
return { value, isOutlier: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rolling window simulation with outlier detection
|
||||||
|
/*
|
||||||
|
const detector = new DynamicClusterDeviation();
|
||||||
|
const dataStream = [10, 10.2, 10.5, 9.8, 11, 50, 10.3, 200, 201, 200.1, 205, 202, 250, 260, 270, 280, 290, 300];
|
||||||
|
|
||||||
|
// Define the number of elements per rolling window chunk.
|
||||||
|
const windowSize = 5;
|
||||||
|
let rollingWindow = [];
|
||||||
|
|
||||||
|
dataStream.forEach((value, index) => {
|
||||||
|
console.log(`\n=== Processing value ${index + 1} ===`);
|
||||||
|
rollingWindow.push(value);
|
||||||
|
const result = detector.update(value);
|
||||||
|
console.log(`Current rolling window: [${rollingWindow.join(', ')}]`);
|
||||||
|
console.log(`Result: value=${result.value} (${result.isOutlier ? 'Outlier' : 'Inlier'})`);
|
||||||
|
|
||||||
|
// Once the window size is reached, show current cluster states and reset the window for the next chunk.
|
||||||
|
if (rollingWindow.length === windowSize) {
|
||||||
|
console.log("\n--- Rolling window chunk finished ---");
|
||||||
|
console.log("Detector cluster states:", JSON.stringify(detector.clusters, null, 2));
|
||||||
|
rollingWindow = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\nFinal detector cluster states:", JSON.stringify(detector.clusters, null, 2));
|
||||||
|
*/
|
||||||
132
helper/outputUtils.js
Normal file
132
helper/outputUtils.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
//this class will handle the output events for the node red node
|
||||||
|
class OutputUtils {
|
||||||
|
constructor() {
|
||||||
|
this.output ={};
|
||||||
|
this.output['influxdb'] = {};
|
||||||
|
this.output['process'] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForChanges(output, format) {
|
||||||
|
const changedFields = {};
|
||||||
|
for (const key in output) {
|
||||||
|
if (output.hasOwnProperty(key) && output[key] !== this.output[format][key]) {
|
||||||
|
let value = output[key];
|
||||||
|
// For fields: if the value is an object (and not a Date), stringify it.
|
||||||
|
if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
|
||||||
|
changedFields[key] = JSON.stringify(value);
|
||||||
|
} else {
|
||||||
|
changedFields[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the saved output state.
|
||||||
|
this.output[format] = { ...this.output[format], ...changedFields };
|
||||||
|
|
||||||
|
return changedFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatMsg(output, config, format) {
|
||||||
|
|
||||||
|
//define emtpy message
|
||||||
|
let msg = {};
|
||||||
|
|
||||||
|
// Compare output with last output and only include changed values
|
||||||
|
const changedFields = this.checkForChanges(output,format);
|
||||||
|
|
||||||
|
if (Object.keys(changedFields).length > 0) {
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'influxdb':
|
||||||
|
// Extract the relevant config properties.
|
||||||
|
const relevantConfig = this.extractRelevantConfig(config);
|
||||||
|
// Flatten the tags so that no nested objects are passed on.
|
||||||
|
const flatTags = this.flattenTags(relevantConfig);
|
||||||
|
msg = this.influxDBFormat(changedFields, config, flatTags);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'process':
|
||||||
|
|
||||||
|
// Compare output with last output and only include changed values
|
||||||
|
msg = this.processFormat(changedFields,config);
|
||||||
|
//console.log(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('Unknown format in output utils');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
influxDBFormat(changedFields, config , flatTags) {
|
||||||
|
// Create the measurement and topic using softwareType and name config.functionality.softwareType + .
|
||||||
|
const measurement = config.general.name;
|
||||||
|
const payload = {
|
||||||
|
measurement: measurement,
|
||||||
|
fields: changedFields,
|
||||||
|
tags: flatTags,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const topic = measurement;
|
||||||
|
const msg = { topic: topic, payload: payload };
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
flattenTags(obj) {
|
||||||
|
const result = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
const value = obj[key];
|
||||||
|
if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
|
||||||
|
// Recursively flatten the nested object.
|
||||||
|
const flatChild = this.flattenTags(value);
|
||||||
|
for (const childKey in flatChild) {
|
||||||
|
if (flatChild.hasOwnProperty(childKey)) {
|
||||||
|
result[`${key}_${childKey}`] = String(flatChild[childKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// InfluxDB tags must be strings.
|
||||||
|
result[key] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractRelevantConfig(config) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
// general properties
|
||||||
|
id: config.general?.id,
|
||||||
|
name: config.general?.name,
|
||||||
|
unit: config.general?.unit,
|
||||||
|
// functionality properties
|
||||||
|
softwareType: config.functionality?.softwareType,
|
||||||
|
role: config.functionality?.role,
|
||||||
|
// asset properties (exclude machineCurve)
|
||||||
|
uuid: config.asset?.uuid,
|
||||||
|
geoLocation: config.asset?.geoLocation,
|
||||||
|
supplier: config.asset?.supplier,
|
||||||
|
type: config.asset?.type,
|
||||||
|
subType: config.asset?.subType,
|
||||||
|
model: config.asset?.model,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
processFormat(changedFields,config) {
|
||||||
|
// Create the measurement and topic using softwareType and name config.functionality.softwareType + .
|
||||||
|
const measurement = config.general.name;
|
||||||
|
const payload = changedFields;
|
||||||
|
const topic = measurement;
|
||||||
|
const msg = { topic: topic, payload: payload };
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = OutputUtils;
|
||||||
277
helper/state/movementManager.js
Normal file
277
helper/state/movementManager.js
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
//const EventEmitter = require('events');
|
||||||
|
|
||||||
|
class movementManager {
|
||||||
|
constructor(config, logger, emitter) {
|
||||||
|
this.emitter = emitter; //new EventEmitter(); //state class emitter
|
||||||
|
|
||||||
|
const { min, max, initial } = config.position;
|
||||||
|
const { speed, maxSpeed, interval } = config.movement;
|
||||||
|
|
||||||
|
this.minPosition = min;
|
||||||
|
this.maxPosition = max;
|
||||||
|
this.currentPosition = initial;
|
||||||
|
|
||||||
|
this.speed = speed;
|
||||||
|
this.maxSpeed = maxSpeed;
|
||||||
|
this.interval = interval;
|
||||||
|
this.timeleft = 0; // timeleft of current movement
|
||||||
|
|
||||||
|
this.logger = logger;
|
||||||
|
this.movementMode = config.movement.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPosition() {
|
||||||
|
return this.currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveTo(targetPosition, signal) {
|
||||||
|
// Constrain target position if necessary
|
||||||
|
if (
|
||||||
|
targetPosition < this.minPosition ||
|
||||||
|
targetPosition > this.maxPosition
|
||||||
|
) {
|
||||||
|
targetPosition = this.constrain(targetPosition);
|
||||||
|
this.logger.warn(
|
||||||
|
`New target position=${targetPosition} is constrained to fit between min=${this.minPosition} and max=${this.maxPosition}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
`Starting movement to position ${targetPosition} in ${this.movementMode} with avg speed=${this.speed}%/s.`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (signal && signal.aborted) {
|
||||||
|
this.logger.debug("Movement aborted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute the movement logic based on the mode
|
||||||
|
switch (this.movementMode) {
|
||||||
|
case "staticspeed":
|
||||||
|
const movelinFeedback = await this.moveLinear(targetPosition,signal);
|
||||||
|
this.logger.info(`Linear move: ${movelinFeedback} `);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "dynspeed":
|
||||||
|
const moveDynFeedback = await this.moveEaseInOut(targetPosition,signal);
|
||||||
|
this.logger.info(`Dynamic move : ${moveDynFeedback}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported movement mode: ${this.movementMode}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveLinear(targetPosition, signal) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Immediate abort if already signalled
|
||||||
|
if (signal?.aborted) {
|
||||||
|
return reject(new Error("Movement aborted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp the final target into [minPosition, maxPosition]
|
||||||
|
targetPosition = this.constrain(targetPosition);
|
||||||
|
|
||||||
|
// Compute direction and remaining distance
|
||||||
|
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||||
|
const distance = Math.abs(targetPosition - this.currentPosition);
|
||||||
|
|
||||||
|
// Speed is a fraction [0,1] of full-range per second
|
||||||
|
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||||
|
const fullRange = this.maxPosition - this.minPosition;
|
||||||
|
const velocity = this.speed * fullRange; // units per second
|
||||||
|
if (velocity === 0) {
|
||||||
|
return reject(new Error("Movement aborted: zero speed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration and bookkeeping
|
||||||
|
const duration = distance / velocity; // seconds to go the remaining distance
|
||||||
|
this.timeleft = duration;
|
||||||
|
this.logger.debug(
|
||||||
|
`Linear move: dir=${direction}, dist=${distance}, vel=${velocity.toFixed(2)} u/s, dur=${duration.toFixed(2)}s`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compute how much to move each tick
|
||||||
|
const intervalMs = this.interval;
|
||||||
|
const intervalSec = intervalMs / 1000;
|
||||||
|
const stepSize = direction * velocity * intervalSec;
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Kick off the loop
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
// 7a) Abort check
|
||||||
|
if (signal?.aborted) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
return reject(new Error("Movement aborted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance position and clamp
|
||||||
|
this.currentPosition += stepSize;
|
||||||
|
this.currentPosition = this.constrain(this.currentPosition);
|
||||||
|
this.emitPos(this.currentPosition);
|
||||||
|
|
||||||
|
// Update timeleft
|
||||||
|
const elapsed = (Date.now() - startTime) / 1000;
|
||||||
|
this.timeleft = Math.max(0, duration - elapsed);
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`pos=${this.currentPosition.toFixed(2)}, timeleft=${this.timeleft.toFixed(2)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Completed the move?
|
||||||
|
if (
|
||||||
|
(direction > 0 && this.currentPosition >= targetPosition) ||
|
||||||
|
(direction < 0 && this.currentPosition <= targetPosition)
|
||||||
|
) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
this.currentPosition = targetPosition;
|
||||||
|
this.emitPos(this.currentPosition);
|
||||||
|
return resolve("Reached target move.");
|
||||||
|
}
|
||||||
|
}, intervalMs);
|
||||||
|
|
||||||
|
// 8) Also catch aborts that happen before the first tick
|
||||||
|
signal?.addEventListener("abort", () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
reject(new Error("Movement aborted"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
moveLinearinTime(targetPosition,signal) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Abort immediately if already signalled
|
||||||
|
if (signal?.aborted) {
|
||||||
|
return reject(new Error("Movement aborted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||||
|
const distance = Math.abs(targetPosition - this.currentPosition);
|
||||||
|
|
||||||
|
// Ensure speed is a percentage [0, 1]
|
||||||
|
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||||
|
|
||||||
|
// Calculate duration based on percentage of distance per second
|
||||||
|
const duration = 1 / this.speed; // 1 second for 100% of the distance
|
||||||
|
|
||||||
|
this.timeleft = duration; //set this so other classes can use it
|
||||||
|
this.logger.debug(
|
||||||
|
`Linear movement: Direction=${direction}, Distance=${distance}, Duration=${duration}s`
|
||||||
|
);
|
||||||
|
|
||||||
|
let elapsedTime = 0;
|
||||||
|
const interval = this.interval; // Update every x ms
|
||||||
|
const totalSteps = Math.ceil((duration * 1000) / interval);
|
||||||
|
const stepSize = direction * (distance / totalSteps);
|
||||||
|
|
||||||
|
// 2) Set up the abort listener once
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
// 3) Check for abort on each tick
|
||||||
|
if (signal?.aborted) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
return reject(new Error("Movement aborted"));
|
||||||
|
}
|
||||||
|
// Update elapsed time
|
||||||
|
elapsedTime += interval / 1000;
|
||||||
|
|
||||||
|
this.timeleft = duration - elapsedTime; //set this so other classes can use it
|
||||||
|
|
||||||
|
// Update the position incrementally
|
||||||
|
this.currentPosition += stepSize;
|
||||||
|
this.emitPos(this.currentPosition);
|
||||||
|
this.logger.debug(
|
||||||
|
`Using ${this.movementMode} => Current position ${this.currentPosition}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the target position has been reached
|
||||||
|
if (
|
||||||
|
(direction > 0 && this.currentPosition >= targetPosition) ||
|
||||||
|
(direction < 0 && this.currentPosition <= targetPosition)
|
||||||
|
) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
this.currentPosition = targetPosition;
|
||||||
|
resolve(`Reached target move.`);
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
// Also attach abort outside the interval in case it fires before the first tick:
|
||||||
|
signal?.addEventListener("abort", () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
reject(new Error("Movement aborted"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
moveEaseInOut(targetPosition, signal) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 1) Bail immediately if already aborted
|
||||||
|
if (signal?.aborted) {
|
||||||
|
return reject(new Error("Movement aborted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||||
|
const totalDistance = Math.abs(targetPosition - this.currentPosition);
|
||||||
|
const startPosition = this.currentPosition;
|
||||||
|
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||||
|
|
||||||
|
const easeFunction = (t) =>
|
||||||
|
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||||
|
|
||||||
|
let elapsedTime = 0;
|
||||||
|
const duration = totalDistance / this.speed;
|
||||||
|
this.timeleft = duration;
|
||||||
|
const interval = this.interval;
|
||||||
|
|
||||||
|
// 2) Start the moving loop
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
// 3) Check for abort on each tick
|
||||||
|
if (signal?.aborted) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
return reject(new Error("Movement aborted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedTime += interval / 1000;
|
||||||
|
const progress = Math.min(elapsedTime / duration, 1);
|
||||||
|
this.timeleft = duration - elapsedTime;
|
||||||
|
const easedProgress = easeFunction(progress);
|
||||||
|
const newPosition =
|
||||||
|
startPosition + (targetPosition - startPosition) * easedProgress;
|
||||||
|
|
||||||
|
this.emitPos(newPosition);
|
||||||
|
this.logger.debug(
|
||||||
|
`Using ${this.movementMode} => Progress=${progress.toFixed(
|
||||||
|
2
|
||||||
|
)}, Eased=${easedProgress.toFixed(2)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (progress >= 1) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
this.currentPosition = targetPosition;
|
||||||
|
resolve(`Reached target move.`);
|
||||||
|
} else {
|
||||||
|
this.currentPosition = newPosition;
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
|
||||||
|
// 4) Also listen once for abort before first tick
|
||||||
|
signal?.addEventListener("abort", () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
reject(new Error("Movement aborted"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emitPos(newPosition) {
|
||||||
|
this.emitter.emit("positionChange", newPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
constrain(value) {
|
||||||
|
return Math.min(Math.max(value, this.minPosition), this.maxPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = movementManager;
|
||||||
131
helper/state/state.js
Normal file
131
helper/state/state.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
//load local dependencies
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const StateManager = require('./stateManager');
|
||||||
|
const MovementManager = require('./movementManager');
|
||||||
|
|
||||||
|
//load all config modules
|
||||||
|
const defaultConfig = require('./stateConfig.json');
|
||||||
|
const ConfigUtils = require('../../../generalFunctions/helper/configUtils');
|
||||||
|
|
||||||
|
class state{
|
||||||
|
constructor(config = {}, logger) {
|
||||||
|
|
||||||
|
this.emitter = new EventEmitter(); // Own EventEmitter
|
||||||
|
this.configUtils = new ConfigUtils(defaultConfig);
|
||||||
|
this.config = this.configUtils.initConfig(config);
|
||||||
|
this.abortController = null; // new abort controller for aborting async tasks
|
||||||
|
// Init after config is set
|
||||||
|
this.logger = logger;
|
||||||
|
|
||||||
|
// Initialize StateManager for state handling
|
||||||
|
this.stateManager = new StateManager(this.config,this.logger);
|
||||||
|
this.movementManager = new MovementManager(this.config, this.logger, this.emitter);
|
||||||
|
|
||||||
|
this.delayedMove = null;
|
||||||
|
this.mode = this.config.mode.current;
|
||||||
|
|
||||||
|
// Log initialization
|
||||||
|
this.logger.info("State class initialized.");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Delegate State Management -------- //
|
||||||
|
|
||||||
|
getMoveTimeLeft() {
|
||||||
|
return this.movementManager.timeleft;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentState() {
|
||||||
|
return this.stateManager.currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStateDescription() {
|
||||||
|
return this.stateManager.getStateDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Movement Methods -------- //
|
||||||
|
getCurrentPosition() {
|
||||||
|
return this.movementManager.getCurrentPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
getRunTimeHours() {
|
||||||
|
return this.stateManager.getRunTimeHours();
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveTo(targetPosition) {
|
||||||
|
|
||||||
|
// Check for invalid conditions and throw errors
|
||||||
|
if (targetPosition === this.getCurrentPosition()) {
|
||||||
|
this.logger.warn(`Target position=${targetPosition} is the same as the current position ${this.getCurrentPosition()}. Not executing move.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.stateManager.getCurrentState() !== "operational") {
|
||||||
|
if (this.config.mode.current === "auto") {
|
||||||
|
this.delayedMove = targetPosition;
|
||||||
|
this.logger.warn(`Saving setpoint=${targetPosition} to execute once back in 'operational' state.`);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.logger.warn(`Not able to accept setpoint=${targetPosition} while not in ${this.stateManager.getCurrentState()} state`);
|
||||||
|
}
|
||||||
|
//return early
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
const { signal } = this.abortController;
|
||||||
|
try {
|
||||||
|
const newState = targetPosition < this.getCurrentPosition() ? "decelerating" : "accelerating";
|
||||||
|
await this.transitionToState(newState,signal); // awaits transition
|
||||||
|
await this.movementManager.moveTo(targetPosition,signal); // awaits moving
|
||||||
|
this.emitter.emit("movementComplete", { position: targetPosition });
|
||||||
|
await this.transitionToState("operational");
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- State Transition Methods -------- //
|
||||||
|
|
||||||
|
async transitionToState(targetState, signal) {
|
||||||
|
|
||||||
|
const fromState = this.getCurrentState();
|
||||||
|
const position = this.getCurrentPosition();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
this.logger.debug(`Starting transition from ${fromState} to ${targetState}.`);
|
||||||
|
const feedback = await this.stateManager.transitionTo(targetState,signal);
|
||||||
|
this.logger.info(`Statemanager: ${feedback}`);
|
||||||
|
|
||||||
|
/* -- Auto pick setpoints in auto mode when operational--*/
|
||||||
|
if (
|
||||||
|
targetState === "operational" &&
|
||||||
|
this.config.mode.current === "auto" &&
|
||||||
|
this.delayedMove !== position &&
|
||||||
|
this.delayedMove
|
||||||
|
) {
|
||||||
|
this.logger.info(`Automatically picking up on last requested setpoint ${this.delayedMove}`);
|
||||||
|
//trigger move
|
||||||
|
await this.moveTo(this.delayedMove,signal);
|
||||||
|
this.delayedMove = null;
|
||||||
|
|
||||||
|
this.logger.info(`moveTo : ${feedback} `);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`State change to ${targetState} completed.`);
|
||||||
|
this.emitter.emit('stateChange', targetState); // <-- Implement Here
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error.message === "Transition aborted" ||
|
||||||
|
error.message === "Movement aborted"
|
||||||
|
) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = state;
|
||||||
|
|
||||||
331
helper/state/stateConfig.json
Normal file
331
helper/state/stateConfig.json
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "State Configuration",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name for the state configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration, assigned dynamically when needed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit used for the state values (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"logLevel": {
|
||||||
|
"default": "info",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "debug",
|
||||||
|
"description": "Log messages are printed for debugging purposes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "info",
|
||||||
|
"description": "Informational messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warn",
|
||||||
|
"description": "Warning messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "error",
|
||||||
|
"description": "Error messages are printed."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether logging is active. If true, log messages will be generated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"functionality": {
|
||||||
|
"softwareType": {
|
||||||
|
"default": "state class",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Logical name identifying the software type."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "StateController",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Functional role within the system."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"starting": {
|
||||||
|
"default": 10,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Time in seconds for the starting phase."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warmingup": {
|
||||||
|
"default": 5,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Time in seconds for the warming-up phase."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stopping": {
|
||||||
|
"default": 5,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Time in seconds for the stopping phase."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"coolingdown": {
|
||||||
|
"default": 10,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Time in seconds for the cooling-down phase."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"movement": {
|
||||||
|
"mode": {
|
||||||
|
"default": "dynspeed",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "staticspeed",
|
||||||
|
"description": "Linear movement to setpoint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "dynspeed",
|
||||||
|
"description": "Ease-in and ease-out to setpoint."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"default": 1,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Current speed setting."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maxSpeed": {
|
||||||
|
"default": 10,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum speed setting."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interval": {
|
||||||
|
"default": 1000,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Feedback interval in milliseconds."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"min": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Minimum position value."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"default": 100,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum position value."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"initial": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Initial position value."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"current": {
|
||||||
|
"default": "idle",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "idle",
|
||||||
|
"description": "Machine is idle."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "starting",
|
||||||
|
"description": "Machine is starting up."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warmingup",
|
||||||
|
"description": "Machine is warming up."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "operational",
|
||||||
|
"description": "Machine is running."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "accelerating",
|
||||||
|
"description": "Machine is accelerating."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "decelerating",
|
||||||
|
"description": "Machine is decelerating."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "stopping",
|
||||||
|
"description": "Machine is stopping."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "coolingdown",
|
||||||
|
"description": "Machine is cooling down."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"description": "Machine is off."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Current state of the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedTransitions":{
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"idle": {
|
||||||
|
"default": ["starting", "off","emergencystop"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from idle state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"starting": {
|
||||||
|
"default": ["starting","warmingup","emergencystop"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from starting state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warmingup": {
|
||||||
|
"default": ["operational","emergencystop"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from warmingup state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operational": {
|
||||||
|
"default": ["accelerating", "decelerating", "stopping","emergencystop"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from operational state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accelerating": {
|
||||||
|
"default": ["operational","emergencystop"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from accelerating state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"decelerating": {
|
||||||
|
"default": ["operational","emergencystop"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from decelerating state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stopping": {
|
||||||
|
"default": ["idle","coolingdown","emergencystop"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from stopping state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"coolingdown": {
|
||||||
|
"default": ["idle","off","emergencystop"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from coolingDown state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"off": {
|
||||||
|
"default": ["idle","emergencystop"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from off state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emergencystop": {
|
||||||
|
"default": ["idle","off"],
|
||||||
|
"rules":{
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Allowed transitions from emergency stop state."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Allowed transitions between states."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"activeStates":{
|
||||||
|
"default": ["operational", "starting", "warmingup", "accelerating", "decelerating"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Active states."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"current": {
|
||||||
|
"default": "auto",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "auto",
|
||||||
|
"description": "Automatically tracks and handles delayed commands for setpoints > 0."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "manual",
|
||||||
|
"description": "Requires explicit commands to start."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Current mode of the machine."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
164
helper/state/stateManager.js
Normal file
164
helper/state/stateManager.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* @file stateManager.js
|
||||||
|
*
|
||||||
|
* Permission is hereby granted to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to use it for personal
|
||||||
|
* or non-commercial purposes, with the following restrictions:
|
||||||
|
*
|
||||||
|
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
|
||||||
|
* be copied, merged, distributed, sublicensed, or sold without explicit
|
||||||
|
* prior written permission from the author.
|
||||||
|
*
|
||||||
|
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
|
||||||
|
* a valid license, obtainable only with the explicit consent of the author.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Ownership of this code remains solely with the original author. Unauthorized
|
||||||
|
* use of this Software is strictly prohibited.
|
||||||
|
*
|
||||||
|
* @summary Class for managing state transitions and state descriptions.
|
||||||
|
* @description Class for managing state transitions and state descriptions.
|
||||||
|
* @module stateManager
|
||||||
|
* @exports stateManager
|
||||||
|
* @version 0.1.0
|
||||||
|
* @since 0.1.0
|
||||||
|
*
|
||||||
|
* Author:
|
||||||
|
* - Rene De Ren
|
||||||
|
* Email:
|
||||||
|
* - rene@thegoldenbasket.nl
|
||||||
|
*/
|
||||||
|
|
||||||
|
class stateManager {
|
||||||
|
constructor(config, logger) {
|
||||||
|
this.currentState = config.state.current;
|
||||||
|
this.availableStates = config.state.available;
|
||||||
|
this.descriptions = config.state.descriptions;
|
||||||
|
this.logger = logger;
|
||||||
|
this.transitionTimeleft = 0;
|
||||||
|
this.transitionTimes = config.time;
|
||||||
|
|
||||||
|
// Define valid transitions (can be extended dynamically if needed)
|
||||||
|
this.validTransitions = config.state.allowedTransitions;
|
||||||
|
|
||||||
|
// NEW: Initialize runtime tracking
|
||||||
|
this.runTimeHours = 0; // cumulative runtime in hours
|
||||||
|
this.runTimeStart = null; // timestamp when active state began
|
||||||
|
|
||||||
|
// Define active states (runtime counts only in these states)
|
||||||
|
this.activeStates = config.state.activeStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentState() {
|
||||||
|
return this.currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionTo(newState,signal) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (signal && signal.aborted) {
|
||||||
|
this.logger.debug("Transition aborted.");
|
||||||
|
return reject("Transition aborted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isValidTransition(newState)) {
|
||||||
|
return reject(
|
||||||
|
`Invalid transition from ${this.currentState} to ${newState}. Transition not executed.`
|
||||||
|
); //go back early and reject promise
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Handle runtime tracking based on active states
|
||||||
|
this.handleRuntimeTracking(newState);
|
||||||
|
|
||||||
|
const transitionDuration = this.transitionTimes[this.currentState] || 0; // Default to 0 if no transition time
|
||||||
|
this.logger.debug(
|
||||||
|
`Transition from ${this.currentState} to ${newState} will take ${transitionDuration}s.`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (transitionDuration > 0) {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
this.currentState = newState;
|
||||||
|
resolve(`Transition from ${this.currentState} to ${newState} completed in ${transitionDuration}s.`);
|
||||||
|
}, transitionDuration * 1000);
|
||||||
|
if (signal) {
|
||||||
|
signal.addEventListener('abort', () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
reject(new Error('Transition aborted'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.currentState = newState;
|
||||||
|
resolve(`Immediate transition to ${this.currentState} completed.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRuntimeTracking(newState) {
|
||||||
|
// NEW: Handle runtime tracking based on active states
|
||||||
|
const wasActive = this.activeStates.has(this.currentState);
|
||||||
|
const willBeActive = this.activeStates.has(newState);
|
||||||
|
if (wasActive && !willBeActive && this.runTimeStart) {
|
||||||
|
// stop runtime timer and accumulate elapsed time
|
||||||
|
const elapsed = (Date.now() - this.runTimeStart) / 3600000; // hours
|
||||||
|
this.runTimeHours += elapsed;
|
||||||
|
this.runTimeStart = null;
|
||||||
|
this.logger.debug(
|
||||||
|
`Runtime timer stopped; elapsed=${elapsed.toFixed(
|
||||||
|
3
|
||||||
|
)}h, total=${this.runTimeHours.toFixed(3)}h.`
|
||||||
|
);
|
||||||
|
} else if (!wasActive && willBeActive && !this.runTimeStart) {
|
||||||
|
// starting new runtime
|
||||||
|
this.runTimeStart = Date.now();
|
||||||
|
this.logger.debug("Runtime timer started.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidTransition(newState) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Check 1 Transition valid ? From ${
|
||||||
|
this.currentState
|
||||||
|
} To ${newState} => ${this.validTransitions[this.currentState]?.has(
|
||||||
|
newState
|
||||||
|
)} `
|
||||||
|
);
|
||||||
|
this.logger.debug(
|
||||||
|
`Check 2 Transition valid ? ${
|
||||||
|
this.currentState
|
||||||
|
} is not equal to ${newState} => ${this.currentState !== newState}`
|
||||||
|
);
|
||||||
|
// check if transition is valid and not the same as before
|
||||||
|
const valid =
|
||||||
|
this.validTransitions[this.currentState]?.has(newState) &&
|
||||||
|
this.currentState !== newState;
|
||||||
|
|
||||||
|
//if not valid
|
||||||
|
if (!valid) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStateDescription(state = this.currentState) {
|
||||||
|
return this.descriptions[state] || "No description available.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Getter to retrieve current cumulative runtime (active time) in hours.
|
||||||
|
getRunTimeHours() {
|
||||||
|
// If currently active add the ongoing duration.
|
||||||
|
let currentElapsed = 0;
|
||||||
|
if (this.runTimeStart) {
|
||||||
|
currentElapsed = (Date.now() - this.runTimeStart) / 3600000;
|
||||||
|
}
|
||||||
|
return this.runTimeHours + currentElapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = stateManager;
|
||||||
528
helper/validationUtils.js
Normal file
528
helper/validationUtils.js
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
/**
|
||||||
|
* @file validation.js
|
||||||
|
*
|
||||||
|
* Permission is hereby granted to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to use it for personal
|
||||||
|
* or non-commercial purposes, with the following restrictions:
|
||||||
|
*
|
||||||
|
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
|
||||||
|
* be copied, merged, distributed, sublicensed, or sold without explicit
|
||||||
|
* prior written permission from the author.
|
||||||
|
*
|
||||||
|
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
|
||||||
|
* a valid license, obtainable only with the explicit consent of the author.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Ownership of this code remains solely with the original author. Unauthorized
|
||||||
|
* use of this Software is strictly prohibited.
|
||||||
|
|
||||||
|
* @summary Validation utility for validating and constraining configuration values.
|
||||||
|
* @description Validation utility for validating and constraining configuration values.
|
||||||
|
* @module ValidationUtils
|
||||||
|
* @requires Logger
|
||||||
|
* @exports ValidationUtils
|
||||||
|
* @version 0.1.0
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Logger = require("./logger");
|
||||||
|
|
||||||
|
class ValidationUtils {
|
||||||
|
constructor(IloggerEnabled, IloggerLevel) {
|
||||||
|
const loggerEnabled = IloggerEnabled || true;
|
||||||
|
const loggerLevel = IloggerLevel || "warn";
|
||||||
|
this.logger = new Logger(loggerEnabled, loggerLevel, 'ValidationUtils');
|
||||||
|
}
|
||||||
|
|
||||||
|
constrain(value, min, max) {
|
||||||
|
if (typeof value !== "number") {
|
||||||
|
this.logger?.warn(`Value '${value}' is not a number. Defaulting to ${min}.`);
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateSchema(config, schema, name) {
|
||||||
|
|
||||||
|
const validatedConfig = {};
|
||||||
|
let configValue;
|
||||||
|
|
||||||
|
// 1. Remove any unknown keys (keys not defined in the schema).
|
||||||
|
// Log a warning and omit them from the final config.
|
||||||
|
for (const key of Object.keys(config)) {
|
||||||
|
if (!(key in schema)) {
|
||||||
|
this.logger.warn(
|
||||||
|
`[${name}] Unknown key '${key}' found in config. Removing it.`
|
||||||
|
);
|
||||||
|
delete config[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each key in the schema and loop over wildcards if they are not in schema
|
||||||
|
for ( const key in schema ) {
|
||||||
|
|
||||||
|
if (key === "rules" || key === "description" || key === "schema") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldSchema = schema[key];
|
||||||
|
const { rules = {} } = fieldSchema;
|
||||||
|
|
||||||
|
// Default to the schema's default value if the key is missing
|
||||||
|
if (config[key] === undefined) {
|
||||||
|
if (fieldSchema.default === undefined) {
|
||||||
|
// If there's a nested schema, go deeper with an empty object rather than logging "no rule"
|
||||||
|
if (rules.schema) {
|
||||||
|
this.logger.warn(`${name}.${key} has no default, but has a nested schema.`);
|
||||||
|
validatedConfig[key] = this.validateSchema({}, rules.schema, `${name}.${key}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.logger.info(
|
||||||
|
`There is no rule for ${name}.${key} and no default value. ` +
|
||||||
|
`Using full schema value but validating deeper levels first...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const SubObject = this.validateSchema({}, fieldSchema, `${name}.${key}`);
|
||||||
|
|
||||||
|
validatedConfig[key] = SubObject;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.info(`There is no value provided for ${name}.${key}. Using default value.`);
|
||||||
|
configValue = fieldSchema.default;
|
||||||
|
}
|
||||||
|
//continue;
|
||||||
|
} else {
|
||||||
|
// Use the provided value if it exists, otherwise use the default value
|
||||||
|
configValue = config[key] !== undefined ? config[key] : fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to parse the value to the expected type if possible
|
||||||
|
switch (rules.type) {
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
configValue = this.validateNumber(configValue, rules, fieldSchema, name, key);
|
||||||
|
break;
|
||||||
|
case "boolean":
|
||||||
|
configValue = this.validateBoolean(configValue, name, key);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "string":
|
||||||
|
configValue = this.validateString(configValue,rules,fieldSchema, name, key);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "array":
|
||||||
|
configValue = this.validateArray(configValue, rules, fieldSchema, name, key);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "set":
|
||||||
|
configValue = this.validateSet(configValue, rules, fieldSchema, name, key);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "object":
|
||||||
|
configValue = this.validateObject(configValue, rules, fieldSchema, name, key);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "enum":
|
||||||
|
configValue = this.validateEnum(configValue, rules, fieldSchema, name, key);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "curve":
|
||||||
|
validatedConfig[key] = this.validateCurve(configValue,fieldSchema.default);
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case "machineCurve":
|
||||||
|
validatedConfig[key] = this.validateMachineCurve(configValue,fieldSchema.default);
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case "integer":
|
||||||
|
validatedConfig[key] = this.validateInteger(configValue, rules, fieldSchema, name, key);
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case undefined:
|
||||||
|
// If we see 'rules.schema' but no 'rules.type', treat it like an object:
|
||||||
|
if (rules.schema && !rules.type) {
|
||||||
|
// Log a warning and skip the extra pass for nested schema
|
||||||
|
this.logger.warn(
|
||||||
|
`${name}.${key} has a nested schema but no type. ` +
|
||||||
|
`Treating it as type="object" to skip extra pass.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Otherwise, fallback to your existing "validateUndefined" logic
|
||||||
|
validatedConfig[key] = this.validateUndefined(configValue, fieldSchema, name, key);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.logger.warn(`${name}.${key} has an unknown validation type: ${rules.type}. Skipping validation.`);
|
||||||
|
validatedConfig[key] = fieldSchema.default;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign the validated or converted value
|
||||||
|
validatedConfig[key] = configValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore unknown keys by not processing them at all
|
||||||
|
this.logger.info(`Validation completed for ${name}.`);
|
||||||
|
|
||||||
|
return validatedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUnwantedKeys(obj) {
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map((item) => this.removeUnwantedKeys(item));
|
||||||
|
}
|
||||||
|
if (obj && typeof obj === "object") {
|
||||||
|
const newObj = {};
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
|
||||||
|
// Skip or remove keys like 'default', 'rules', 'description', etc.
|
||||||
|
if (["rules", "description"].includes(k)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if("default" in v){
|
||||||
|
//put the default value in the object
|
||||||
|
newObj[k] = v.default;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
newObj[k] = this.removeUnwantedKeys(v);
|
||||||
|
}
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateMachineCurve(curve, defaultCurve) {
|
||||||
|
if (!curve || typeof curve !== "object" || Object.keys(curve).length === 0) {
|
||||||
|
this.logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
|
||||||
|
return defaultCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that nq and np exist and are objects
|
||||||
|
const { nq, np } = curve;
|
||||||
|
if (!nq || typeof nq !== "object" || !np || typeof np !== "object") {
|
||||||
|
this.logger.warn("Curve must contain valid 'nq' and 'np' objects. Defaulting to basic curve.");
|
||||||
|
return defaultCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that each dimension key points to a valid object with x and y arrays
|
||||||
|
const validatedNq = this.validateDimensionStructure(nq, "nq");
|
||||||
|
const validatedNp = this.validateDimensionStructure(np, "np");
|
||||||
|
|
||||||
|
if (!validatedNq || !validatedNp) {
|
||||||
|
return defaultCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nq: validatedNq, np: validatedNp }; // Return the validated curve
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCurve(curve, defaultCurve) {
|
||||||
|
if (!curve || typeof curve !== "object" || Object.keys(curve).length === 0) {
|
||||||
|
this.logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
|
||||||
|
return defaultCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that each dimension key points to a valid object with x and y arrays
|
||||||
|
const validatedCurve = this.validateDimensionStructure(curve, "curve");
|
||||||
|
if (!validatedCurve) {
|
||||||
|
return defaultCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validatedCurve; // Return the validated curve
|
||||||
|
}
|
||||||
|
|
||||||
|
validateDimensionStructure(dimension, name) {
|
||||||
|
const validatedDimension = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(dimension)) {
|
||||||
|
// Validate that each key points to an object with x and y arrays
|
||||||
|
if (typeof value !== "object") {
|
||||||
|
this.logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate that x and y are arrays
|
||||||
|
else if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
|
||||||
|
this.logger.warn(`Dimension '${name}' key '${key}' is missing x or y arrays. Converting to arrays.`);
|
||||||
|
// Try to convert to arrays first
|
||||||
|
value.x = Object.values(value.x);
|
||||||
|
value.y = Object.values(value.y);
|
||||||
|
|
||||||
|
// If still not arrays return false
|
||||||
|
if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
|
||||||
|
this.logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Validate that x and y arrays are the same length
|
||||||
|
else if (value.x.length !== value.y.length) {
|
||||||
|
this.logger.warn(`Dimension '${name}' key '${key}' has mismatched x and y lengths. Ignoring this key.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate that x values are in ascending order
|
||||||
|
else if (!this.isSorted(value.x)) {
|
||||||
|
this.logger.warn(`Dimension '${name}' key '${key}' has unsorted x values. Sorting...`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate that x values are unique
|
||||||
|
else if (!this.isUnique(value.x)) {
|
||||||
|
this.logger.warn(`Dimension '${name}' key '${key}' has duplicate x values. Removing duplicates...`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate that y values are numbers
|
||||||
|
else if (!this.areNumbers(value.y)) {
|
||||||
|
this.logger.warn(`Dimension '${name}' key '${key}' has non-numeric y values. Ignoring this key.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validatedDimension[key] = value;
|
||||||
|
}
|
||||||
|
return validatedDimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSorted(arr) {
|
||||||
|
return arr.every((_, i) => i === 0 || arr[i] >= arr[i - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
isUnique(arr) {
|
||||||
|
return new Set(arr).size === arr.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
areNumbers(arr) {
|
||||||
|
return arr.every((x) => typeof x === "number");
|
||||||
|
}
|
||||||
|
|
||||||
|
validateNumber(configValue, rules, fieldSchema, name, key) {
|
||||||
|
|
||||||
|
if (typeof configValue !== "number") {
|
||||||
|
const parsedValue = parseFloat(configValue);
|
||||||
|
if (!isNaN(parsedValue)) {
|
||||||
|
this.logger.warn(`${name}.${key} was parsed to a number: ${configValue} -> ${parsedValue}`);
|
||||||
|
configValue = parsedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.min !== undefined && configValue < rules.min) {
|
||||||
|
this.logger.warn(
|
||||||
|
`${name}.${key} is below the minimum (${rules.min}). Using default value.`
|
||||||
|
);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
if (rules.max !== undefined && configValue > rules.max) {
|
||||||
|
this.logger.warn(
|
||||||
|
`${name}.${key} exceeds the maximum (${rules.max}). Using default value.`
|
||||||
|
);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`${name}.${key} is a valid number: ${configValue}`);
|
||||||
|
|
||||||
|
return configValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
validateInteger(configValue, rules, fieldSchema, name, key) {
|
||||||
|
if (typeof configValue !== "number" || !Number.isInteger(configValue)) {
|
||||||
|
const parsedValue = parseInt(configValue, 10);
|
||||||
|
if (!isNaN(parsedValue) && Number.isInteger(parsedValue)) {
|
||||||
|
this.logger.warn(`${name}.${key} was parsed to an integer: ${configValue} -> ${parsedValue}`);
|
||||||
|
configValue = parsedValue;
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`${name}.${key} is not a valid integer. Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.min !== undefined && configValue < rules.min) {
|
||||||
|
this.logger.warn(`${name}.${key} is below the minimum integer value (${rules.min}). Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.max !== undefined && configValue > rules.max) {
|
||||||
|
this.logger.warn(`${name}.${key} exceeds the maximum integer value (${rules.max}). Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`${name}.${key} is a valid integer: ${configValue}`);
|
||||||
|
return configValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateBoolean(configValue, name, key) {
|
||||||
|
if (typeof configValue !== "boolean") {
|
||||||
|
if (configValue === "true" || configValue === "false") {
|
||||||
|
const parsedValue = configValue === "true";
|
||||||
|
this.logger.debug(`${name}.${key} was parsed to a boolean: ${configValue} -> ${parsedValue}`);
|
||||||
|
configValue = parsedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateString(configValue, rules, fieldSchema, name, key) {
|
||||||
|
let newConfigValue = configValue;
|
||||||
|
|
||||||
|
if (typeof configValue !== "string") {
|
||||||
|
//check if the value is nullable
|
||||||
|
if(rules.nullable){
|
||||||
|
if(configValue === null){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(`${name}.${key} is not a string. Trying to convert to string.`);
|
||||||
|
newConfigValue = String(configValue); // Coerce to string if not already
|
||||||
|
}
|
||||||
|
|
||||||
|
//check if the string is a valid string after conversion
|
||||||
|
if (typeof newConfigValue !== "string") {
|
||||||
|
this.logger.warn(`${name}.${key} is not a valid string. Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newConfigValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateSet(configValue, rules, fieldSchema, name, key) {
|
||||||
|
// 1. Ensure we have a Set. If not, use default.
|
||||||
|
if (!(configValue instanceof Set)) {
|
||||||
|
this.logger.info(`${name}.${key} is not a Set. Converting to one using default value.`);
|
||||||
|
return new Set(fieldSchema.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Convert the Set to an array for easier filtering.
|
||||||
|
const validatedArray = [...configValue]
|
||||||
|
.filter((item) => {
|
||||||
|
// 3. Filter based on `rules.itemType`.
|
||||||
|
switch (rules.itemType) {
|
||||||
|
case "number":
|
||||||
|
return typeof item === "number";
|
||||||
|
case "string":
|
||||||
|
return typeof item === "string";
|
||||||
|
case "null":
|
||||||
|
// "null" might mean no type restriction (your usage may vary).
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
// Fallback if itemType is something else
|
||||||
|
return typeof item === rules.itemType;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.slice(0, rules.maxLength || Infinity);
|
||||||
|
|
||||||
|
// 4. Check if the filtered array meets the minimum length.
|
||||||
|
if (validatedArray.length < (rules.minLength || 1)) {
|
||||||
|
this.logger.warn(
|
||||||
|
`${name}.${key} contains fewer items than allowed (${rules.minLength}). Using default value.`
|
||||||
|
);
|
||||||
|
return new Set(fieldSchema.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Return a new Set containing only the valid items.
|
||||||
|
return new Set(validatedArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateArray(configValue, rules, fieldSchema, name, key) {
|
||||||
|
if (!Array.isArray(configValue)) {
|
||||||
|
this.logger.info(`${name}.${key} is not an array. Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate individual items in the array
|
||||||
|
const validatedArray = configValue
|
||||||
|
.filter((item) => {
|
||||||
|
switch (rules.itemType) {
|
||||||
|
case "number":
|
||||||
|
return typeof item === "number";
|
||||||
|
case "string":
|
||||||
|
return typeof item === "string";
|
||||||
|
case "null":
|
||||||
|
// anything goes
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return typeof item === rules.itemType;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.slice(0, rules.maxLength || Infinity);
|
||||||
|
|
||||||
|
if (validatedArray.length < (rules.minLength || 1)) {
|
||||||
|
this.logger.warn(
|
||||||
|
`${name}.${key} contains fewer items than allowed (${rules.minLength}). Using default value.`
|
||||||
|
);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validatedArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateObject(configValue, rules, fieldSchema, name, key) {
|
||||||
|
if (typeof configValue !== "object" || Array.isArray(configValue)) {
|
||||||
|
this.logger.warn(`${name}.${key} is not a valid object. Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.schema) {
|
||||||
|
// Recursively validate nested objects if a schema is defined
|
||||||
|
return this.validateSchema(configValue || {}, rules.schema, `${name}.${key}`);
|
||||||
|
} else {
|
||||||
|
// If no schema is defined, log a warning and use the default
|
||||||
|
this.logger.warn(`${name}.${key} is an object with no schema. Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateEnum(configValue, rules, fieldSchema, name, key) {
|
||||||
|
|
||||||
|
if (Array.isArray(rules.values)) {
|
||||||
|
|
||||||
|
//if value is null take default
|
||||||
|
if(configValue === null){
|
||||||
|
this.logger.warn(`${name}.${key} is null. Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validValues = rules.values.map(e => e.value.toLowerCase());
|
||||||
|
|
||||||
|
//remove caps
|
||||||
|
configValue = configValue.toLowerCase();
|
||||||
|
|
||||||
|
if (!validValues.includes(configValue)) {
|
||||||
|
this.logger.warn(
|
||||||
|
`${name}.${key} has an invalid value : ${configValue}. Allowed values: [${validValues.join(", ")}]. Using default value.`
|
||||||
|
);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.warn(
|
||||||
|
`${name}.${key} is an enum with no 'values' array. Using default value.`
|
||||||
|
);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
return configValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateUndefined(configValue, fieldSchema, name, key) {
|
||||||
|
if (typeof configValue === "object" && !Array.isArray(configValue)) {
|
||||||
|
|
||||||
|
this.logger.debug(`${name}.${key} has no defined rules but is an object going 1 level deeper.`);
|
||||||
|
|
||||||
|
// Recursively validate the nested object
|
||||||
|
return this.validateSchema( configValue || {}, fieldSchema, `${name}.${key}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.logger.warn(`${name}.${key} has no defined rules. Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ValidationUtils;
|
||||||
14
settings/projectSettings.json
Normal file
14
settings/projectSettings.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"uuid": "b02b3a50-3f36-4ab0-9479-84de2fca9cb1",
|
||||||
|
"locationId": "1",
|
||||||
|
"configUrls": {
|
||||||
|
"cloud": {
|
||||||
|
"units": "https://example.com/api/units",
|
||||||
|
"taggcodeAPI": "https://pimmoerman.nl/rdlab/tagcode.app/v2.1/apiBLAH"
|
||||||
|
},
|
||||||
|
"local": {
|
||||||
|
"units": "http://localhost:1880/generalFunctions/datasets/unitData.json",
|
||||||
|
"taggcodeAPI": "http://localhost:1880/generalFunctions/datasets/assetData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
settings/projectSpecificSettings.js
Normal file
109
settings/projectSpecificSettings.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class ProjectSettings {
|
||||||
|
constructor() {
|
||||||
|
this.userDir = path.resolve(__dirname); // Use the directory of the current script
|
||||||
|
this.settingsFilePath = path.join(this.userDir, 'projectSettings.json'); // File path for settings
|
||||||
|
this.uuid = this.loadOrCreateUUID();
|
||||||
|
this.getProjectVars(); // load project variables
|
||||||
|
//this url could also be the source of all the configs
|
||||||
|
this.cloudAPI = "https://pimmoerman.nl/rdlab/tagcode.app/v2/api";
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOrCreateUUID() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(this.settingsFilePath)) {
|
||||||
|
const fileContent = fs.readFileSync(this.settingsFilePath, 'utf8');
|
||||||
|
if (fileContent.trim()) { // Check if file is not empty
|
||||||
|
try {
|
||||||
|
const jsonContent = JSON.parse(fileContent);
|
||||||
|
if (jsonContent && jsonContent.uuid) {
|
||||||
|
return jsonContent.uuid;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid JSON structure");
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error("Error parsing settings file:", parseError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the file is empty or invalid, fall back to creating a new UUID
|
||||||
|
const newUuid = this.generateUUID();
|
||||||
|
this.saveSettings({ uuid: newUuid });
|
||||||
|
return newUuid;
|
||||||
|
} else {
|
||||||
|
// If the file doesn't exist, create a new UUID and save it
|
||||||
|
const newUuid = this.generateUUID();
|
||||||
|
this.saveSettings({ uuid: newUuid });
|
||||||
|
return newUuid;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error handling settings file:", err.message);
|
||||||
|
const newUuid = this.generateUUID(); // Fallback
|
||||||
|
this.saveSettings({ uuid: newUuid });
|
||||||
|
return newUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings(settings) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(this.settingsFilePath, JSON.stringify(settings, null, 2), 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error saving settings file:", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateUUID() {
|
||||||
|
let d = new Date().getTime(); // Timestamp
|
||||||
|
let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0; // Time in microseconds since page-load or 0 if unsupported
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
let r = Math.random() * 16; // Random number between 0 and 16
|
||||||
|
if (d > 0) { // Use timestamp until depleted
|
||||||
|
r = (d + r) % 16 | 0;
|
||||||
|
d = Math.floor(d / 16);
|
||||||
|
} else { // Use microseconds since page-load if supported
|
||||||
|
r = (d2 + r) % 16 | 0;
|
||||||
|
d2 = Math.floor(d2 / 16);
|
||||||
|
}
|
||||||
|
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getProjectVars() {
|
||||||
|
try {
|
||||||
|
let settings = {};
|
||||||
|
if (fs.existsSync(this.settingsFilePath)) {
|
||||||
|
const fileContent = fs.readFileSync(this.settingsFilePath, 'utf8');
|
||||||
|
settings = JSON.parse(fileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge default settings with existing settings
|
||||||
|
const defaultVars = {
|
||||||
|
uuid: this.uuid, // Ensure UUID is included
|
||||||
|
locationId: "1",
|
||||||
|
configUrls: {
|
||||||
|
cloud: {
|
||||||
|
units: "https://example.com/api/units",
|
||||||
|
taggcodeAPI: this.cloudAPI,
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
units: "http://localhost:1880/generalFunctions/datasets/unitData.json",
|
||||||
|
taggcodeAPI: "http://localhost:1880/generalFunctions/datasets/assetData",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save merged settings back to the file
|
||||||
|
const mergedSettings = { ...defaultVars, ...settings };
|
||||||
|
this.saveSettings(mergedSettings);
|
||||||
|
|
||||||
|
return mergedSettings;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading project variables:", err.message);
|
||||||
|
return null; // Fallback if there's an issue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ProjectSettings;
|
||||||
597
settings/settings.js
Normal file
597
settings/settings.js
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
/**
|
||||||
|
* This is the default settings file provided by Node-RED.
|
||||||
|
*
|
||||||
|
* It can contain any valid JavaScript code that will get run when Node-RED
|
||||||
|
* is started.
|
||||||
|
*
|
||||||
|
* Lines that start with // are commented out.
|
||||||
|
* Each entry should be separated from the entries above and below by a comma ','
|
||||||
|
*
|
||||||
|
* For more information about individual settings, refer to the documentation:
|
||||||
|
* https://nodered.org/docs/user-guide/runtime/configuration
|
||||||
|
*
|
||||||
|
* The settings are split into the following sections:
|
||||||
|
* - Flow File and User Directory Settings
|
||||||
|
* - Security
|
||||||
|
* - Server Settings
|
||||||
|
* - Runtime Settings
|
||||||
|
* - Editor Settings
|
||||||
|
* - Node Settings
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Flow File and User Directory Settings
|
||||||
|
* - flowFile
|
||||||
|
* - credentialSecret
|
||||||
|
* - flowFilePretty
|
||||||
|
* - userDir
|
||||||
|
* - nodesDir
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/** The file containing the flows. If not set, defaults to flows_<hostname>.json **/
|
||||||
|
flowFile: 'flows.json',
|
||||||
|
|
||||||
|
/** By default, credentials are encrypted in storage using a generated key. To
|
||||||
|
* specify your own secret, set the following property.
|
||||||
|
* If you want to disable encryption of credentials, set this property to false.
|
||||||
|
* Note: once you set this property, do not change it - doing so will prevent
|
||||||
|
* node-red from being able to decrypt your existing credentials and they will be
|
||||||
|
* lost.
|
||||||
|
*/
|
||||||
|
//credentialSecret: "a-secret-key",
|
||||||
|
|
||||||
|
/** By default, the flow JSON will be formatted over multiple lines making
|
||||||
|
* it easier to compare changes when using version control.
|
||||||
|
* To disable pretty-printing of the JSON set the following property to false.
|
||||||
|
*/
|
||||||
|
flowFilePretty: true,
|
||||||
|
|
||||||
|
/** By default, all user data is stored in a directory called `.node-red` under
|
||||||
|
* the user's home directory. To use a different location, the following
|
||||||
|
* property can be used
|
||||||
|
*/
|
||||||
|
//userDir: '/home/nol/.node-red/',
|
||||||
|
|
||||||
|
/** Node-RED scans the `nodes` directory in the userDir to find local node files.
|
||||||
|
* The following property can be used to specify an additional directory to scan.
|
||||||
|
*/
|
||||||
|
//nodesDir: '/home/nol/.node-red/nodes',
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Security
|
||||||
|
* - adminAuth
|
||||||
|
* - https
|
||||||
|
* - httpsRefreshInterval
|
||||||
|
* - requireHttps
|
||||||
|
* - httpNodeAuth
|
||||||
|
* - httpStaticAuth
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/** To password protect the Node-RED editor and admin API, the following
|
||||||
|
* property can be used. See https://nodered.org/docs/security.html for details.
|
||||||
|
*/
|
||||||
|
//adminAuth: {
|
||||||
|
// type: "credentials",
|
||||||
|
// users: [{
|
||||||
|
// username: "admin",
|
||||||
|
// password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.",
|
||||||
|
// permissions: "*"
|
||||||
|
// }]
|
||||||
|
//},
|
||||||
|
|
||||||
|
/** The following property can be used to enable HTTPS
|
||||||
|
* This property can be either an object, containing both a (private) key
|
||||||
|
* and a (public) certificate, or a function that returns such an object.
|
||||||
|
* See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener
|
||||||
|
* for details of its contents.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Option 1: static object */
|
||||||
|
//https: {
|
||||||
|
// key: require("fs").readFileSync('privkey.pem'),
|
||||||
|
// cert: require("fs").readFileSync('cert.pem')
|
||||||
|
//},
|
||||||
|
|
||||||
|
/** Option 2: function that returns the HTTP configuration object */
|
||||||
|
// https: function() {
|
||||||
|
// // This function should return the options object, or a Promise
|
||||||
|
// // that resolves to the options object
|
||||||
|
// return {
|
||||||
|
// key: require("fs").readFileSync('privkey.pem'),
|
||||||
|
// cert: require("fs").readFileSync('cert.pem')
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
|
||||||
|
/** If the `https` setting is a function, the following setting can be used
|
||||||
|
* to set how often, in hours, the function will be called. That can be used
|
||||||
|
* to refresh any certificates.
|
||||||
|
*/
|
||||||
|
//httpsRefreshInterval : 12,
|
||||||
|
|
||||||
|
/** The following property can be used to cause insecure HTTP connections to
|
||||||
|
* be redirected to HTTPS.
|
||||||
|
*/
|
||||||
|
//requireHttps: true,
|
||||||
|
|
||||||
|
/** To password protect the node-defined HTTP endpoints (httpNodeRoot),
|
||||||
|
* including node-red-dashboard, or the static content (httpStatic), the
|
||||||
|
* following properties can be used.
|
||||||
|
* The `pass` field is a bcrypt hash of the password.
|
||||||
|
* See https://nodered.org/docs/security.html#generating-the-password-hash
|
||||||
|
*/
|
||||||
|
//httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
|
||||||
|
//httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Server Settings
|
||||||
|
* - uiPort
|
||||||
|
* - uiHost
|
||||||
|
* - apiMaxLength
|
||||||
|
* - httpServerOptions
|
||||||
|
* - httpAdminRoot
|
||||||
|
* - httpAdminMiddleware
|
||||||
|
* - httpAdminCookieOptions
|
||||||
|
* - httpNodeRoot
|
||||||
|
* - httpNodeCors
|
||||||
|
* - httpNodeMiddleware
|
||||||
|
* - httpStatic
|
||||||
|
* - httpStaticRoot
|
||||||
|
* - httpStaticCors
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/** the tcp port that the Node-RED web server is listening on */
|
||||||
|
uiPort: process.env.PORT || 1880,
|
||||||
|
|
||||||
|
/** By default, the Node-RED UI accepts connections on all IPv4 interfaces.
|
||||||
|
* To listen on all IPv6 addresses, set uiHost to "::",
|
||||||
|
* The following property can be used to listen on a specific interface. For
|
||||||
|
* example, the following would only allow connections from the local machine.
|
||||||
|
*/
|
||||||
|
//uiHost: "127.0.0.1",
|
||||||
|
|
||||||
|
/** The maximum size of HTTP request that will be accepted by the runtime api.
|
||||||
|
* Default: 5mb
|
||||||
|
*/
|
||||||
|
//apiMaxLength: '5mb',
|
||||||
|
|
||||||
|
/** The following property can be used to pass custom options to the Express.js
|
||||||
|
* server used by Node-RED. For a full list of available options, refer
|
||||||
|
* to http://expressjs.com/en/api.html#app.settings.table
|
||||||
|
*/
|
||||||
|
//httpServerOptions: { },
|
||||||
|
|
||||||
|
/** By default, the Node-RED UI is available at http://localhost:1880/
|
||||||
|
* The following property can be used to specify a different root path.
|
||||||
|
* If set to false, this is disabled.
|
||||||
|
*/
|
||||||
|
//httpAdminRoot: '/admin',
|
||||||
|
|
||||||
|
/** The following property can be used to add a custom middleware function
|
||||||
|
* in front of all admin http routes. For example, to set custom http
|
||||||
|
* headers. It can be a single function or an array of middleware functions.
|
||||||
|
*/
|
||||||
|
// httpAdminMiddleware: function(req,res,next) {
|
||||||
|
// // Set the X-Frame-Options header to limit where the editor
|
||||||
|
// // can be embedded
|
||||||
|
// //res.set('X-Frame-Options', 'sameorigin');
|
||||||
|
// next();
|
||||||
|
// },
|
||||||
|
|
||||||
|
/** The following property can be used to set addition options on the session
|
||||||
|
* cookie used as part of adminAuth authentication system
|
||||||
|
* Available options are documented here: https://www.npmjs.com/package/express-session#cookie
|
||||||
|
*/
|
||||||
|
// httpAdminCookieOptions: { },
|
||||||
|
|
||||||
|
/** Some nodes, such as HTTP In, can be used to listen for incoming http requests.
|
||||||
|
* By default, these are served relative to '/'. The following property
|
||||||
|
* can be used to specify a different root path. If set to false, this is
|
||||||
|
* disabled.
|
||||||
|
*/
|
||||||
|
//httpNodeRoot: '/red-nodes',
|
||||||
|
|
||||||
|
/** The following property can be used to configure cross-origin resource sharing
|
||||||
|
* in the HTTP nodes.
|
||||||
|
* See https://github.com/troygoode/node-cors#configuration-options for
|
||||||
|
* details on its contents. The following is a basic permissive set of options:
|
||||||
|
*/
|
||||||
|
//httpNodeCors: {
|
||||||
|
// origin: "*",
|
||||||
|
// methods: "GET,PUT,POST,DELETE"
|
||||||
|
//},
|
||||||
|
|
||||||
|
/** If you need to set an http proxy please set an environment variable
|
||||||
|
* called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system.
|
||||||
|
* For example - http_proxy=http://myproxy.com:8080
|
||||||
|
* (Setting it here will have no effect)
|
||||||
|
* You may also specify no_proxy (or NO_PROXY) to supply a comma separated
|
||||||
|
* list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** The following property can be used to add a custom middleware function
|
||||||
|
* in front of all http in nodes. This allows custom authentication to be
|
||||||
|
* applied to all http in nodes, or any other sort of common request processing.
|
||||||
|
* It can be a single function or an array of middleware functions.
|
||||||
|
*/
|
||||||
|
//httpNodeMiddleware: function(req,res,next) {
|
||||||
|
// // Handle/reject the request, or pass it on to the http in node by calling next();
|
||||||
|
// // Optionally skip our rawBodyParser by setting this to true;
|
||||||
|
// //req.skipRawBodyParser = true;
|
||||||
|
// next();
|
||||||
|
//},
|
||||||
|
|
||||||
|
/** When httpAdminRoot is used to move the UI to a different root path, the
|
||||||
|
* following property can be used to identify a directory of static content
|
||||||
|
* that should be served at http://localhost:1880/.
|
||||||
|
* When httpStaticRoot is set differently to httpAdminRoot, there is no need
|
||||||
|
* to move httpAdminRoot
|
||||||
|
*/
|
||||||
|
httpStatic: "C:\\Users\\zn375\\.node-red\\node_modules\\typicals",
|
||||||
|
/**
|
||||||
|
* OR multiple static sources can be created using an array of objects...
|
||||||
|
* Each object can also contain an options object for further configuration.
|
||||||
|
* See https://expressjs.com/en/api.html#express.static for available options.
|
||||||
|
* They can also contain an option `cors` object to set specific Cross-Origin
|
||||||
|
* Resource Sharing rules for the source. `httpStaticCors` can be used to
|
||||||
|
* set a default cors policy across all static routes.
|
||||||
|
*/
|
||||||
|
//httpStatic: [
|
||||||
|
// {path: '/home/nol/pics/', root: "/img/"},
|
||||||
|
// {path: '/home/nol/reports/', root: "/doc/"},
|
||||||
|
// {path: '/home/nol/videos/', root: "/vid/", options: {maxAge: '1d'}}
|
||||||
|
//],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All static routes will be appended to httpStaticRoot
|
||||||
|
* e.g. if httpStatic = "/home/nol/docs" and httpStaticRoot = "/static/"
|
||||||
|
* then "/home/nol/docs" will be served at "/static/"
|
||||||
|
* e.g. if httpStatic = [{path: '/home/nol/pics/', root: "/img/"}]
|
||||||
|
* and httpStaticRoot = "/static/"
|
||||||
|
* then "/home/nol/pics/" will be served at "/static/img/"
|
||||||
|
*/
|
||||||
|
//httpStaticRoot: '/static/',
|
||||||
|
|
||||||
|
/** The following property can be used to configure cross-origin resource sharing
|
||||||
|
* in the http static routes.
|
||||||
|
* See https://github.com/troygoode/node-cors#configuration-options for
|
||||||
|
* details on its contents. The following is a basic permissive set of options:
|
||||||
|
*/
|
||||||
|
//httpStaticCors: {
|
||||||
|
// origin: "*",
|
||||||
|
// methods: "GET,PUT,POST,DELETE"
|
||||||
|
//},
|
||||||
|
|
||||||
|
/** The following property can be used to modify proxy options */
|
||||||
|
// proxyOptions: {
|
||||||
|
// mode: "legacy", // legacy mode is for non-strict previous proxy determination logic (node-red < v4 compatible)
|
||||||
|
// },
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Runtime Settings
|
||||||
|
* - lang
|
||||||
|
* - runtimeState
|
||||||
|
* - diagnostics
|
||||||
|
* - logging
|
||||||
|
* - contextStorage
|
||||||
|
* - exportGlobalContextKeys
|
||||||
|
* - externalModules
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/** Uncomment the following to run node-red in your preferred language.
|
||||||
|
* Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko
|
||||||
|
* Some languages are more complete than others.
|
||||||
|
*/
|
||||||
|
// lang: "de",
|
||||||
|
|
||||||
|
/** Configure diagnostics options
|
||||||
|
* - enabled: When `enabled` is `true` (or unset), diagnostics data will
|
||||||
|
* be available at http://localhost:1880/diagnostics
|
||||||
|
* - ui: When `ui` is `true` (or unset), the action `show-system-info` will
|
||||||
|
* be available to logged in users of node-red editor
|
||||||
|
*/
|
||||||
|
diagnostics: {
|
||||||
|
/** enable or disable diagnostics endpoint. Must be set to `false` to disable */
|
||||||
|
enabled: true,
|
||||||
|
/** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */
|
||||||
|
ui: true,
|
||||||
|
},
|
||||||
|
/** Configure runtimeState options
|
||||||
|
* - enabled: When `enabled` is `true` flows runtime can be Started/Stopped
|
||||||
|
* by POSTing to available at http://localhost:1880/flows/state
|
||||||
|
* - ui: When `ui` is `true`, the action `core:start-flows` and
|
||||||
|
* `core:stop-flows` will be available to logged in users of node-red editor
|
||||||
|
* Also, the deploy menu (when set to default) will show a stop or start button
|
||||||
|
*/
|
||||||
|
runtimeState: {
|
||||||
|
/** enable or disable flows/state endpoint. Must be set to `false` to disable */
|
||||||
|
enabled: false,
|
||||||
|
/** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */
|
||||||
|
ui: false,
|
||||||
|
},
|
||||||
|
/** Configure the logging output */
|
||||||
|
logging: {
|
||||||
|
/** Only console logging is currently supported */
|
||||||
|
console: {
|
||||||
|
/** Level of logging to be recorded. Options are:
|
||||||
|
* fatal - only those errors which make the application unusable should be recorded
|
||||||
|
* error - record errors which are deemed fatal for a particular request + fatal errors
|
||||||
|
* warn - record problems which are non fatal + errors + fatal errors
|
||||||
|
* info - record information about the general running of the application + warn + error + fatal errors
|
||||||
|
* debug - record information which is more verbose than info + info + warn + error + fatal errors
|
||||||
|
* trace - record very detailed logging + debug + info + warn + error + fatal errors
|
||||||
|
* off - turn off all logging (doesn't affect metrics or audit)
|
||||||
|
*/
|
||||||
|
level: "info",
|
||||||
|
/** Whether or not to include metric events in the log output */
|
||||||
|
metrics: false,
|
||||||
|
/** Whether or not to include audit events in the log output */
|
||||||
|
audit: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Context Storage
|
||||||
|
* The following property can be used to enable context storage. The configuration
|
||||||
|
* provided here will enable file-based context that flushes to disk every 30 seconds.
|
||||||
|
* Refer to the documentation for further options: https://nodered.org/docs/api/context/
|
||||||
|
*/
|
||||||
|
//contextStorage: {
|
||||||
|
// default: {
|
||||||
|
// module:"localfilesystem"
|
||||||
|
// },
|
||||||
|
//},
|
||||||
|
|
||||||
|
/** `global.keys()` returns a list of all properties set in global context.
|
||||||
|
* This allows them to be displayed in the Context Sidebar within the editor.
|
||||||
|
* In some circumstances it is not desirable to expose them to the editor. The
|
||||||
|
* following property can be used to hide any property set in `functionGlobalContext`
|
||||||
|
* from being list by `global.keys()`.
|
||||||
|
* By default, the property is set to false to avoid accidental exposure of
|
||||||
|
* their values. Setting this to true will cause the keys to be listed.
|
||||||
|
*/
|
||||||
|
exportGlobalContextKeys: true,
|
||||||
|
|
||||||
|
/** Configure how the runtime will handle external npm modules.
|
||||||
|
* This covers:
|
||||||
|
* - whether the editor will allow new node modules to be installed
|
||||||
|
* - whether nodes, such as the Function node are allowed to have their
|
||||||
|
* own dynamically configured dependencies.
|
||||||
|
* The allow/denyList options can be used to limit what modules the runtime
|
||||||
|
* will install/load. It can use '*' as a wildcard that matches anything.
|
||||||
|
*/
|
||||||
|
externalModules: {
|
||||||
|
// autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */
|
||||||
|
// autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */
|
||||||
|
// palette: { /** Configuration for the Palette Manager */
|
||||||
|
// allowInstall: true, /** Enable the Palette Manager in the editor */
|
||||||
|
// allowUpdate: true, /** Allow modules to be updated in the Palette Manager */
|
||||||
|
// allowUpload: true, /** Allow module tgz files to be uploaded and installed */
|
||||||
|
// allowList: ['*'],
|
||||||
|
// denyList: [],
|
||||||
|
// allowUpdateList: ['*'],
|
||||||
|
// denyUpdateList: []
|
||||||
|
// },
|
||||||
|
// modules: { /** Configuration for node-specified modules */
|
||||||
|
// allowInstall: true,
|
||||||
|
// allowList: [],
|
||||||
|
// denyList: []
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Editor Settings
|
||||||
|
* - disableEditor
|
||||||
|
* - editorTheme
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/** The following property can be used to disable the editor. The admin API
|
||||||
|
* is not affected by this option. To disable both the editor and the admin
|
||||||
|
* API, use either the httpRoot or httpAdminRoot properties
|
||||||
|
*/
|
||||||
|
//disableEditor: false,
|
||||||
|
|
||||||
|
/** Customising the editor
|
||||||
|
* See https://nodered.org/docs/user-guide/runtime/configuration#editor-themes
|
||||||
|
* for all available options.
|
||||||
|
*/
|
||||||
|
editorTheme: {
|
||||||
|
/** The following property can be used to set a custom theme for the editor.
|
||||||
|
* See https://github.com/node-red-contrib-themes/theme-collection for
|
||||||
|
* a collection of themes to chose from.
|
||||||
|
*/
|
||||||
|
//theme: "",
|
||||||
|
|
||||||
|
/** To disable the 'Welcome to Node-RED' tour that is displayed the first
|
||||||
|
* time you access the editor for each release of Node-RED, set this to false
|
||||||
|
*/
|
||||||
|
//tours: false,
|
||||||
|
|
||||||
|
palette: {
|
||||||
|
/** The following property can be used to order the categories in the editor
|
||||||
|
* palette. If a node's category is not in the list, the category will get
|
||||||
|
* added to the end of the palette.
|
||||||
|
* If not set, the following default order is used:
|
||||||
|
*/
|
||||||
|
//categories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'],
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: {
|
||||||
|
/** To enable the Projects feature, set this value to true */
|
||||||
|
enabled: true,
|
||||||
|
workflow: {
|
||||||
|
/** Set the default projects workflow mode.
|
||||||
|
* - manual - you must manually commit changes
|
||||||
|
* - auto - changes are automatically committed
|
||||||
|
* This can be overridden per-user from the 'Git config'
|
||||||
|
* section of 'User Settings' within the editor
|
||||||
|
*/
|
||||||
|
mode: "manual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
codeEditor: {
|
||||||
|
/** Select the text editor component used by the editor.
|
||||||
|
* As of Node-RED V3, this defaults to "monaco", but can be set to "ace" if desired
|
||||||
|
*/
|
||||||
|
lib: "monaco",
|
||||||
|
options: {
|
||||||
|
/** The follow options only apply if the editor is set to "monaco"
|
||||||
|
*
|
||||||
|
* theme - must match the file name of a theme in
|
||||||
|
* packages/node_modules/@node-red/editor-client/src/vendor/monaco/dist/theme
|
||||||
|
* e.g. "tomorrow-night", "upstream-sunburst", "github", "my-theme"
|
||||||
|
*/
|
||||||
|
// theme: "vs",
|
||||||
|
/** other overrides can be set e.g. fontSize, fontFamily, fontLigatures etc.
|
||||||
|
* for the full list, see https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.IStandaloneEditorConstructionOptions.html
|
||||||
|
*/
|
||||||
|
//fontSize: 14,
|
||||||
|
//fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace",
|
||||||
|
//fontLigatures: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
markdownEditor: {
|
||||||
|
mermaid: {
|
||||||
|
/** enable or disable mermaid diagram in markdown document
|
||||||
|
*/
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
multiplayer: {
|
||||||
|
/** To enable the Multiplayer feature, set this value to true */
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Node Settings
|
||||||
|
* - fileWorkingDirectory
|
||||||
|
* - functionGlobalContext
|
||||||
|
* - functionExternalModules
|
||||||
|
* - functionTimeout
|
||||||
|
* - nodeMessageBufferMaxLength
|
||||||
|
* - ui (for use with Node-RED Dashboard)
|
||||||
|
* - debugUseColors
|
||||||
|
* - debugMaxLength
|
||||||
|
* - debugStatusLength
|
||||||
|
* - execMaxBufferSize
|
||||||
|
* - httpRequestTimeout
|
||||||
|
* - mqttReconnectTime
|
||||||
|
* - serialReconnectTime
|
||||||
|
* - socketReconnectTime
|
||||||
|
* - socketTimeout
|
||||||
|
* - tcpMsgQueueSize
|
||||||
|
* - inboundWebSocketTimeout
|
||||||
|
* - tlsConfigDisableLocalFiles
|
||||||
|
* - webSocketNodeVerifyClient
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/** The working directory to handle relative file paths from within the File nodes
|
||||||
|
* defaults to the working directory of the Node-RED process.
|
||||||
|
*/
|
||||||
|
//fileWorkingDirectory: "",
|
||||||
|
|
||||||
|
/** Allow the Function node to load additional npm modules directly */
|
||||||
|
functionExternalModules: true,
|
||||||
|
|
||||||
|
/** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */
|
||||||
|
functionTimeout: 0,
|
||||||
|
|
||||||
|
/** The following property can be used to set predefined values in Global Context.
|
||||||
|
* This allows extra node modules to be made available with in Function node.
|
||||||
|
* For example, the following:
|
||||||
|
* functionGlobalContext: { os:require('os') }
|
||||||
|
* will allow the `os` module to be accessed in a Function node using:
|
||||||
|
* global.get("os")
|
||||||
|
*/
|
||||||
|
functionGlobalContext: {
|
||||||
|
locationId: "1",
|
||||||
|
uuid: "1",
|
||||||
|
// os:require('os'),
|
||||||
|
},
|
||||||
|
|
||||||
|
/** The maximum number of messages nodes will buffer internally as part of their
|
||||||
|
* operation. This applies across a range of nodes that operate on message sequences.
|
||||||
|
* defaults to no limit. A value of 0 also means no limit is applied.
|
||||||
|
*/
|
||||||
|
//nodeMessageBufferMaxLength: 0,
|
||||||
|
|
||||||
|
/** If you installed the optional node-red-dashboard you can set it's path
|
||||||
|
* relative to httpNodeRoot
|
||||||
|
* Other optional properties include
|
||||||
|
* readOnly:{boolean},
|
||||||
|
* middleware:{function or array}, (req,res,next) - http middleware
|
||||||
|
* ioMiddleware:{function or array}, (socket,next) - socket.io middleware
|
||||||
|
*/
|
||||||
|
//ui: { path: "ui" },
|
||||||
|
|
||||||
|
/** Colourise the console output of the debug node */
|
||||||
|
//debugUseColors: true,
|
||||||
|
|
||||||
|
/** The maximum length, in characters, of any message sent to the debug sidebar tab */
|
||||||
|
debugMaxLength: 1000,
|
||||||
|
|
||||||
|
/** The maximum length, in characters, of status messages under the debug node */
|
||||||
|
//debugStatusLength: 32,
|
||||||
|
|
||||||
|
/** Maximum buffer size for the exec node. Defaults to 10Mb */
|
||||||
|
//execMaxBufferSize: 10000000,
|
||||||
|
|
||||||
|
/** Timeout in milliseconds for HTTP request connections. Defaults to 120s */
|
||||||
|
//httpRequestTimeout: 120000,
|
||||||
|
|
||||||
|
/** Retry time in milliseconds for MQTT connections */
|
||||||
|
mqttReconnectTime: 15000,
|
||||||
|
|
||||||
|
/** Retry time in milliseconds for Serial port connections */
|
||||||
|
serialReconnectTime: 15000,
|
||||||
|
|
||||||
|
/** Retry time in milliseconds for TCP socket connections */
|
||||||
|
//socketReconnectTime: 10000,
|
||||||
|
|
||||||
|
/** Timeout in milliseconds for TCP server socket connections. Defaults to no timeout */
|
||||||
|
//socketTimeout: 120000,
|
||||||
|
|
||||||
|
/** Maximum number of messages to wait in queue while attempting to connect to TCP socket
|
||||||
|
* defaults to 1000
|
||||||
|
*/
|
||||||
|
//tcpMsgQueueSize: 2000,
|
||||||
|
|
||||||
|
/** Timeout in milliseconds for inbound WebSocket connections that do not
|
||||||
|
* match any configured node. Defaults to 5000
|
||||||
|
*/
|
||||||
|
//inboundWebSocketTimeout: 5000,
|
||||||
|
|
||||||
|
/** To disable the option for using local files for storing keys and
|
||||||
|
* certificates in the TLS configuration node, set this to true.
|
||||||
|
*/
|
||||||
|
//tlsConfigDisableLocalFiles: true,
|
||||||
|
|
||||||
|
/** The following property can be used to verify WebSocket connection attempts.
|
||||||
|
* This allows, for example, the HTTP request headers to be checked to ensure
|
||||||
|
* they include valid authentication information.
|
||||||
|
*/
|
||||||
|
//webSocketNodeVerifyClient: function(info) {
|
||||||
|
// /** 'info' has three properties:
|
||||||
|
// * - origin : the value in the Origin header
|
||||||
|
// * - req : the HTTP request
|
||||||
|
// * - secure : true if req.connection.authorized or req.connection.encrypted is set
|
||||||
|
// *
|
||||||
|
// * The function should return true if the connection should be accepted, false otherwise.
|
||||||
|
// *
|
||||||
|
// * Alternatively, if this function is defined to accept a second argument, callback,
|
||||||
|
// * it can be used to verify the client asynchronously.
|
||||||
|
// * The callback takes three arguments:
|
||||||
|
// * - result : boolean, whether to accept the connection or not
|
||||||
|
// * - code : if result is false, the HTTP error status to return
|
||||||
|
// * - reason: if result is false, the HTTP reason string to return
|
||||||
|
// */
|
||||||
|
//},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user