before functional changes by codex

This commit is contained in:
znetsixe
2026-02-19 17:37:09 +01:00
parent 6b58dd4bd5
commit 00858eb853
20 changed files with 1982 additions and 3 deletions

View File

@@ -1,3 +1,13 @@
# convert
# monster
Makes unit conversions
Monsternamekast control node for EVOLV.
Primary responsibilities:
- combine measured/manual flow, rain context and schedule context
- predict sampling demand and pulse distribution over sampling window
- enforce bucket/sampling constraints (volume, weight, cooldown)
- emit process fields used by PLC pulse output, report tooling (e.g. Z-Info), and dashboards
Examples:
- `nodes/monster/examples/monster-dashboard.flow.json` (dashboard-ready visualization flow)
- `nodes/monster/examples/monster-api-dashboard.flow.json` (full API orchestration template with placeholder credentials)

31
examples/README.md Normal file
View File

@@ -0,0 +1,31 @@
# Monster Example Flows
Import-ready Node-RED examples for `monster`.
## Files
- `basic.flow.json`
- Purpose: quick-start flow with dashboard charts for key monster outputs.
- `integration.flow.json`
- Purpose: lightweight integration contract example (`registerChild` path).
- `edge.flow.json`
- Purpose: unknown-topic/edge handling smoke example.
- `monster-dashboard.flow.json`
- Purpose: richer dashboard-focused visualization of process output.
- Includes:
- manual flow input
- manual start trigger
- seeded `rain_data` and `monsternametijden`
- parsed report fields (`m3Total`, `m3PerPuls`, `pulse`, `running`)
- `monster-api-dashboard.flow.json`
- Purpose: full orchestration template around `monster` with API paths and dashboard output.
- Includes:
- Open-Meteo weather fetch -> `rain_data`
- Aquon SFTP CSV fetch -> `monsternametijden`
- Z-Info token + import payload builder for `m3Total`/`m3PerPuls`
- dashboard API publish template (Grafana)
- placeholder-only credentials/hosts (`__SET_*__`)
## Notes
- `basic.flow.json` and `monster-dashboard.flow.json` are intentionally API-free.
- `monster-api-dashboard.flow.json` is the full API template variant and must be hardened with environment-backed secrets before production use.
- `ui-chart` uses series by `msg.topic` (`category: "topic"`, `categoryType: "msg"`).

365
examples/basic.flow.json Normal file
View File

@@ -0,0 +1,365 @@
[
{
"id": "monster_basic_tab",
"type": "tab",
"label": "monster basic",
"disabled": false,
"info": "monster basic dashboard example"
},
{
"id": "ui_base_monster_basic",
"type": "ui-base",
"name": "EVOLV Demo",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "page",
"navigationStyle": "default",
"titleBarStyle": "default"
},
{
"id": "ui_theme_monster_basic",
"type": "ui-theme",
"name": "Monster Theme",
"colors": {
"surface": "#ffffff",
"primary": "#4f8582",
"bgPage": "#efefef",
"groupBg": "#ffffff",
"groupOutline": "#d8d8d8"
},
"sizes": {
"density": "default",
"pagePadding": "14px",
"groupGap": "14px",
"groupBorderRadius": "6px",
"widgetGap": "12px"
}
},
{
"id": "ui_page_monster_basic",
"type": "ui-page",
"name": "Monster Basic",
"ui": "ui_base_monster_basic",
"path": "/monster-basic",
"icon": "science",
"layout": "grid",
"theme": "ui_theme_monster_basic",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 1,
"className": ""
},
{
"id": "ui_group_monster_basic_ctrl",
"type": "ui-group",
"name": "Input",
"page": "ui_page_monster_basic",
"width": "6",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "ui_group_monster_basic_obs",
"type": "ui-group",
"name": "Output",
"page": "ui_page_monster_basic",
"width": "12",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "monster_basic_node",
"type": "monster",
"z": "monster_basic_tab",
"name": "monster basic",
"samplingtime": "24",
"minvolume": "5",
"maxweight": "23",
"nominalFlowMin": "1000",
"flowMax": "6000",
"maxRainRef": "10",
"minSampleIntervalSec": "60",
"emptyWeightBucket": "8.3",
"aquon_sample_name": "112150",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "⊥",
"hasDistance": false,
"distance": "",
"x": 710,
"y": 220,
"wires": [
[
"monster_basic_parse"
],
[
"monster_basic_dbg_influx"
],
[
"monster_basic_dbg_parent"
]
]
},
{
"id": "monster_basic_inj_flow",
"type": "inject",
"z": "monster_basic_tab",
"group": "ui_group_monster_basic_ctrl",
"name": "flow 1800 m3/h",
"props": [
{
"p": "payload"
}
],
"repeat": "5",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"payload": "1800",
"payloadType": "num",
"x": 170,
"y": 180,
"wires": [
[
"monster_basic_build_flow"
]
]
},
{
"id": "monster_basic_build_flow",
"type": "function",
"z": "monster_basic_tab",
"name": "build input_q",
"func": "msg.topic='input_q';\nmsg.payload={value:Number(msg.payload),unit:'m3/h'};\nreturn Number.isFinite(msg.payload.value)?msg:null;",
"outputs": 1,
"noerr": 0,
"x": 390,
"y": 180,
"wires": [
[
"monster_basic_node"
]
]
},
{
"id": "monster_basic_inj_start",
"type": "inject",
"z": "monster_basic_tab",
"group": "ui_group_monster_basic_ctrl",
"name": "manual start",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "0.1",
"topic": "i_start",
"payload": "true",
"payloadType": "bool",
"x": 160,
"y": 240,
"wires": [
[
"monster_basic_node"
]
]
},
{
"id": "monster_basic_parse",
"type": "function",
"z": "monster_basic_tab",
"name": "parse output",
"func": "const p=(msg&&msg.payload&&typeof msg.payload==='object')?msg.payload:{};\nconst now=Date.now();\nreturn [\n Number.isFinite(Number(p.q))?{topic:'q_m3h',payload:Number(p.q),timestamp:now}:null,\n Number.isFinite(Number(p.m3Total))?{topic:'m3_total',payload:Number(p.m3Total),timestamp:now}:null,\n Number.isFinite(Number(p.bucketVol))?{topic:'bucket_l',payload:Number(p.bucketVol),timestamp:now}:null,\n Number.isFinite(Number(p.m3PerPuls||p.m3PerPulse))?{topic:'m3_per_pulse',payload:Number(p.m3PerPuls||p.m3PerPulse),timestamp:now}:null,\n {topic:'status',payload:`running=${Boolean(p.running)} | pulse=${Boolean(p.pulse)} | remaining=${Number(p.pulsesRemaining||0)}`}\n];",
"outputs": 5,
"noerr": 0,
"x": 930,
"y": 220,
"wires": [
[
"monster_basic_chart_q"
],
[
"monster_basic_chart_total"
],
[
"monster_basic_chart_bucket"
],
[
"monster_basic_chart_pulse"
],
[
"monster_basic_text_status"
]
]
},
{
"id": "monster_basic_chart_q",
"type": "ui-chart",
"z": "monster_basic_tab",
"group": "ui_group_monster_basic_obs",
"name": "q",
"label": "Flow q (m3/h)",
"order": 1,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"removeOlder": "15",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1170,
"y": 120,
"wires": []
},
{
"id": "monster_basic_chart_total",
"type": "ui-chart",
"z": "monster_basic_tab",
"group": "ui_group_monster_basic_obs",
"name": "m3Total",
"label": "m3Total",
"order": 2,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"removeOlder": "15",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1180,
"y": 180,
"wires": []
},
{
"id": "monster_basic_chart_bucket",
"type": "ui-chart",
"z": "monster_basic_tab",
"group": "ui_group_monster_basic_obs",
"name": "bucket",
"label": "Bucket (L)",
"order": 3,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"removeOlder": "15",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1180,
"y": 240,
"wires": []
},
{
"id": "monster_basic_chart_pulse",
"type": "ui-chart",
"z": "monster_basic_tab",
"group": "ui_group_monster_basic_obs",
"name": "m3PerPuls",
"label": "m3PerPuls",
"order": 4,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"removeOlder": "15",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1190,
"y": 300,
"wires": []
},
{
"id": "monster_basic_text_status",
"type": "ui-text",
"z": "monster_basic_tab",
"group": "ui_group_monster_basic_obs",
"name": "status",
"label": "Status",
"order": 5,
"width": 12,
"height": 1,
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 1170,
"y": 360,
"wires": []
},
{
"id": "monster_basic_dbg_influx",
"type": "debug",
"z": "monster_basic_tab",
"name": "influx output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 930,
"y": 300,
"wires": []
},
{
"id": "monster_basic_dbg_parent",
"type": "debug",
"z": "monster_basic_tab",
"name": "parent output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 920,
"y": 340,
"wires": []
}
]

