before functional changes by codex
This commit is contained in:
14
README.md
14
README.md
@@ -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
31
examples/README.md
Normal 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
365
examples/basic.flow.json
Normal 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
6
examples/edge.flow.json
Normal 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":[]}
|
||||
]
|
||||
6
examples/integration.flow.json
Normal file
6
examples/integration.flow.json
Normal 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":[]}
|
||||
]
|
||||
743
examples/monster-api-dashboard.flow.json
Normal file
743
examples/monster-api-dashboard.flow.json
Normal 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": []
|
||||
}
|
||||
]
|
||||
483
examples/monster-dashboard.flow.json
Normal file
483
examples/monster-dashboard.flow.json
Normal 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": []
|
||||
}
|
||||
]
|
||||
@@ -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
12
test/README.md
Normal 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
0
test/basic/.gitkeep
Normal file
27
test/basic/constructor.basic.test.js
Normal file
27
test/basic/constructor.basic.test.js
Normal 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'));
|
||||
});
|
||||
8
test/basic/structure-module-load.basic.test.js
Normal file
8
test/basic/structure-module-load.basic.test.js
Normal 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
0
test/edge/.gitkeep
Normal file
58
test/edge/sampling-guards.edge.test.js
Normal file
58
test/edge/sampling-guards.edge.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
21
test/edge/structure-examples-node-type.edge.test.js
Normal file
21
test/edge/structure-examples-node-type.edge.test.js
Normal 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
0
test/helpers/.gitkeep
Normal file
128
test/helpers/factories.js
Normal file
128
test/helpers/factories.js
Normal 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,
|
||||
};
|
||||
0
test/integration/.gitkeep
Normal file
0
test/integration/.gitkeep
Normal file
49
test/integration/flow-and-schedule.integration.test.js
Normal file
49
test/integration/flow-and-schedule.integration.test.js
Normal 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());
|
||||
});
|
||||
});
|
||||
32
test/integration/structure-examples.integration.test.js
Normal file
32
test/integration/structure-examples.integration.test.js
Normal 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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user