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_idas 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"
}
]