6
examples/edge.flow.json Normal file
View File

@@ -0,0 +1,6 @@
[
{"id":"monster_edge_tab","type":"tab","label":"monster edge","disabled":false,"info":"monster edge example"},
{"id":"monster_edge_node","type":"monster","z":"monster_edge_tab","name":"monster edge","x":420,"y":180,"wires":[["monster_edge_dbg"]]},
{"id":"monster_edge_inj","type":"inject","z":"monster_edge_tab","name":"unknown topic","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"doesNotExist","payload":"x","payloadType":"str","x":170,"y":180,"wires":[["monster_edge_node"]]},
{"id":"monster_edge_dbg","type":"debug","z":"monster_edge_tab","name":"monster edge debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":660,"y":180,"wires":[]}
]

View File

@@ -0,0 +1,6 @@
[
{"id":"monster_int_tab","type":"tab","label":"monster integration","disabled":false,"info":"monster integration example"},
{"id":"monster_int_node","type":"monster","z":"monster_int_tab","name":"monster integration","x":420,"y":180,"wires":[["monster_int_dbg"]]},
{"id":"monster_int_inj","type":"inject","z":"monster_int_tab","name":"registerChild","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"registerChild","payload":"example-child-id","payloadType":"str","x":170,"y":180,"wires":[["monster_int_node"]]},
{"id":"monster_int_dbg","type":"debug","z":"monster_int_tab","name":"monster integration debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":680,"y":180,"wires":[]}
]

View File

@@ -0,0 +1,743 @@
[
{
"id": "monster_api_tab",
"type": "tab",
"label": "Monster API + Dashboard",
"disabled": false,
"info": "Full monster orchestration example with API integrations. Credentials are placeholders."
},
{
"id": "ui_base_monster_api",
"type": "ui-base",
"name": "EVOLV Demo",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "page",
"navigationStyle": "default",
"titleBarStyle": "default"
},
{
"id": "ui_theme_monster_api",
"type": "ui-theme",
"name": "Monster API Theme",
"colors": {
"surface": "#ffffff",
"primary": "#4f8582",
"bgPage": "#efefef",
"groupBg": "#ffffff",
"groupOutline": "#d8d8d8"
},
"sizes": {
"density": "default",
"pagePadding": "14px",
"groupGap": "14px",
"groupBorderRadius": "6px",
"widgetGap": "12px"
}
},
{
"id": "ui_page_monster_api",
"type": "ui-page",
"name": "Monster API",
"ui": "ui_base_monster_api",
"path": "/monster-api",
"icon": "science",
"layout": "grid",
"theme": "ui_theme_monster_api",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 1,
"className": ""
},
{
"id": "ui_group_monster_api_ctrl",
"type": "ui-group",
"name": "Input",
"page": "ui_page_monster_api",
"width": "6",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "ui_group_monster_api_obs",
"type": "ui-group",
"name": "Output",
"page": "ui_page_monster_api",
"width": "12",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "monster_api_node",
"type": "monster",
"z": "monster_api_tab",
"name": "Monster API",
"samplingtime": "24",
"minvolume": "5",
"maxweight": "23",
"nominalFlowMin": "1000",
"flowMax": "6000",
"maxRainRef": "10",
"minSampleIntervalSec": "60",
"emptyWeightBucket": "8.3",
"aquon_sample_name": "112150",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "⊥",
"hasDistance": false,
"distance": "",
"x": 980,
"y": 320,
"wires": [
[
"monster_api_parse_output",
"monster_api_zinfo_prepare"
],
[
"monster_api_dbg_influx"
],
[
"monster_api_dbg_parent"
]
]
},
{
"id": "monster_api_info",
"type": "comment",
"z": "monster_api_tab",
"name": "Template only: set credentials/URLs before production",
"info": "All secrets in this flow are placeholders. Replace with env vars or credential nodes.",
"x": 260,
"y": 80,
"wires": []
},
{
"id": "monster_api_inj_flow",
"type": "inject",
"z": "monster_api_tab",
"group": "ui_group_monster_api_ctrl",
"name": "Flow 1800 m3/h",
"props": [
{
"p": "payload"
}
],
"repeat": "5",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"payload": "1800",
"payloadType": "num",
"x": 170,
"y": 180,
"wires": [
[
"monster_api_build_flow"
]
]
},
{
"id": "monster_api_build_flow",
"type": "function",
"z": "monster_api_tab",
"name": "Build input_q",
"func": "msg.topic='input_q';\nmsg.payload={value:Number(msg.payload),unit:'m3/h'};\nreturn Number.isFinite(msg.payload.value)?msg:null;",
"outputs": 1,
"noerr": 0,
"x": 390,
"y": 180,
"wires": [
[
"monster_api_node"
]
]
},
{
"id": "monster_api_inj_start",
"type": "inject",
"z": "monster_api_tab",
"group": "ui_group_monster_api_ctrl",
"name": "Manual Start",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "0.1",
"topic": "i_start",
"payload": "true",
"payloadType": "bool",
"x": 160,
"y": 240,
"wires": [
[
"monster_api_node"
]
]
},
{
"id": "monster_api_weather_trigger",
"type": "inject",
"z": "monster_api_tab",
"name": "Weather fetch (daily)",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "55 07 * * *",
"once": false,
"onceDelay": "",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 190,
"y": 420,
"wires": [
[
"monster_api_weather_http"
]
]
},
{
"id": "monster_api_weather_http",
"type": "http request",
"z": "monster_api_tab",
"name": "Open-Meteo",
"method": "GET",
"ret": "txt",
"paytoqs": "ignore",
"url": "https://api.open-meteo.com/v1/forecast?latitude=51.71&longitude=4.81&hourly=precipitation,precipitation_probability&timezone=Europe%2FBerlin&past_days=1&forecast_days=2",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 380,
"y": 420,
"wires": [
[
"monster_api_weather_json"
]
]
},
{
"id": "monster_api_weather_json",
"type": "json",
"z": "monster_api_tab",
"name": "rain_data",
"property": "payload",
"action": "",
"pretty": false,
"x": 550,
"y": 420,
"wires": [
[
"monster_api_weather_topic"
]
]
},
{
"id": "monster_api_weather_topic",
"type": "change",
"z": "monster_api_tab",
"name": "topic rain_data",
"rules": [
{
"t": "set",
"p": "topic",
"pt": "msg",
"to": "rain_data",
"tot": "str"
}
],
"x": 720,
"y": 420,
"wires": [
[
"monster_api_node"
]
]
},
{
"id": "monster_api_aquon_trigger",
"type": "inject",
"z": "monster_api_tab",
"name": "Aquon fetch (daily)",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "15 07 * * *",
"once": false,
"onceDelay": "",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 180,
"y": 500,
"wires": [
[
"monster_api_sftp_get"
]
]
},
{
"id": "monster_api_sftp_get",
"type": "sftp in",
"z": "monster_api_tab",
"sftp": "monster_api_sftp_cfg",
"operation": "get",
"filename": "wsBD_MONSTERNAMETIJDEN.csv",
"localFilename": "./.node-red/node_modules/typicals/monster/config/monsternametijden.csv",
"name": "Aquon schedule",
"x": 380,
"y": 500,
"wires": [
[
"monster_api_file_in"
]
]
},
{
"id": "monster_api_file_in",
"type": "file in",
"z": "monster_api_tab",
"name": "read monsternametijden",
"filename": "./.node-red/node_modules/typicals/monster/config/monsternametijden.csv",
"filenameType": "str",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"allProps": false,
"x": 590,
"y": 500,
"wires": [
[
"monster_api_csv"
]
]
},
{
"id": "monster_api_csv",
"type": "csv",
"z": "monster_api_tab",
"name": "monsternametijden",
"sep": ",",
"hdrin": true,
"hdrout": "all",
"multi": "mult",
"ret": "\\n",
"temp": "SAMPLE_NAME,DESCRIPTION,SAMPLED_DATE,START_DATE,END_DATE",
"skip": "0",
"strings": true,
"include_empty_strings": "",
"include_null_values": "",
"x": 780,
"y": 500,
"wires": [
[
"monster_api_schedule_topic"
]
]
},
{
"id": "monster_api_schedule_topic",
"type": "change",
"z": "monster_api_tab",
"name": "topic monsternametijden",
"rules": [
{
"t": "set",
"p": "topic",
"pt": "msg",
"to": "monsternametijden",
"tot": "str"
}
],
"x": 990,
"y": 500,
"wires": [
[
"monster_api_node"
]
]
},
{
"id": "monster_api_zinfo_prepare",
"type": "function",
"z": "monster_api_tab",
"name": "Z-Info prepare on run stop",
"func": "const p=(msg&&msg.payload&&typeof msg.payload==='object')?msg.payload:{};\nconst runningNow=Boolean(p.running);\nconst runningPrev=Boolean(context.get('runningPrev'));\ncontext.set('runningPrev',runningNow);\nif(!(runningPrev && !runningNow)){\n return null;\n}\nconst today=new Date();\nconst day=String(today.getDate()).padStart(2,'0');\nconst month=String(today.getMonth()+1).padStart(2,'0');\nconst year=today.getFullYear();\nconst yesterdayDate=new Date(today.getTime()-24*3600*1000);\nconst yDay=String(yesterdayDate.getDate()).padStart(2,'0');\nconst yMonth=String(yesterdayDate.getMonth()+1).padStart(2,'0');\nconst yYear=yesterdayDate.getFullYear();\nmsg.zinfoDateFrom=`${yYear}-${yMonth}-${yDay}`;\nmsg.zinfoDateUntil=`${year}-${month}-${day}`;\nmsg.zinfoData={\n m3Total:Number(p.m3Total||0),\n pulse:Math.max(0,Math.floor(Number(p.m3PerPuls||p.m3PerPulse||0)))\n};\nmsg.payload='grant_type=password&username=__SET_ZINFO_USERNAME__&password=__SET_ZINFO_PASSWORD__&client_id=__SET_ZINFO_CLIENT_ID__&client_secret=__SET_ZINFO_CLIENT_SECRET__';\nmsg.headers=msg.headers||{};\nmsg.headers['content-type']='application/x-www-form-urlencoded';\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 1260,
"y": 320,
"wires": [
[
"monster_api_zinfo_token"
]
]
},
{
"id": "monster_api_zinfo_token",
"type": "http request",
"z": "monster_api_tab",
"name": "Z-Info token",
"method": "POST",
"ret": "txt",
"paytoqs": "ignore",
"url": "https://__SET_ZINFO_HOST__/WSR/zi_wsr.svc/token",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 1450,
"y": 320,
"wires": [
[
"monster_api_zinfo_token_json"
]
]
},
{
"id": "monster_api_zinfo_token_json",
"type": "json",
"z": "monster_api_tab",
"name": "token json",
"property": "payload",
"action": "",
"pretty": false,
"x": 1630,
"y": 320,
"wires": [
[
"monster_api_zinfo_import_builder"
]
]
},
{
"id": "monster_api_zinfo_import_builder",
"type": "function",
"z": "monster_api_tab",
"name": "Build Z-Info import",
"func": "const token=msg.payload&&msg.payload.access_token;\nconst z=msg.zinfoData||{};\nconst from=msg.zinfoDateFrom;\nconst until=msg.zinfoDateUntil;\nconst ns='__SET_ZINFO_NAMESPACE__';\nmsg.payload={\n import:{\n algemeen:{\n AanleverendeOrganisatie:'NL.25',\n Versie:'IMm2018',\n Batchid:`ZI_PA_NL.25_${Date.now()}.json`,\n Systeembron:'WBD/NEERSG',\n Systeemdoel:'HWH/Z-info',\n Opmerking:'template'\n },\n data:[{\n Meetwaarden:[\n {mepid:`${ns}.F021.m3`,dbmDtm:from,dbmTijd:'06:00',demDtm:until,demTijd:'06:00',mwdWaarde:`${Number(z.m3Total||0)}`,mwdWaardeAN:'',nMwd:'',mwdOpmerk:'template'},\n {mepid:`${ns}.Q000.PULS`,dbmDtm:from,dbmTijd:'06:00',demDtm:until,demTijd:'06:00',mwdWaarde:`${Number(z.pulse||0)}`,mwdWaardeAN:'',nMwd:'',mwdOpmerk:'template'}\n ]\n }]\n }\n};\nmsg.headers=msg.headers||{};\nif(token){msg.headers.authorization='Bearer '+token;}\nmsg.headers['content-type']='application/json';\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 1830,
"y": 320,
"wires": [
[
"monster_api_zinfo_import_put"
]
]
},
{
"id": "monster_api_zinfo_import_put",
"type": "http request",
"z": "monster_api_tab",
"name": "Z-Info import PUT",
"method": "PUT",
"ret": "txt",
"paytoqs": "ignore",
"url": "https://__SET_ZINFO_HOST__/WSR/zi_wsr.svc/json/NL.25/importmwd/pa/?gebruiker=__SET_ZINFO_USER__",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 2040,
"y": 320,
"wires": [
[
"monster_api_dbg_zinfo"
]
]
},
{
"id": "monster_api_dbg_zinfo",
"type": "debug",
"z": "monster_api_tab",
"name": "Z-Info response",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"targetType": "full",
"x": 2250,
"y": 320,
"wires": []
},
{
"id": "monster_api_parse_output",
"type": "function",
"z": "monster_api_tab",
"name": "Parse output for dashboard",
"func": "const p=(msg&&msg.payload&&typeof msg.payload==='object')?msg.payload:{};\nconst now=Date.now();\nconst m3PerPuls=Number(p.m3PerPuls||p.m3PerPulse);\nreturn [\n Number.isFinite(Number(p.q))?{topic:'q_m3h',payload:Number(p.q),timestamp:now}:null,\n Number.isFinite(Number(p.m3Total))?{topic:'m3_total',payload:Number(p.m3Total),timestamp:now}:null,\n Number.isFinite(Number(p.bucketVol))?{topic:'bucket_l',payload:Number(p.bucketVol),timestamp:now}:null,\n Number.isFinite(m3PerPuls)?{topic:'m3_per_pulse',payload:m3PerPuls,timestamp:now}:null,\n {topic:'status',payload:`running=${Boolean(p.running)} | pulse=${Boolean(p.pulse)} | m3PerPuls=${Number.isFinite(m3PerPuls)?m3PerPuls:'n/a'} | missed=${Number(p.missedSamples||0)}`}\n];",
"outputs": 5,
"noerr": 0,
"x": 1240,
"y": 220,
"wires": [
[
"monster_api_chart_q"
],
[
"monster_api_chart_total"
],
[
"monster_api_chart_bucket"
],
[
"monster_api_chart_pulse"
],
[
"monster_api_text_status"
]
]
},
{
"id": "monster_api_chart_q",
"type": "ui-chart",
"z": "monster_api_tab",
"group": "ui_group_monster_api_obs",
"name": "q",
"label": "Flow q (m3/h)",
"order": 1,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"removeOlder": "30",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1470,
"y": 120,
"wires": []
},
{
"id": "monster_api_chart_total",
"type": "ui-chart",
"z": "monster_api_tab",
"group": "ui_group_monster_api_obs",
"name": "m3Total",
"label": "m3Total",
"order": 2,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"removeOlder": "30",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1480,
"y": 180,
"wires": []
},
{
"id": "monster_api_chart_bucket",
"type": "ui-chart",
"z": "monster_api_tab",
"group": "ui_group_monster_api_obs",
"name": "bucket",
"label": "Bucket (L)",
"order": 3,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"removeOlder": "30",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1480,
"y": 240,
"wires": []
},
{
"id": "monster_api_chart_pulse",
"type": "ui-chart",
"z": "monster_api_tab",
"group": "ui_group_monster_api_obs",
"name": "m3PerPuls",
"label": "m3PerPuls",
"order": 4,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"removeOlder": "30",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1490,
"y": 300,
"wires": []
},
{
"id": "monster_api_text_status",
"type": "ui-text",
"z": "monster_api_tab",
"group": "ui_group_monster_api_obs",
"name": "status",
"label": "Status",
"order": 5,
"width": 12,
"height": 1,
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 1460,
"y": 360,
"wires": []
},
{
"id": "monster_api_dashboardapi",
"type": "dashboardapi",
"z": "monster_api_tab",
"name": "dashboard template",
"x": 1430,
"y": 420,
"wires": [
[
"monster_api_grafana_post"
]
]
},
{
"id": "monster_api_grafana_post",
"type": "http request",
"z": "monster_api_tab",
"name": "Grafana dashboard API",
"method": "POST",
"ret": "txt",
"paytoqs": "ignore",
"url": "https://__SET_GRAFANA_HOST__/api/dashboards/db",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 1650,
"y": 420,
"wires": [
[
"monster_api_dbg_dashboard"
]
]
},
{
"id": "monster_api_dbg_dashboard",
"type": "debug",
"z": "monster_api_tab",
"name": "dashboard API response",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"targetType": "full",
"x": 1870,
"y": 420,
"wires": []
},
{
"id": "monster_api_dbg_influx",
"type": "debug",
"z": "monster_api_tab",
"name": "influx output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 1240,
"y": 460,
"wires": []
},
{
"id": "monster_api_dbg_parent",
"type": "debug",
"z": "monster_api_tab",
"name": "parent output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 1230,
"y": 500,
"wires": []
},
{
"id": "monster_api_sftp_cfg",
"type": "sftp",
"host": "__SET_AQUON_SFTP_HOST__",
"port": "22",
"username": "__SET_AQUON_SFTP_USERNAME__",
"password": "__SET_AQUON_SFTP_PASSWORD__",
"hmac": [],
"cipher": []
}
]

View File

@@ -0,0 +1,483 @@
[
{
"id": "monster_tab_demo",
"type": "tab",
"label": "Monster Dashboard Demo",
"disabled": false,
"info": "Dashboard-focused example for monster output visualization"
},
{
"id": "ui_base_monster_demo",
"type": "ui-base",
"name": "EVOLV Demo",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "page",
"navigationStyle": "default",
"titleBarStyle": "default"
},
{
"id": "ui_theme_monster_demo",
"type": "ui-theme",
"name": "EVOLV Monster Theme",
"colors": {
"surface": "#ffffff",
"primary": "#4f8582",
"bgPage": "#efefef",
"groupBg": "#ffffff",
"groupOutline": "#d8d8d8"
},
"sizes": {
"density": "default",
"pagePadding": "14px",
"groupGap": "14px",
"groupBorderRadius": "6px",
"widgetGap": "12px"
}
},
{
"id": "ui_page_monster_demo",
"type": "ui-page",
"name": "Monster Demo",
"ui": "ui_base_monster_demo",
"path": "/monster-demo",
"icon": "science",
"layout": "grid",
"theme": "ui_theme_monster_demo",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 1,
"className": ""
},
{
"id": "ui_group_monster_ctrl",
"type": "ui-group",
"name": "Monster Inputs",
"page": "ui_page_monster_demo",
"width": "6",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "ui_group_monster_obs",
"type": "ui-group",
"name": "Monster Output",
"page": "ui_page_monster_demo",
"width": "12",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "monster_node_demo",
"type": "monster",
"z": "monster_tab_demo",
"name": "Monster Demo",
"samplingtime": "24",
"minvolume": "5",
"maxweight": "23",
"nominalFlowMin": "1000",
"flowMax": "6000",
"maxRainRef": "10",
"minSampleIntervalSec": "60",
"emptyWeightBucket": "8.3",
"aquon_sample_name": "112150",
"uuid": "",
"supplier": "monster",
"category": "monster",
"assetType": "sampling-cabinet",
"model": "monster-standard",
"unit": "m3/h",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "⊥",
"hasDistance": false,
"distance": "",
"x": 900,
"y": 260,
"wires": [
[
"monster_parse_output"
],
[
"monster_debug_influx"
],
[
"monster_debug_parent"
]
]
},
{
"id": "monster_flow_inject",
"type": "inject",
"z": "monster_tab_demo",
"group": "ui_group_monster_ctrl",
"name": "Flow 1800 m3/h",
"props": [
{
"p": "payload"
}
],
"repeat": "5",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"payload": "1800",
"payloadType": "num",
"x": 170,
"y": 180,
"wires": [
[
"monster_build_flow"
]
]
},
{
"id": "monster_build_flow",
"type": "function",
"z": "monster_tab_demo",
"name": "Build input_q",
"func": "msg.topic = 'input_q';\nmsg.payload = { value: Number(msg.payload), unit: 'm3/h' };\nreturn Number.isFinite(msg.payload.value) ? msg : null;",
"outputs": 1,
"noerr": 0,
"x": 380,
"y": 180,
"wires": [
[
"monster_node_demo"
]
]
},
{
"id": "monster_start_inject",
"type": "inject",
"z": "monster_tab_demo",
"group": "ui_group_monster_ctrl",
"name": "Manual Start",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "0.1",
"topic": "i_start",
"payload": "true",
"payloadType": "bool",
"x": 160,
"y": 240,
"wires": [
[
"monster_node_demo"
]
]
},
{
"id": "monster_rain_inject",
"type": "inject",
"z": "monster_tab_demo",
"group": "ui_group_monster_ctrl",
"name": "Seed rain_data",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "2",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 160,
"y": 300,
"wires": [
[
"monster_build_rain"
]
]
},
{
"id": "monster_build_rain",
"type": "function",
"z": "monster_tab_demo",
"name": "Build rain_data",
"func": "const now = new Date();\nconst mk = (offset, rain, prob) => {\n const d = new Date(now.getTime() + offset * 3600 * 1000);\n return { t: d.toISOString().slice(0, 13) + ':00', rain, prob };\n};\nconst rows = [mk(-1, 0.2, 20), mk(0, 0.8, 40), mk(1, 1.1, 60), mk(2, 0.5, 30)];\nmsg.topic = 'rain_data';\nmsg.payload = [\n {\n latitude: 51.71,\n longitude: 4.81,\n hourly: {\n time: rows.map(r => r.t),\n precipitation: rows.map(r => r.rain),\n precipitation_probability: rows.map(r => r.prob)\n }\n }\n];\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 380,
"y": 300,
"wires": [
[
"monster_node_demo"
]
]
},
{
"id": "monster_schedule_inject",
"type": "inject",
"z": "monster_tab_demo",
"group": "ui_group_monster_ctrl",
"name": "Seed monsternametijden",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "3",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 190,
"y": 360,
"wires": [
[
"monster_build_schedule"
]
]
},
{
"id": "monster_build_schedule",
"type": "function",
"z": "monster_tab_demo",
"name": "Build monsternametijden",
"func": "const now = new Date();\nconst next = new Date(now.getTime() + 24 * 3600 * 1000);\nconst end = new Date(next.getTime() + 24 * 3600 * 1000);\nmsg.topic = 'monsternametijden';\nmsg.payload = [\n {\n SAMPLE_NAME: '112150',\n DESCRIPTION: 'demo schedule',\n SAMPLED_DATE: next.toISOString().slice(0, 19).replace('T', ' '),\n START_DATE: next.toISOString().slice(0, 19).replace('T', ' '),\n END_DATE: end.toISOString().slice(0, 19).replace('T', ' ')\n }\n];\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 410,
"y": 360,
"wires": [
[
"monster_node_demo"
]
]
},
{
"id": "monster_parse_output",
"type": "function",
"z": "monster_tab_demo",
"name": "Parse monster output",
"func": "const p = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst now = Date.now();\nconst q = Number(p.q);\nconst total = Number(p.m3Total);\nconst bucket = Number(p.bucketVol);\nconst rem = Number(p.pulsesRemaining);\nconst m3PerPulse = Number(p.m3PerPuls || p.m3PerPulse);\nconst status = `running=${Boolean(p.running)} | pulse=${Boolean(p.pulse)} | m3PerPuls=${Number.isFinite(m3PerPulse) ? m3PerPulse : 'n/a'} | missed=${Number(p.missedSamples || 0)}`;\nreturn [\n Number.isFinite(q) ? { topic: 'q_m3h', payload: q, timestamp: now } : null,\n Number.isFinite(total) ? { topic: 'm3_total', payload: total, timestamp: now } : null,\n Number.isFinite(bucket) ? { topic: 'bucket_l', payload: bucket, timestamp: now } : null,\n Number.isFinite(rem) ? { topic: 'pulses_remaining', payload: rem, timestamp: now } : null,\n Number.isFinite(m3PerPulse) ? { topic: 'm3_per_pulse', payload: m3PerPulse, timestamp: now } : null,\n { topic: 'status', payload: status }\n];",
"outputs": 6,
"noerr": 0,
"x": 1130,
"y": 260,
"wires": [
[
"monster_chart_q"
],
[
"monster_chart_m3total"
],
[
"monster_chart_bucket"
],
[
"monster_chart_remaining"
],
[
"monster_chart_m3pulse"
],
[
"monster_text_status"
]
]
},
{
"id": "monster_chart_q",
"type": "ui-chart",
"z": "monster_tab_demo",
"group": "ui_group_monster_obs",
"name": "Flow q",
"label": "Flow q (m3/h)",
"order": 1,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisPropertyType": "msg",
"yAxisProperty": "payload",
"removeOlder": "30",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1370,
"y": 120,
"wires": []
},
{
"id": "monster_chart_m3total",
"type": "ui-chart",
"z": "monster_tab_demo",
"group": "ui_group_monster_obs",
"name": "m3 Total",
"label": "m3Total (m3)",
"order": 2,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisPropertyType": "msg",
"yAxisProperty": "payload",
"removeOlder": "30",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1380,
"y": 180,
"wires": []
},
{
"id": "monster_chart_bucket",
"type": "ui-chart",
"z": "monster_tab_demo",
"group": "ui_group_monster_obs",
"name": "Bucket Volume",
"label": "Bucket (L)",
"order": 3,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisPropertyType": "msg",
"yAxisProperty": "payload",
"removeOlder": "30",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1380,
"y": 240,
"wires": []
},
{
"id": "monster_chart_remaining",
"type": "ui-chart",
"z": "monster_tab_demo",
"group": "ui_group_monster_obs",
"name": "Pulses Remaining",
"label": "Pulses Remaining",
"order": 4,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisPropertyType": "msg",
"yAxisProperty": "payload",
"removeOlder": "30",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1400,
"y": 300,
"wires": []
},
{
"id": "monster_chart_m3pulse",
"type": "ui-chart",
"z": "monster_tab_demo",
"group": "ui_group_monster_obs",
"name": "m3 per pulse",
"label": "m3PerPuls",
"order": 5,
"width": 6,
"height": 4,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"xAxisPropertyType": "timestamp",
"yAxisPropertyType": "msg",
"yAxisProperty": "payload",
"removeOlder": "30",
"removeOlderUnit": "60",
"showLegend": false,
"action": "append",
"x": 1390,
"y": 360,
"wires": []
},
{
"id": "monster_text_status",
"type": "ui-text",
"z": "monster_tab_demo",
"group": "ui_group_monster_obs",
"name": "Sampling status",
"label": "Status",
"order": 6,
"width": 12,
"height": 1,
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 1380,
"y": 420,
"wires": []
},
{
"id": "monster_debug_influx",
"type": "debug",
"z": "monster_tab_demo",
"name": "Influx output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 1130,
"y": 320,
"wires": []
},
{
"id": "monster_debug_parent",
"type": "debug",
"z": "monster_tab_demo",
"name": "Parent output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 1130,
"y": 360,
"wires": []
}
]

View File

@@ -4,7 +4,7 @@
"description": "Control module Monsternamekast",
"main": "monster.js",
"scripts": {
"test": "node test/monster.specific.test.js"
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js"
},
"repository": {
"type": "git",

12
test/README.md Normal file
View File

@@ -0,0 +1,12 @@
# monster Test Suite Layout
Required EVOLV layout:
- basic/
- integration/
- edge/
- helpers/
Baseline structure tests:
- basic/structure-module-load.basic.test.js
- integration/structure-examples.integration.test.js
- edge/structure-examples-node-type.edge.test.js

0
test/basic/.gitkeep Normal file
View File

View File

@@ -0,0 +1,27 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const Monster = require('../../src/specificClass');
const { makeMonsterConfig } = require('../helpers/factories');
test('constructor initializes sampling boundaries and target values', () => {
const monster = new Monster(makeMonsterConfig());
assert.equal(monster.maxVolume, 20);
assert.equal(monster.minPuls, Math.round(monster.minVolume / monster.volume_pulse));
assert.equal(monster.absMaxPuls, Math.round(monster.cap_volume / monster.volume_pulse));
assert.ok(monster.targetPuls > 0);
});
test('output contract contains report tooling fields', () => {
const monster = new Monster(makeMonsterConfig());
const output = monster.getOutput();
assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPuls'));
assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPulse'));
assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3Total'));
assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulse'));
assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulsesRemaining'));
assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetDeltaM3'));
assert.ok(Object.prototype.hasOwnProperty.call(output, 'predictedRateM3h'));
});

View File

@@ -0,0 +1,8 @@
const test = require('node:test');
const assert = require('node:assert/strict');
test('monster module load smoke', () => {
assert.doesNotThrow(() => {
require('../../monster.js');
});
});

0
test/edge/.gitkeep Normal file
View File

View File

@@ -0,0 +1,58 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const Monster = require('../../src/specificClass');
const { makeMonsterConfig, withMockedDate } = require('../helpers/factories');
test('invalid flow bounds prevent sampling start', () => {
const monster = new Monster(
makeMonsterConfig({
constraints: {
samplingtime: 1,
minVolume: 5,
maxWeight: 23,
nominalFlowMin: 10,
flowMax: 5,
minSampleIntervalSec: 60,
},
})
);
monster.handleInput('i_start', true);
monster.sampling_program();
assert.equal(monster.invalidFlowBounds, true);
assert.equal(monster.running, false);
assert.equal(monster.i_start, false);
});
test('cooldown guard blocks pulses when flow implies oversampling', () => {
withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
const monster = new Monster(
makeMonsterConfig({
constraints: {
samplingtime: 1,
minVolume: 5,
maxWeight: 23,
nominalFlowMin: 0,
flowMax: 6000,
maxRainRef: 10,
minSampleIntervalSec: 60,
},
})
);
monster.handleInput('input_q', { value: 200, unit: 'm3/h' });
monster.handleInput('i_start', true);
for (let i = 0; i < 80; i++) {
advance(1000);
monster.tick();
}
assert.ok(monster.sumPuls > 0);
assert.ok(monster.bucketVol > 0);
assert.ok(monster.missedSamples > 0);
assert.ok(monster.getSampleCooldownMs() > 0);
});
});

View File

@@ -0,0 +1,21 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const dir = path.resolve(__dirname, '../../examples');
const exampleFlows = [
'basic.flow.json',
'integration.flow.json',
'edge.flow.json',
'monster-dashboard.flow.json',
'monster-api-dashboard.flow.json'
];
test('all example flows include node type monster', () => {
for (const file of exampleFlows) {
const flow = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
const count = flow.filter((n) => n && n.type === 'monster').length;
assert.equal(count >= 1, true, file + ' missing monster node');
}
});

0
test/helpers/.gitkeep Normal file
View File

128
test/helpers/factories.js Normal file
View File

@@ -0,0 +1,128 @@
const fs = require('node:fs');
const path = require('node:path');
const { MeasurementContainer } = require('generalFunctions');
function makeMonsterConfig(overrides = {}) {
return {
general: {
name: 'Monster Test',
logging: { enabled: false, logLevel: 'error' },
},
asset: {
emptyWeightBucket: 3,
},
constraints: {
samplingtime: 1,
minVolume: 5,
maxWeight: 23,
nominalFlowMin: 1000,
flowMax: 6000,
maxRainRef: 10,
minSampleIntervalSec: 60,
},
...overrides,
};
}
function withMockedDate(iso, fn) {
const RealDate = Date;
let now = new RealDate(iso).getTime();
class MockDate extends RealDate {
constructor(...args) {
if (args.length === 0) {
super(now);
} else {
super(...args);
}
}
static now() {
return now;
}
}
global.Date = MockDate;
try {
return fn({
advance(ms) {
now += ms;
},
});
} finally {
global.Date = RealDate;
}
}
function parseMonsternametijdenCsv(filePath) {
const raw = fs.readFileSync(filePath, 'utf8').trim();
const lines = raw.split(/\r?\n/);
const header = lines.shift();
const columns = header.split(',');
return lines
.filter((line) => line && !line.startsWith('-----------'))
.map((line) => {
const parts = [];
let cur = '';
let inQ = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (ch === '"') {
inQ = !inQ;
continue;
}
if (ch === ',' && !inQ) {
parts.push(cur);
cur = '';
} else {
cur += ch;
}
}
parts.push(cur);
const obj = {};
columns.forEach((col, idx) => {
obj[col] = parts[idx];
});
return obj;
});
}
function makeFlowMeasurementChild({
id = 'flow-child-1',
name = 'FlowSensor',
positionVsParent = 'downstream',
unit = 'm3/h',
} = {}) {
const measurements = new MeasurementContainer({
autoConvert: true,
defaultUnits: { flow: 'm3/h' },
});
return {
config: {
general: { id, name, unit },
functionality: { positionVsParent },
asset: { type: 'flow', unit },
},
measurements,
};
}
function loadRainSeed() {
const rainPath = path.join(__dirname, '..', 'seed_data', 'raindataFormat.json');
return JSON.parse(fs.readFileSync(rainPath, 'utf8'));
}
function loadScheduleSeed() {
const csvPath = path.join(__dirname, '..', 'seed_data', 'monsternametijden.csv');
return parseMonsternametijdenCsv(csvPath);
}
module.exports = {
makeMonsterConfig,
withMockedDate,
makeFlowMeasurementChild,
loadRainSeed,
loadScheduleSeed,
};

