Phasing out the Node-RED Dynamic ESS implementation

Update! Display is coming along, but it is no replacement yet for the old functionality.

edit2 I’m still on firmware 3.70~40 using vrm-api-0.3.3 /edit

edit 3 Updated to 3.70~47 but somehow I only get hour stats now

TODO:

  • Let the schedule-time be based on the timestamp difference (not hardcoded 15 or 60 minutes)
  • Figure out restrictions
  • Clean up the code

/edit3

edit4

  • Use custom query for 15minute data
  • set vrm_site_id as a flow variable

*/edit4

The compatibility code is coming along nicely and it might work with the old flow but I have not tested it. (I compare SOC with previous hour to decide green_mode=1. The variable names are different now.)

It took me a while to decide on how the data should be structed. For compatibility, the old structure would be nice but that is not really compatible with the data we receive now so I decided to restructure it.

Anyway, /dess shows this and if you dare to try: here are all the node-red nodes that have changes (if I recall..)

Old screenshot, still at 60minutes

[
    {
        "id": "b28dcffd153cf041",
        "type": "inject",
        "z": "735c8e332d71dbc8",
        "g": "e3ee967aca916ead",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "method",
                "v": "get",
                "vt": "str"
            }
        ],
        "repeat": "300",
        "crontab": "",
        "once": true,
        "onceDelay": "1",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 890,
        "y": 140,
        "wires": [
            [
                "286c9bab874b9698"
            ]
        ]
    },
    {
        "id": "bef5a0955c0d986b",
        "type": "inject",
        "z": "735c8e332d71dbc8",
        "g": "8533e36f629ee4dd",
        "name": "VRM site ID",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "12345",
        "payloadType": "num",
        "x": 470,
        "y": 140,
        "wires": [
            [
                "9d282a6a859b27b3"
            ]
        ]
    },
    {
        "id": "9d282a6a859b27b3",
        "type": "change",
        "z": "735c8e332d71dbc8",
        "g": "8533e36f629ee4dd",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "vrm_site_id",
                "pt": "flow",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 650,
        "y": 140,
        "wires": [
            []
        ]
    },
    {
        "id": "44bd1bb2fabd2dc6",
        "type": "inject",
        "z": "735c8e332d71dbc8",
        "g": "e3ee967aca916ead",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "7200",
        "crontab": "",
        "once": true,
        "onceDelay": "0.1",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 890,
        "y": 60,
        "wires": [
            [
                "cfa647392335ac90"
            ]
        ]
    },
    {
        "id": "cfa647392335ac90",
        "type": "vrm-api",
        "z": "735c8e332d71dbc8",
        "g": "e3ee967aca916ead",
        "vrm": "3b4ac6755e086ba7",
        "name": "dynamic-ess-settings",
        "api_type": "installations",
        "idUser": "",
        "users": "",
        "idSite": "{{flow.vrm_site_id}}",
        "installations": "dynamic-ess-settings",
        "attribute": "Bc",
        "stats_interval": "",
        "show_instance": false,
        "stats_start": "",
        "stats_end": "",
        "use_utc": false,
        "gps_start": "",
        "gps_end": "",
        "widgets": "",
        "instance": "",
        "store_in_global_context": false,
        "verbose": false,
        "x": 1100,
        "y": 60,
        "wires": [
            [
                "b03b161c227eb171"
            ]
        ]
    },
    {
        "id": "b03b161c227eb171",
        "type": "change",
        "z": "735c8e332d71dbc8",
        "g": "e3ee967aca916ead",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "dess_options",
                "pt": "flow",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 920,
        "y": 100,
        "wires": [
            [
                "fb359d1ea66c96de"
            ]
        ]
    },
    {
        "id": "286c9bab874b9698",
        "type": "function",
        "z": "735c8e332d71dbc8",
        "g": "e3ee967aca916ead",
        "name": "query",
        "func": "const getStartOfDay = (date) => {\n    const start = new Date(date)\n    start.setHours(0, 0, 0, 0)\n    return Math.floor(start.getTime() / 1000)\n}\n\nconst now = new Date()\n\nconst start = getStartOfDay(now)\n  \nconst endOfTomorrow = new Date(now)\nendOfTomorrow.setDate(endOfTomorrow.getDate() + 1)\nendOfTomorrow.setHours(23, 59, 59, 999)\nconst end = Math.floor(endOfTomorrow.getTime() / 1000)\n\nmsg.query = `installations/${flow.get('vrm_site_id')}/stats?type=dynamic_ess&interval=15mins&start=${start}&end=${end}` \n  \nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1050,
        "y": 140,
        "wires": [
            [
                "a11a5afae1c21eec"
            ]
        ]
    },
    {
        "id": "a11a5afae1c21eec",
        "type": "vrm-api",
        "z": "735c8e332d71dbc8",
        "g": "e3ee967aca916ead",
        "vrm": "3b4ac6755e086ba7",
        "name": "dynamic-ess-schedules",
        "api_type": "installations",
        "idUser": "",
        "users": "",
        "idSite": "{{flow.vrm_site_id}}",
        "installations": "fetch-dynamic-ess-schedules",
        "attribute": "Bg",
        "stats_interval": "15mins",
        "show_instance": false,
        "stats_start": "",
        "stats_end": "",
        "use_utc": false,
        "gps_start": "",
        "gps_end": "",
        "widgets": "",
        "instance": "",
        "store_in_global_context": false,
        "verbose": true,
        "x": 1250,
        "y": 140,
        "wires": [
            [
                "3c9a231cbaf73ca2"
            ]
        ]
    },
    {
        "id": "3c9a231cbaf73ca2",
        "type": "change",
        "z": "735c8e332d71dbc8",
        "g": "e3ee967aca916ead",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "dess2",
                "pt": "flow",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 900,
        "y": 180,
        "wires": [
            [
                "ee41f77e86648873",
                "fb359d1ea66c96de"
            ]
        ]
    },
    {
        "id": "12c722152e382ae8",
        "type": "inject",
        "z": "735c8e332d71dbc8",
        "g": "e3ee967aca916ead",
        "name": "Test parsing",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "true",
        "payloadType": "bool",
        "x": 890,
        "y": 340,
        "wires": [
            [
                "ee41f77e86648873"
            ]
        ]
    },
    {
        "id": "fb359d1ea66c96de",
        "type": "debug",
        "z": "735c8e332d71dbc8",
        "g": "e3ee967aca916ead",
        "name": "debug",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1090,
        "y": 340,
        "wires": []
    },
    {
        "id": "ee41f77e86648873",
        "type": "function",
        "z": "735c8e332d71dbc8",
        "name": "prepare dess variables",
        "func": "const dess = flow.get('dess2')\nconst dess_options = flow.get('dess_options').data\n\nmsg.dess = dess\nmsg.dess_options = dess_options\n\nmsg.options = {\n    \"vrm_id\": dess_options.idSite,\n    \"b_max\": dess_options.batteryCapacity,\n    \"tb_max\": dess_options.batteryChargeLimit,\n    \"fb_max\": dess_options.batteryDischargeLimit,\n    \"tg_max\": dess_options.gridExportLimit,\n    \"fg_max\": dess_options.gridImportLimit,\n    \"b_cycle_cost\": dess_options.batteryCosts,\n    \"buy_price_formula\": dess_options.buyPriceFormula,\n    \"sell_price_formula\": dess_options.sellPriceFormula,\n    \"green_mode_on\": dess_options.isGreenModeOn,\n    \"feed_in_possible\":\"true\",\n    \"feed_in_control_on\":\"false\",\n    \"country\":\"NL\"\n}\n\nfunction transform(input) {\n  const out = {};\n  const records = input && input.records ? input.records : {};\n  const metrics = Object.keys(records);\n\n  // collect all timestamps from metrics that are arrays\n  //const tsSet = new Set();\n  \n  //Object.values(records[metrics[0]]).map(x => tsSet.add(x[0]))\n  for (const m of metrics) {\n    const val = records[m];\n    if (Array.isArray(val)) {\n      for (const pair of val) {\n        //if (Array.isArray(pair) && pair.length > 0) tsSet.add(pair[0]);\n        if (Array.isArray(pair) && pair.length > 0) {\n            if (! out[pair[0]]) {\n                out[pair[0]] = {};   \n                Object.values(metrics).map(x => out[pair[0]][x] = 0);\n            }\n            out[pair[0]][m] = Number(pair[1]).toFixed(2);\n        }\n      }\n    }\n  }\n\n  //const timestamps = Array.from(tsSet).sort((a, b) => a - b);\n  //const timestamps = Array.from(tsSet);\n\n    /**\n  for (const ts of timestamps) {\n    const key = String(ts);\n    out[key] = {};\n    for (const metric of metrics) {\n      const arr = records[metric];\n      let value = \"0\";\n      if (Array.isArray(arr)) {\n        const pair = arr.find(p => Array.isArray(p) && p[0] === ts);\n        if (pair && pair.length > 1) value = String(pair[1]);\n      }\n      out[key][metric] = Number(value).toFixed(3);\n    }\n  }\n  **/\n  \n\n  return out;\n}\n\nmsg.outputdata =  transform(dess)\nlet list_of_timestamps = Object.keys(msg.outputdata).map(x => x)\n//var round = 1000 * 60 * 60//* 15;\nvar round = list_of_timestamps[1] - list_of_timestamps[0];\nmsg.duration = round / 1000\nvar date = new Date()\nvar rounded = Math.floor(date.getTime() / round) * round\n\n\nlet currentIndex = list_of_timestamps.indexOf(String(rounded))\nmsg.current_index = currentIndex\nmsg.currentquarter = rounded\nmsg.list_of_timestamps = list_of_timestamps\nflow.set('dess', msg)\n\n\nconst output = []\n\nif (msg.outputdata && msg.currentquarter && msg.list_of_timestamps) {\n    for (let schedule = 0; schedule <= 3; schedule++) {\n        let timestamp = msg.list_of_timestamps[msg.current_index + schedule]\n      let schedulePick = msg.outputdata[timestamp];\n      output.push({\n        topic: `Schedule ${schedule}`,\n        soc: Number(schedulePick.vrm_soc_plan),\n        feed_in: dess_options.feed_in_possible ? 1 : 0,\n        duration: msg.duration,\n        start: timestamp,\n        restrictions: 0,\n        strategy: dess_options.isGreenModeOn ? 1 : 0\n      })\n    }\n    output.push(msg)\n}\n\n\nreturn output",
        "outputs": 5,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1320,
        "y": 340,
        "wires": [
            [
                "9e407e08deb53fdc"
            ],
            [],
            [],
            [
                "9e407e08deb53fdc"
            ],
            [
                "9e407e08deb53fdc"
            ]
        ]
    },
    {
        "id": "73f166b0d08743a5",
        "type": "function",
        "z": "735c8e332d71dbc8",
        "g": "24ecb66d21e6743e",
        "name": "prepare dess variables",
        "func": "let dess = flow.get('dess')\n\n//dess.options = {}\n//dess.options.b_max = 47\n\nmsg.lastValidUpdate = new Date(flow.get('lastValidUpdate')).toLocaleString()\n\nmsg.dess_mode_text = 'Auto'\n/**\nif (Object.keys(dess.schedule).length == 96) {\n    msg.dess_hours = 96\n} else {\n    msg.dess_hours = 192\n}\n**/\n\n\nmsg.dess_hours = Object.keys(dess.outputdata).length\n\nmsg.scheduleTargets = []\nmsg.table = []\n\n/**\nconst currentDateTime = new Date()\ncurrentDateTime.setMinutes(0, 0, 0)\nconst unixTimestamp = Math.floor(currentDateTime.getTime() / 1000)\nlet currentHour = currentDateTime.getHours()\n\nvar round = 1000 * 60 * 60;\nvar currentQuarter = Math.floor(date.getTime() / round) * round\nlet list_of_timestamps = Object.keys(dess.outputdata).map(x => x)\nlet currentIndex = list_of_timestamps.indexOf(String(currentQuarter))\n**/\n\nlet currentIndex = dess.current_index\nmsg.current_index = currentIndex\n\n/**\nfor (let schedule = 0; schedule <= 4; schedule++) {\n    let schedulePick = currentIndex + schedule\n    if (schedulePick > Object.keys(dess.schedule).length) {\n        schedulePick -= 24\n    }\n    let scheduledDate = currentDateTime;\n    scheduledDate.setHours(currentHour + schedule)\n    msg.table.push({\n        soc: Number((dess.output.SOC[schedulePick]).toFixed(1)),\n        duration: 900,\n        start: scheduledDate.toLocaleString()\n\n    })\n    msg.scheduleTargets.push(\n        Number((dess.output.SOC[schedulePick]).toFixed(1) / 100 * 14),\n    )\n}\n**/\n\nconst formatDate = (date) => {\n    const month = String(date.getMonth()).padStart(2, '0'); // Months are 0-indexed\n    const day = String(date.getDate()).padStart(2, '0');\n    const hour = String(date.getHours()).padStart(2, '0');\n    const minutes = String(date.getMinutes()).padStart(2, '0');\n    //return `'${month}-${day} ${hour}:${minutes}'`;\n    return `'${hour}:${minutes}'`;\n};\n\n\n//let labels = Object.keys(dess.outputdata).map(x => \"'\" + new Date(Number(x)).toLocaleTimeString(\"nl-NL\").substring(0, 17).replace('T', '-').replace(':', '') + \"'\")\nconst datelabels = Object.keys(dess.outputdata).map(x => new Date(Number(x)))\nconst labels = Object.values(datelabels).map(x => formatDate(x))\n\n\nmsg.estimations = {\n    payload: {\n        \"datasets\": [\n            {\n                label: 'Consumption',\n                type: 'line',\n                data: Object.values(dess.outputdata).map(x => x.vrm_consumption_fc),\n                borderColor: \"#FA716F\",\n                fill: false,\n                stepped: 'left',\n                pointRadius: 1,\n                yAxisID: 'y',\n                borderWidth: 1\n            },\n            {\n                label: 'Battery',\n                type: 'line',\n                data: Object.values(dess.outputdata).map(x => dess.options.b_max * x.vrm_soc_plan / 100),\n                borderColor: \"#4790D0\",\n                fill: false,\n                stepped: 'left',\n                pointRadius: 1,\n                yAxisID: 'socpercent',\n                borderWidth: 1\n            },\n            {\n                label: 'PV yield',\n                type: 'line',\n                data: Object.values(dess.outputdata).map(x => x.solar_yield_forecast),\n                borderColor: \"#F7AB3E\",\n                backgroundColor: \"#f8aa3dAA\",\n                fill: true,\n                stepped: 'left',\n                pointRadius: 1,\n                yAxisID: 'y',\n                borderWidth: 1\n            }, {\n                label: 'Schedule targets',\n                type: 'line',\n                data: Array(new Date().getHours()).fill(null).concat(\n                    msg.scheduleTargets\n                ),\n                borderColor: \"#000000\",\n                fill: false,\n                stepped: 'left',\n                pointRadius: 1,\n                yAxisID: 'y',\n                borderWidth: 1\n            }\n        ],\n        \"labels\": labels,\n        \"B_max\": dess.options.b_max\n    }\n}\n\nmsg.schedule = {\n    payload: {\n        \"datasets\": [\n            {\n                label: \"Grid Usage\",\n                data: Object.values(dess.outputdata).map(x =>  x.vrm_to_grid_fc),\n                borderColor: \"#FA716F\",\n                backgroundColor: \"#FA716F\",\n                fill: true\n            },\n            {\n                label: \"Battery usage\",\n                data: Object.values(dess.outputdata).map(x => x.vrm_to_battery_fc),\n                borderColor: \"#4790D0\",\n                backgroundColor: \"#4790D0\",\n                fill: true\n            }],\n        \"labels\": labels,\n    }\n}\n\n\nmsg.dayaheadprices = {\n    payload: {\n        \"datasets\": [\n            {\n                label: \"Buy Price\",\n                type: 'line',\n                data: Object.values(dess.outputdata).map(x => x.deGb),\n                borderColor: \"#FA716F\",\n                backgroundColor: \"#FA716F\",\n                fill: false,\n                stepped: 'left',\n                pointRadius: 1,\n                borderWidth: 1\n            }, {\n                label: \"Sell Price\",\n                type: 'line',\n                data: Object.values(dess.outputdata).map(x => x.deGs),\n                borderColor: \"#8BC964\",\n                backgroundColor: \"#8BC964\",\n                fill: false,\n                stepped: 'left',\n                pointRadius: 1,\n                borderWidth: 1\n            },],\n        \"labels\": labels,\n    }\n}\n\n\nmsg.costs = {\n    payload: {\n        \"datasets\": [\n            {\n                label: \"Grid costs\",\n                data: Object.values(dess.outputdata).map(x => (x.Gc )),\n                borderColor: \"#FA716F\",\n                backgroundColor: \"#FA716F\",\n                fill: true\n            },\n            {\n                label: \"Battery costs\",\n                data: Object.values(dess.outputdata).map(x => x.Bc),\n                borderColor: \"#4790D0\",\n                backgroundColor: \"#4790D0\",\n                fill: true\n            }],\n        \"labels\": labels,\n    }\n}\n\nlet b = Object.values(dess.outputdata).map(x => Number(-x.total_battery_flow));\nlet g = Object.values(dess.outputdata).map(x => -x.total_grid_flow);\nlet C = Object.values(dess.outputdata).map(x => x.total_consumption);\nlet PV = Object.values(dess.outputdata).map(x => x.total_solar_yield);\nlet n = C.map((c, i) => PV[i] - c);\n\nlet to_b = b.map(x => { if (x < 0) { return -x } else { return 0 } });\nlet from_b = b.map(x => { if (x > 0) { return x } else { return 0 } });\nlet to_g = g.map(x => { if (x < 0) { return -x } else { return 0 } });\nlet from_g = g.map(x => { if (x > 0) { return x } else { return 0 } });\n\n//let to_g = Object.values(dess.outputdata).map(x => x.grid_history_to);\n//let from_g = Object.values(dess.outputdata).map(x => x.grid_history_from);\n\nlet met_need = C.map((c, i) => Math.min(c, PV[i]));\n\nlet from_g_to_b = []\nlet from_b_to_g = []\n\nfor (let i = 0; i <= (msg.dess_hours - 1); i++) {\n    if (Math.sign(b[i]) * Math.sign(g[i]) >= 0) {\n        from_b_to_g.push(0)\n        from_g_to_b.push(0)\n    } else if (Math.sign(b[i]) == 1 && Math.sign(n[i]) >= 0) {\n        from_b_to_g.push(b[i])\n        from_g_to_b.push(0)\n    } else if (Math.sign(b[i]) == 1 && Math.sign(n[i]) == -1) {\n        from_b_to_g.push(-g[i])\n        from_g_to_b.push(0)\n    } else if (Math.sign(g[i]) == 1 && Math.sign(n[i]) >= 0) {\n        from_b_to_g.push(0)\n        from_g_to_b.push(g[i])\n    } else if (Math.sign(g[i]) == 1 && Math.sign(n[i]) == -1) {\n        from_b_to_g.push(0)\n        from_g_to_b.push(-b[i])\n    }\n}\n\nfrom_g = from_g.map((x, i) => (x - from_g_to_b[i]).toFixed(3))\nfrom_b = from_b.map((x, i) => (x - from_b_to_g[i]).toFixed(3))\nto_b = to_b.map((x, i) => (x - from_g_to_b[i]).toFixed(3))\nto_g = to_g.map((x, i) => (x - from_b_to_g[i]).toFixed(3))\n\nmsg.energy = {\n    payload: {\n        \"datasets\": [\n            {\n                label: \"Consumption\",\n                type: 'line',\n                yAxisID: 'yleft',\n                data: C,\n                borderColor: \"#1066B1\",\n                backgroundColor: \"#1066B1\",\n                fill: false,\n                stepped: 'middle',\n                borderWidth: 1\n            },\n            {\n                label: \"PV Yield\",\n                type: 'line',\n                yAxisID: 'yleft',\n                data: PV,\n                borderColor: \"#F7AB3E\",\n                backgroundColor: \"#F7AB3E\",\n                fill: false,\n                stepped: 'middle',\n                borderWidth: 1\n            },\n            {\n                label: \"\",\n                type: 'bar',\n                yAxisID: 'yleft',\n                data: met_need,\n                legend: false,\n                borderColor: \"#FFFFFF00\",\n                backgroundColor: \"#FFFFFF00\",\n                fill: true,\n                stack: 1,\n            },\n            {\n                label: \"From Grid\",\n                type: 'bar',\n                yAxisID: 'yleft',\n                data: from_g,\n                borderColor: \"#FA716F\",\n                backgroundColor: \"#FA716F\",\n                barPercentage: 0.95,\n                fill: true,\n                stack: 1,\n            },\n            {\n                label: \"To Grid\",\n                type: 'bar',\n                yAxisID: 'yleft',\n                data: to_g,\n                borderColor: \"#8BC964\",\n                backgroundColor: \"#8BC964\",\n                barPercentage: 0.95,\n                fill: true,\n                stack: 1,\n            },\n            {\n                label: \"From Battery\",\n                type: 'bar',\n                yAxisID: 'yleft',\n                data: from_b,\n                borderColor: \"#4790D0\",\n                backgroundColor: \"#4790D0\",\n                barPercentage: 0.95,\n                fill: true,\n                stack: 1,\n            },\n            {\n                label: \"To Battery\",\n                type: 'bar',\n                yAxisID: 'yleft',\n                data: to_b,\n                borderColor: \"#9683EC\",\n                backgroundColor: \"#9683EC\",\n                barPercentage: 0.95,\n                fill: true,\n                stack: 1,\n            },\n            {\n                label: \"From Battery to Grid\",\n                type: 'bar',\n                yAxisID: 'yleft',\n                data: from_b_to_g,\n                borderColor: \"#4790D0\",\n                backgroundColor: \"#8BC964\",\n                borderWidth: 4,\n                barPercentage: 0.95,\n                fill: true,\n                stack: 1,\n            },\n            {\n                label: \"From Grid to Battery\",\n                type: 'bar',\n                yAxisID: 'yleft',\n                data: from_g_to_b,\n                borderColor: \"#FA716F\",\n                borderWidth: 4,\n                backgroundColor: \"#9683EC\",\n                barPercentage: 0.95,\n                fill: true,\n                stack: 1,\n            },\n            {\n                label: 'Battery',\n                type: 'line',\n                yAxisID: 'yright',\n                data: Object.values(dess.outputdata).map(x => x.vrm_soc_plan) / dess.options.b_max * 100,\n                borderColor: \"#aaaaaa\",\n                backgroundColor: \"#cccccc\",\n                fill: false,\n                stepped: 'left',\n                pointRadius: 0,\n                borderWidth: 1\n            },],\n        \"labels\": labels\n    }\n}\n\nmsg.full_charge_duration_h = flow.get('full_charge_duration_h')\nmsg.full_charge_interval_d = flow.get('full_charge_interval_d')\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 480,
        "y": 420,
        "wires": [
            [
                "9d5eba896d1e357e"
            ]
        ]
    },
    {
        "id": "5850a20550de7345",
        "type": "template",
        "z": "735c8e332d71dbc8",
        "g": "24ecb66d21e6743e",
        "name": "arbitrary NOW line",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "\n     const arbitraryLine = {\n        id: 'arbitraryLine',\n        beforeDraw(chart, args, options){\n            const { \n                ctx, \n                chartArea: { top, right, bottom, left, width, height, margins}, \n                scales: {x, y}\n            } = chart;\n            ctx.save()\n            const d = new Date();\n            let hours = options.hours || 24\n            let currentIndex = options.currentIndex || 0\n            \n            let mp = options.is_half_hour_schedule ? 2 : 1\n            ctx.strokeStyle = options.nowBackgroundColor\n            ctx.fillStyle = options.nowBackgroundColor\n            let widthNow = (width / hours) * mp\n            let offset = (width / hours) * (currentIndex + 0.5)\n\n            ctx.fillRect(left + offset, top, widthNow, height)\n            \n            \n            ctx.strokeStyle = options.pastBackgroundColor\n            ctx.fillStyle = options.pastBackgroundColor\n            \n            ctx.fillRect(left, top, offset, height)\n            ctx.restore()\n            \n        }\n    }\n\n",
        "output": "str",
        "x": 570,
        "y": 460,
        "wires": [
            [
                "2b7e57b278665947"
            ]
        ]
    },
    {
        "id": "9d5eba896d1e357e",
        "type": "template",
        "z": "735c8e332d71dbc8",
        "g": "24ecb66d21e6743e",
        "name": "html page",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"refresh\" content=\"60\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n    <title>Dynamic ESS</title>\n        <link rel=\"stylesheet\" href=\"/dess/style.css\">\n  </head>\n  <body>\n    <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n    <script src=\"/dess/index.js\"></script>\n  \n  <h2>Dynamic ESS - {{ flow.dess.options.vrm_id }} ({{ flow.dess.options.country }}) - {{ dess_mode_text }}</h2>\n\n<!--\n<div class=\"canvas-container\">\n  <div class=\"canvas-pair\">\n    <div id=\"ca\" style=\"height: 30vh; width: 45%;display:inline-block;\">\n      <canvas id=\"estimations\"></canvas>\n    </div>\n\n    <div id=\"cb\" style=\"height: 30vh; width: 45%;margin-left:5%;display:inline-block;\">\n      <canvas id=\"schedule\"></canvas>\n    </div>\n  </div>\n  <div class=\"canvas-pair\">\n    <div id=\"cc\" style=\"height: 30vh; width: 45%;display:inline-block;\">\n      <canvas id=\"dayaheadprices\"></canvas>\n    </div>\n\n    <div id=\"cd\" style=\"height: 30vh; width: 45%;margin-left:5%;display:inline-block;\">\n      <canvas id=\"costs\"></canvas>\n    </div>\n  </div>\n</div>\n-->\n\n    <div id=\"ca\" style=\"height: 30vh;\">\n      <canvas id=\"estimations\"></canvas>\n    </div>\n\n    <div id=\"cb\" style=\"height: 30vh;\">\n      <canvas id=\"schedule\"></canvas>\n    </div>\n  </div>\n  <div class=\"canvas-pair\">\n    <div id=\"cc\" style=\"height: 30vh;\">\n      <canvas id=\"dayaheadprices\"></canvas>\n    </div>\n\n    <div id=\"cd\" style=\"height: 30vh;\">\n      <canvas id=\"costs\"></canvas>\n    </div>\n\n<div id=\"ce\" style=\"height: 30vh;\">\n  <canvas id=\"energy\"></canvas>\n</div>\n\n<script>\n    const estimationschart = new Chart(\n    document.getElementById('estimations'),\n      {\n        type: 'bar',\n      data: {\n        labels: [{{{estimations.payload.labels}}}],\n        datasets: [\n            {{#estimations.payload.datasets}}\n            {\n                label: '{{label}}',\n                type: '{{type}}',\n                yAxisID: '{{yAxisID}}',\n                data: [{{data}}],\n                borderColor: '{{borderColor}}',\n                {{#backgroundColor}}\n                backgroundColor: '{{backgroundColor}}',\n                {{/backgroundColor}}\n                fill: {{fill}},\n                stepped: '{{stepped}}',\n                pointRadius: {{pointRadius}},\n                borderWidth: {{borderWidth}}\n            },\n            {{/estimations.payload.datasets}}\n        ]\n      },\n        options: {\n          maintainAspectRatio: false,\n          title: {\n            display: true,\n            text: ''\n          },\n          legend: {\n            position: 'top',\n            display: true\n          },\n          interaction: {\n            intersect: false,\n            mode: 'index',\n          },      \n          scales: {\n              y: {\n                  position: 'left',\n                  beginAtZero: true,\n                  suggestedMax: 14,\n                  title: {\n                      display: true,\n                      text: \"Energy in Wh\"\n                  }\n              },\n            socpercent: {\n                position: 'right',\n                title: 'Batt charge %',\n                ticks: {\n                    count: 6,\n                }\n            }\n          },\n          plugins: {\n              arbitraryLine: {\n                  pastBackgroundColor: '#eee',\n                  nowBackgroundColor: '#ddd',\n                  offset: 1,\n                  hours: {{ dess_hours }},\n                  currentIndex: {{ current_index }}\n              },\n              title: {\n                  display: true,\n                  text: \"Overview graph\"\n              },\n              subtitle: {\n                  display: true,\n                  text: \"\"\n              },\n              tooltip: {\n                  callbacks: {\n                      label: function(ctx) {\n                let label = ctx.dataset.label || \"\";\n                if (label === 'Battery') {\n                    label += ': ';\n                    label += ctx.parsed.y.toFixed(2)\n                    label += ' ('\n                    label += ((ctx.parsed.y / {{ flow.dess.options.b_max}} ) * 100).toFixed(1)\n                    label += '%)'\n                }\n                else {\n                    label += ': ' + ctx.parsed.y.toFixed(2)\n                }\n                return label;\n                      }\n                  }\n              }\n          }\n        },\n        plugins: [arbitraryLine]\n      }\n    );\n\n  const schedulechart = new Chart(\n    document.getElementById('schedule'),\n    {\n      type: 'bar',\n      data: {\n        labels: [{{{schedule.payload.labels}}}],\n        datasets: [\n            {{#schedule.payload.datasets}}\n            {\n                label: '{{label}}',\n                data: [{{data}}],\n                borderColor: '{{borderColor}}',\n                backgroundColor: '{{backgroundColor}}',\n                fill: {{fill}},\n            },\n            {{/schedule.payload.datasets}}\n        ]\n      },\n      options: {\n        maintainAspectRatio: false,\n        title: {\n          display: true,\n          text: ''\n        },\n        legend: {\n          position: 'top',\n          display: true\n        },\n        interaction: {\n          intersect: false,\n          mode: 'index',\n        },      \n        scales: {\n            y: {\n                beginAtZero: true,\n                title: {\n                    display: true,\n                    text: \"Energy in Wh\"\n                }\n            }\n        },\n        plugins: {\n            arbitraryLine: {\n                pastBackgroundColor: '#EEEEEE',\n                nowBackgroundColor: '#DDDDDD',\n                hours: {{ dess_hours }},\n                currentIndex: {{ current_index }}\n            },\n            title: {\n                display: true,\n                text: \"Schedule graph\"\n            },\n            subtitle: {\n                display: true,\n                text: \"Positive values represent the energy used from the item (opposite for negatives).\"\n            }\n        }\n      },\n      plugins: [arbitraryLine]\n    }\n  );\n\n  const dapchart = new Chart(\n    document.getElementById('dayaheadprices'),\n    {\n      type: 'bar',\n      data: {\n        labels: [{{{dayaheadprices.payload.labels}}}],\n        datasets: [\n            {{#dayaheadprices.payload.datasets}}\n            {\n                label: '{{label}}',\n                type: '{{type}}',\n                data: [{{data}}],\n                borderColor: '{{borderColor}}',\n                backgroundColor: '{{backgroundColor}}',\n                fill: {{fill}},\n                stepped: '{{stepped}}',\n                pointRadius: {{pointRadius}},\n                borderWidth: {{borderWidth}}\n            },\n            {{/dayaheadprices.payload.datasets}}\n        ]\n      },\n      options: {\n        responsive: true,\n        maintainAspectRatio: false,\n        title: {\n          display: true,\n          text: ''\n        },\n        legend: {\n          position: 'top',\n          display: true\n        },\n        interaction: {\n          intersect: false,\n          mode: 'index',\n        },\n        scales: {\n            y: {\n                beginAtZero: true,\n                title: {\n                    display: true,\n                    text: \"Price in ?/kWh\"\n                }\n            }\n        },\n        plugins: {\n            arbitraryLine: {\n                pastBackgroundColor: '#EEEEEE',\n                nowBackgroundColor: '#DDDDDD',\n                offset: 1,\n                hours: {{ dess_hours }},\n                currentIndex: {{ current_index }}\n            },\n            title: {\n                display: true,\n                text: \"Price graph\"\n            },\n            subtitle: {\n                display: true,\n                text: \"Buy & Sell prices take the provider fee, energy tax and VAT into account.\"\n            }\n        }\n      },\n      plugins: [arbitraryLine]\n    }\n  );\n\n  const ucchart = new Chart(\n    document.getElementById('costs'),\n    {\n      type: 'bar',\n      data: {\n        labels: [{{{costs.payload.labels}}}],\n        datasets: [\n            {{#costs.payload.datasets}}\n            {\n                label: '{{label}}',\n                data: [{{data}}],\n                borderColor: '{{borderColor}}',\n                backgroundColor: '{{backgroundColor}}',\n                fill: {{fill}},\n            },\n            {{/costs.payload.datasets}}\n        ]\n      },\n      options: {\n        maintainAspectRatio: false,\n        title: {\n          display: true,\n          text: ''\n        },\n        legend: {\n          position: 'top',\n          display: true\n        },\n        interaction: {\n          intersect: false,\n          mode: 'index',\n        },      \n        scales: {\n            y: {\n                beginAtZero: true,\n                title: {\n                    display: true,\n                    text: \"Cost in ?\"\n                }\n            }\n        },\n        plugins: {\n            arbitraryLine: {\n                pastBackgroundColor: '#EEEEEE',\n                nowBackgroundColor: '#DDDDDD',\n                hours: {{ dess_hours }},\n                currentIndex: {{ current_index }}\n            },\n            title: {\n                display: true,\n                text: \"Costs graph\"\n            }\n        }\n      },\n      plugins: [arbitraryLine]\n    }\n  );\n\n  const enchart = new Chart(\n    document.getElementById('energy'),\n    {\n      type: 'bar',\n      data: {\n        labels: [{{{energy.payload.labels}}}],\n        datasets: [\n            {{#energy.payload.datasets}}\n            {\n                label: '{{label}}',\n                type: '{{type}}',\n                yAxisID: '{{yAxisID}}',\n                data: [{{data}}],\n                borderColor: '{{borderColor}}',\n                backgroundColor: '{{backgroundColor}}',\n                fill: {{fill}},\n                stepped: '{{stepped}}',\n                {{#pointStyle}}\n                pointStyle: {{pointStyle}},\n                {{/pointStyle}}\n                {{#borderWidth}}\n                borderWidth: {{borderWidth}},\n                {{/borderWidth}}\n                {{#stack}}\n                stack: {{stack}},\n                {{/stack}}\n            },\n            {{/energy.payload.datasets}}\n        ]\n      },\n      options: {\n        elements: {\n            point:{\n                radius: 0\n            }\n        },\n        maintainAspectRatio: false,\n        title: {\n          display: true,\n          text: ''\n        },\n        legend: {\n          position: 'top',\n          display: true,\n          labels: {\n              filter: function(item, chart) {\n                return !item.text.includes(\"DISCARD\");\n              }\n          }\n        },\n        interaction: {\n          intersect: false,\n          mode: 'index',\n        },\n        plugins: {\n            arbitraryLine: {\n                pastBackgroundColor: '#EEEEEE',\n                nowBackgroundColor: '#DDDDDD',\n                hours: {{ dess_hours }},\n                currentIndex: {{ current_index }}\n            },\n            title: {\n                display: true,\n                text: \"Energy Graph\"\n            }\n        },\n        scales: {\n            yleft: {\n                position: 'left',\n                ticks: {\n                    count: 6,\n                }\n            },\n            yright: {\n                position: 'right',\n                title: 'Batt charge %',\n                ticks: {\n                    count: 6,\n                }\n            }\n        }\n    },\n      plugins: [arbitraryLine]\n    }\n  );\n\n</script>\n<hr />\n\n\n<div class=\"canvas-container\">\n  <div class=\"canvas-pair\">\n    <div id=\"ca\" style=\"width: 45%;display:inline-block;\">\n    <h3>Dynamic ESS schedules</h3>\n\n    <p>\n    The table shows all inserted Dynamic ESS Schedules.\n    </p>\n    \n    <table class=\"dess-table\">\n      <thead>\n        <tr>\n          <th>Schedule</th>\n          <th>Start</th>\n          <th>Targeted Soc</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr>\n          <td>#0</td>\n          <td>{{table.0.start}}</td>\n          <td>{{table.0.soc}} %</td>\n        </tr>\n        <tr>\n          <td>#1</td>\n          <td>{{table.1.start}}</td>\n          <td>{{table.1.soc}} %</td>\n        </tr>\n        <tr>\n          <td>#2</td>\n          <td>{{table.2.start}}</td>\n          <td>{{table.2.soc}} %</td>\n        </tr>\n        <tr>\n          <td>#3</td>\n          <td>{{table.3.start}}</td>\n          <td>{{table.3.soc}} %</td>\n        </tr>\n      </tbody>\n    </table>\n    \n        </div>\n    <div id=\"ca\" style=\"width: 45%;display:inline-block;\">\n       <h3>Battery balancing settings</h3>\n       <ul>\n           <li>Charging to full every <b>{{ full_charge_interval_d}} day(s)</b></li>\n           <li>Keep full for <b>{{ full_charge_duration_h}} hour(s)</b></li>\n       </ul>\n   \n    </div>\n\n  </div>\n</div>\n\n<p>\n\n  <hr />\n  Last update: {{ lastValidUpdate }}\n\n  </body>\n</html>",
        "output": "str",
        "x": 860,
        "y": 420,
        "wires": [
            [
                "55e1d0d83b367e50"
            ]
        ]
    },
    {
        "id": "9e407e08deb53fdc",
        "type": "debug",
        "z": "735c8e332d71dbc8",
        "name": "debug 36",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1400,
        "y": 520,
        "wires": []
    },
    {
        "id": "9be64feb160da6a7",
        "type": "inject",
        "z": "735c8e332d71dbc8",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "true",
        "payloadType": "bool",
        "x": 1210,
        "y": 520,
        "wires": [
            [
                "2303d194fbe178d4"
            ]
        ]
    },
    {
        "id": "2303d194fbe178d4",
        "type": "function",
        "z": "735c8e332d71dbc8",
        "name": "function 14",
        "func": "msg.dess = flow.get('dess');\nmsg.dess_options = flow.get('dess_options');\n\n\nreturn msg",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1310,
        "y": 600,
        "wires": [
            [
                "9e407e08deb53fdc"
            ]
        ]
    },
    {
        "id": "3b4ac6755e086ba7",
        "type": "config-vrm-api",
        "name": "dess-token"
    }
]
1 Like