That look like exactly like the “VRM dynamic ESS node” settings belonging to the standard Node-RED DESS “Dynamic ESS” node. Only difference I see is the extra “Store the response in the global context” tick-box option for the “VRM API” node “Dynamic ESS” API type. And yes both show b_goal SOC and b_goal hour settings but that does not work on VRM API. At least it did not back in the old days (like 3 months back haha). EDIT: and it still does not work for VRM API, just checked.
Can we please bump this @dfaber
Once this is working, all the customer special DESS requests for extra min SOC, max SOC and any other socking SOCs can be solved by a drastically less complicated version of my (rather complicated) hybrid DESS solution (which only true function is to emulate the b_goal SOC and b_goal hour settings). Heck I will even promise to refactor and publish my current operational (hybrid) DESS Trade flow once this gets implemented.
PS, this is what a reasonably performing single phase 40A 230Vac 80kWh MultiPlus II 5000 (+ 8kW HF boost charger) DESS Trade Only system looks like over 90 days:
Why would you say it does not work?
Before: 0/0 (turned off, see screenshot above). NO green mode, I force green mode in hours without expected trading “if (b==0 || soc < 30%) then strategy=green mode (1)"
After:
edit Same for green-mode enabled:
Wait what?
I downloaded your flow.
I see what you do setting flow.dess and I suppose you disabled the Node-RED DESS node (deprecated) replacing it’s output by the output created by the code in the function node “VRM DESS Compatibility”. So far so good, I suppose this makes the https://venus.local:1881/dess/ webpage pick up on the return of VRM-API node instead of that from the Node-RED DESS “Dynamic ESS” node.
But when I set b_goal SOC (100%) and b_goal hour (12) within the VRM-API node settings, the scheduler does not pick up on that at all. It still targets minSOC at the end of the known price schedule: midnight today (before new prices roll in) or midnight next day (after new proces roll in). Only difference that I am aware of is that I did not push the schedule to the webpage renderer like you did (and kept DESS in mode 1 auto). But if the schedule targets 100% at 12:00 I would expect to see that in VRM portal just as well.
What am I missing here?
EDIT: I added the rest of your flow as well, here the results:
VRM:
Node-RED:
Hmm ?
I have indeed deleted the deprecated node and replicate the output of it using the VRM api-node + the 2 extra ones.
You should call the VRM api once using the button on the timestamp inject. (Or it will do it for you within 5 minutes and you just have to be patient). Having dynamic ESS in mode 1(auto/VRM) or 4 (node-red) does not make a difference here. -edit- It actually does make a difference: mode 1 will take over within 5 minutes -/edit-
I agree it should also be visible in automatic mode on VRM but maybe it takes a little longer to see the results there?
And then there is this as well. Now I am royally confused.
It just does not show up in VRM, I will let it run past the whole hour plus a few minutes (that is normally when a new calculated schedule gets calculated and displayed in VRM). Could you check VRM displayes the same graph as the Node-RED webpage at your end?
UPDATE:
I waited to the whole hour and far beyond and can confirm that the b_goal SOC and b-goal hour settings do not work for VRM DESS mode 1 (auto).
And I found something interesting pointing to reason why:
When using VRM-API node to set b_goal SOC (at 100%) and b_goal hour (at 12:00):
-
A) The VRM-API node returns a schedule object that is structured identical to the schedule object returned by the Node-RED DESS node.
This explaines why the “VRM DESS compatibility” function node works for feeding the Node-RED DESS mode 4 implementation, including the https://venus.local:1881/dess/ graphing website that comes with it. -
B) The VRM-API node also sets all the VRM DESS settings.
BUT: The VRM DESS mode 1 (auto) implementation does not use the schedule object that gets returned by the VRM-API node, it uses a completely different schedule that can be read through a call to “installation stats” (also through VRM-API) and is consistent with the schedule as displayed on the VRM DESS page.
The “installation stats” schedule does not take the b_goal SOC and b_goal hour settings into consideration when calculating the schedule, it consistently targets minSOC at the end of the known price window instead.
All this can be clearly seen when selecting “Store the response in the global context?” for both the VRM-API settings call as well as the VRM-API installation stats call, and comparing the resulting SOC schedules saved to the global context.
@dfaber could you please verify these findings to be correct and flag a bug in the “installation stats” results. Please do not remove the b_goal SOC and b_goal hour functionality, instead please make the schedule as presented by calling “installation stats” consistent with the schedule object returned by the VRM-API settings call.
@snowwie Is there a way to change or add to your flow the functionality to take the schedule object returned by the VRM-API settings call and retrospectively feed that back to VRM as the (corrected) schedule to be presented by the VRM-API “installations stats” call, the VRM DESS Dashboard and of course to the actual mode 1 (auto) scheduler on the Cerbo GX / Inverter. That would be very helpfull, even if only as a workaround.
Also please verify your statement here, I suspect you may be in for a surprise: (and if not you, then me)
You are right. Setting it to mode 1, VRM takes over within 5 minutes and sets “real trade mode”, making me draw power from the grid. ![]()
And thank you for the hint on “installation stats”, let me play around with that. I’d love to see VRM and my node-red /dess page in sync some day!
-edit-
The installation stats are probably just that: stats. I cannot post to them even though they seem to show part of today’s schedule. The plot thickens when reviewing the API documentation since there is no mention at all of Dynamic ESS (yet?).
The way I (wish to) understand the OP is the VRM-API node will get you the schedule you want, but it is up to you to actually set and tweak that schedule using node-red (in mode 4). The returned data structure will probably change in the future, invalidating the current flow. Victron does not wish to keep 2 codebases (VRM / custom node) up-to-date so here we are. (eg: 15min prices would give me max 192 schedules instead of the current 48)
Since “one still requires some changes to be up to date as well” I really hope that up-to-dateness is the possibility of updating the VRM graphs and not phasing out mode-4 nor b_goal_soc/hour.
Yes I was afraid it would do just that. Even more worrying is the notion that Victron is planning to phase out the only working option to set even only a single target SOC at a specified hour all together, without first enabling the scheduled SOC targets available through the GUI settings (currently disabled when running DESS). I just don’t understand why they would do that. Basically I’m hoping either to be misunderstanding Victron’s intentions to do so (as per @dfaber remark mentioned above) or to be able to draw the devs attention to the foolishness of such a regression of functionality. I welcome any help to get clarity on this situation, both from Victron staff as well as other DESS end users and community members. Preferably with enough time left to create and test alternative solutions before the 15 minute price scheduling goes life for dynamic pricing here in NL (and other EU countries such as DE as well).
The way I understand it, is that Node-RED DESS will not be updated to function with 15 minute price scheduling. And with that the whole ability to run DESS in mode 4 will cease to work (as-is at least) once the dynamic pricing energy supplier switches over to it. That’s why I started to create that monster of a hybrid DESS Trade flow, to keep DESS actively running in mode 1 (auto) while still using the schedulers ability to calculate an alternative schedule targeted to mode 4 operation (with b_goal SOC and hour functional) and using that alternative schedule to time a charge run when certain conditions are met (price far enough below a running average ánd the alternative mode 4 schedule trying to charge as well).
I got that working good enough to be able to leave the system alone for days on a row but not much longer really. Also not getting clarity on this issue took the wind out of the/my sails to invest more time into it as well to be honest. The only area I didn’t really do a deep dive into is the (alleged) ability of VRM-API to somehow feed a new or modified schedule back to VRM for it to be picked up by the VRM DESS dashboard and actual DESS schedule execution engine in the Cerbo GX/ inverter. Should be under VRM-API “Installations" somewhere if I recall correctly. (EDIT: that would be one or a combination of Installation: Get Dynamic ESS configuration, Modify Dynamic ESS configuration and/or Add Dynamic ESS configuration). Unfortunately the documentation on these VRM-API calls seem to be missing or at least to hard for me to find.
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"
}
]
Did you manage to read 15 minute stats again? On v3.70~47
Yes I did, see my updated flow ![]()
What made the difference? Can you show the API call url (from the debug window, maybe you’d need to enable it in the API node)
The difference is making your own query and use 15mins instead of hours. See the “query” function before calling the dynamic-ess-schedules:
msg.query = `installations/${flow.get('vrm_site_id')}/stats?type=dynamic_ess&interval=15mins&start=${start}&end=${end}`
Yep I just read it. All I can say to that is: What the freaking duck are those devs doing sending everybody on wild goose chase constructing undocumented querys? This isn’t funny anymore, really isn’t. I don’t even want to list all that went wrong here, the headlights of this trainwreck were brightly shining half a year ago already, from the day of the announcement to phase out Node-RED DESS (May 2).
Relax ¯\_(ツ)_/¯
For a beta implementation I think they are doing quite alright. I do not know if the old, deprecated, node still works but it would not surprise me if it does.
This is us, testing and playing with how Victron sees the future and they will definitely love our constructive feedback. In a beta things will not always function as expected and all you can do is deal with it. (Or fork it and make everything yourself ;-). I love open source!)
I respectfully disagree, waisted 5 months to prepare despite multiple requests to leave some time for testing before 15m pricing starts. Being distracted by the big VRM update hardly aware that a few days later half of Europe will roll out 15m pricing, until I asked about it the day before, making panic chances breaking the old API, forgetting to mention the API change in the change log, breaking the reversal of the broken API, breaking the new API endpoint VRM-API Node not to function as stated in the change log, needing to build undocumented custom querys to bypass the broken API node to access the new API endpoint, Not responding to messages that the API and Node are still broken and not testing/verifying whether the new changes mentioned in the change log actually work as stated / intended. That is hardly an incident, let alone a business as usual kind of thing. IMHO, that’s waisting time and resources on both sides of the aisle.
Well, I figurred it out.
This is what the VRM API Node says:
Check the note:
The API always returns buckets of 15 minute intervals. If you have hourly prices configured for your site, the buckets will contain the same price for 4 records in a row. The records returned will contain data from the beginning of today until the end of tomorrow (if the prices are in, else until the end of today).
This is clearly incorrect, here is the url this call produces:
“https://vrmapi.victronenergy.com/v2/installations/xxxxxx/stats?type=dynamic_ess&interval=hours&start=1761004800&end=1761177599”
interval, start and end values are incorrect interval should have been “15mins” and the start and end time seems to be UTC
Then inserting your query function (slightly adapted to my flow) between the inject node and the VRM-API node produces the correct url:
“https://vrmapi.victronenergy.com/v2/installations/xxxxxx/stats?type=dynamic_ess&interval=15mins&start=1760997600&end=1761170399”
Fun fact, if I call the original installation stats set to 15 minutes:
The url will still use hours instead of 15mins:
“https://vrmapi.victronenergy.com/v2/installations/xxxxxx/stats?type=dynamic_ess&interval=hours&start=1760997600&end=1761170399”
But if I insert the same query function again, it also produces the correct url:
“https://vrmapi.victronenergy.com/v2/installations/xxxxxx/stats?type=dynamic_ess&interval=15mins&start=1760997600&end=1761170399”
So now I truly wonder why VRM-API had to be changed at all when all we actually needed was the correct API documentation on how to construct the query. Or even better, installation stats actually using the interval selected in the node, as could reasonably expected, why else have it selectable in the first place.
And I hear you and generally speaking agree when you say that:
But this has clearly been a botch job on Victron’s end, with all due respect for their dev’s being extremely time constraint and all, not the dev’s fault but management decision on priorities. But between you and myself we must have spend a multitude of valuable professional engineering hours to figure this all out compared to the time it took a Victron employee to break it. And I do not know about you but I do value my time running a single person start-up trying to make ends meet. The idea that this is an acceptable normal way for us (community beta testers) to cooperate with Victron towards a common goal really does not sit well with me. Victron could at least man up and admit they did botch this job up and I would not have any problems accepting compensation for doing what is basically their job , send us a couple of multiplusses of choice for our troubles would go long way towards making this worth our/my contribution to the good cause. Doing this for free to counter the lack of any regression testing at Victron’s end is not a sustainable cooperation model.
Final words: “If it ain’t broken, don’t fix it”
Here the slightly modified query function:
const getStartOfDay = (date) => {
const start = new Date(date)
start.setHours(0, 0, 0, 0)
return Math.floor(start.getTime() / 1000)
}
const now = new Date()
const start = getStartOfDay(now)
const endOfTomorrow = new Date(now)
endOfTomorrow.setDate(endOfTomorrow.getDate() + 1)
endOfTomorrow.setHours(23, 59, 59, 999)
const end = Math.floor(endOfTomorrow.getTime() / 1000)
const siteId = flow.get('siteId')
msg.method = 'get'
msg.url = 'https://vrmapi.victronenergy.com/v2'
msg.query = `installations/${siteId}/stats?type=dynamic_ess&interval=15mins&start=${start}&end=${end}`
msg.topic = 'installations fetch-dynamic-ess-schedules'
return msg;
And flow:
[
{
"id": "cef9dffd6d2982d6",
"type": "group",
"z": "bf1f8a77236fa165",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"b660b2324cc8eb6f",
"013ab2c69f508dcd",
"49d176854b3e2b4c",
"c19d2568d02b223b",
"b01e44bd4918434b",
"4d19309a45dded93"
],
"x": 74,
"y": 79,
"w": 392,
"h": 202
},
{
"id": "b660b2324cc8eb6f",
"type": "debug",
"z": "bf1f8a77236fa165",
"g": "cef9dffd6d2982d6",
"name": "msg object",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "auto",
"x": 350,
"y": 240,
"wires": []
},
{
"id": "013ab2c69f508dcd",
"type": "vrm-api",
"z": "bf1f8a77236fa165",
"g": "cef9dffd6d2982d6",
"vrm": "cb2c784a33130f48",
"name": "",
"api_type": "installations",
"idUser": "",
"users": "",
"idSite": "{{flow.siteId}}",
"installations": "fetch-dynamic-ess-schedules",
"attribute": "Bc",
"stats_interval": "",
"show_instance": false,
"stats_start": "",
"stats_end": "",
"use_utc": false,
"gps_start": "",
"gps_end": "",
"widgets": "",
"instance": "",
"store_in_global_context": true,
"verbose": true,
"x": 270,
"y": 200,
"wires": [
[
"b660b2324cc8eb6f"
]
]
},
{
"id": "49d176854b3e2b4c",
"type": "function",
"z": "bf1f8a77236fa165",
"g": "cef9dffd6d2982d6",
"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\nconst siteId = flow.get('siteId')\n\nmsg.method = 'get'\nmsg.url = 'https://vrmapi.victronenergy.com/v2'\nmsg.query = `installations/${siteId}/stats?type=dynamic_ess&interval=15mins&start=${start}&end=${end}`\nmsg.topic = 'installations fetch-dynamic-ess-schedules'\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 390,
"y": 160,
"wires": [
[
"013ab2c69f508dcd"
]
]
},
{
"id": "c19d2568d02b223b",
"type": "inject",
"z": "bf1f8a77236fa165",
"g": "cef9dffd6d2982d6",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "0.1",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 180,
"y": 160,
"wires": [
[
"49d176854b3e2b4c"
]
]
},
{
"id": "b01e44bd4918434b",
"type": "function",
"z": "bf1f8a77236fa165",
"g": "cef9dffd6d2982d6",
"name": "set flow siteId",
"func": "flow.set('siteId', msg.payload)\nreturn\n// use : {{flow.siteId}} ",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 360,
"y": 120,
"wires": [
[]
]
},
{
"id": "4d19309a45dded93",
"type": "inject",
"z": "bf1f8a77236fa165",
"g": "cef9dffd6d2982d6",
"name": "VRM Site ID",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "siteId",
"payload": "123456",
"payloadType": "num",
"x": 190,
"y": 120,
"wires": [
[
"b01e44bd4918434b"
]
]
},
{
"id": "cb2c784a33130f48",
"type": "config-vrm-api",
"name": "VRM"
}
]
Weird, my vrm-api node does not show the “Dynamic ESS (deprecated)” item!
I am on 3.70~49 and node-red says the vrm-api node is version 3.6.0. And if I look at the vrm-api.html file on the cerbo it does have this entry.
Am I missing something? Some debug or ‘allow deprecated’ setting somewhere?
See below, the “Solar Yield Forecast” really is the last item.