View File

View File

@@ -0,0 +1,49 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const Monster = require('../../src/specificClass');
const {
makeMonsterConfig,
withMockedDate,
makeFlowMeasurementChild,
loadRainSeed,
loadScheduleSeed,
} = require('../helpers/factories');
test('effective flow uses average of measured and manual flow', () => {
withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
const monster = new Monster(makeMonsterConfig());
const child = makeFlowMeasurementChild({ positionVsParent: 'downstream' });
monster.registerChild(child, 'measurement');
child.measurements
.type('flow')
.variant('measured')
.position('downstream')
.value(60, Date.now(), 'm3/h');
monster.handleInput('input_q', { value: 20, unit: 'm3/h' });
advance(1000);
monster.tick();
assert.equal(monster.q, 40);
});
});
test('rain and schedule payloads update prediction context and next date', () => {
withMockedDate('2024-10-15T00:00:00Z', () => {
const monster = new Monster(makeMonsterConfig());
const rain = loadRainSeed();
const schedule = loadScheduleSeed();
monster.aquonSampleName = '112100';
monster.handleInput('rain_data', rain);
monster.handleInput('monsternametijden', schedule);
assert.ok(monster.avgRain >= 0);
assert.ok(monster.sumRain >= 0);
const nextDate = monster.nextDate instanceof Date ? monster.nextDate.getTime() : Number(monster.nextDate);
assert.ok(Number.isFinite(nextDate));
assert.ok(nextDate > Date.now());
});
});

View File

@@ -0,0 +1,32 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const dir = path.resolve(__dirname, '../../examples');
const requiredFiles = [
'README.md',
'basic.flow.json',
'integration.flow.json',
'edge.flow.json',
'monster-dashboard.flow.json',
'monster-api-dashboard.flow.json'
];
const flowFiles = requiredFiles.filter((file) => file.endsWith('.flow.json'));
function loadJson(file) {
return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
}
test('examples package exists for monster', () => {
for (const file of requiredFiles) {
assert.equal(fs.existsSync(path.join(dir, file)), true, file + ' missing');
}
});
test('example flows are parseable arrays for monster', () => {
for (const file of flowFiles) {
const parsed = loadJson(file);
assert.equal(Array.isArray(parsed), true);
}
});