Victron Cerbo - Set Max Charge Current based on forecast.solar

This flow grabs the forecast.solar data, calculates some data and adjusts the Max Charge Current of the Victron Cerbo with the goal, to charge the battery mainly with the Victron Solarcharger, if possible.

The Max Charge Current is additionally limited, in case of high JK BMS temperature, high voltage, reaching absorption voltage, maximum cell voltage.

The limits can be adjusted the flows environment variables page.

The flow can be easily adapted if another BMS is used or other requirements must be taken into account.

IMPORTANT: Use this Node-RED flow at your own risk; no warranty or liability is provided.

[{"id":"93f485839dfa27a8","type":"subflow","name":"Solar forecast","info":"This subflow uses the http request node to fetch solar forecasts for geographical positions, using the API from https://forecast.solar/. Please check their website and consider getting a paid account.\n\nDo note that, on a free account, you are limited in the number of requests to do. Also note that the data only gets updated once every 15 minutes, so there is no reason to query more often. There is rate limiting built in the subflow not to perform requests more than once every 15 minutes.\n\n# Configuration\n\nIt uses the parameters as described on: http://doc.forecast.solar/doku.php\n\n - `:apikey` - personal API key for registered users\n - `:lat` - latitude of location, -90 (south) … 90 (north); Internal precission is 0.0001 or abt. 10 m\n - `:lon` - longitude of location, -180 (west) … 180 (east); Internal precission is 0.0001 or abt. 10 m\n - `:dec` - plane declination, 0 (horizontal) … 90 (vertical); Internal precission is integer\n - `:az` - plane azimuth, -180 … 180 (-180 = north, -90 = east, 0 = south, 90 = west, 180 = north); Internal precission is integer\n - `:kwp` - installed modules power in kilo watt peak (kWp)\n\nYou can choose between 3 different type of requests. Note that only `estimate` is available on the free plan.\n\n- `estimate` - this is the forecasted estimate that your panels should produce (given the right parameter settings)\n- `history` - historycal data\n- `clear sky` - estimate given if there would be a clear sky tomorrow\n\nIn case of estimates, one of the following options can be selected:\n- `watts` - Watts (power) average for the period\n- `watthours/period` - Watt hours (energy) for the period\n- `watthours` - Watt hours (energy) summarized over the day\n- `watthours/day` - Watt hours (energy) summarized for each day\n\nFor the graph output there are some extra settings available:\n\n- _Output in kWh_ - when checked output can be set to kWh instead of Wh\n- _Show todays forecast_ - whether or not to include todays forecast\n- _Days to forecast_ - the number of days to forecast (excluding today). Note that you can not get more days forecasted than your API key allows.\n- _Widen graph_ - widen the graph to only show non-zero values\n- _Show day instead of date_ - Show the day instead of the date in the series and labels\n\nThe optional _horizon_ field can be filled out in case an object blocks\nyour solar panels from the sun. See the description [here](https://doc.forecast.solar/horizon)\non what numbers to fill out.\nLeave it empty if you have no objects blocking your panels.\n\n# Input \n\nThe input is for triggering the solar forecast request. \nIt triggers when injecting a message into the node.\n\n# Output\n\nThere are two outputs. The first output is an object with the result and a status message stored into the `msg.payload`.\n\nMost important is the `msg.payload.result`, which contains the estimated production of the panels. E.g.:\n\n```\npayload: object\n  result: object\n    2022-11-28: 23\n    2022-11-29: 35\n```\n\nThe `msg.payload.message` gives information on how successful the query was, the exitcode of the query and the status of the rate limit (how many queries you have left).\n\nThe **second** output can be directly linked to a line or a bar chart, quickly giving a once-glance overview for the predicted forecast.\n\n# Status\n\nInitially the status of the note will be a blue dot, showing \"_Unknown limit_\", as it is unaware of the set ratelmits. After the first request, the returned ratelimit will be put in the text in the form of `remaining/limit`. If more than half the limit is remaining, the dot will be green. If less then half the limit is remaining, the dot will be yellow. If no limit is left, the dot will turn red.\nPlease keep in mind that the ratelimit will be reset after one hour, so you can send a new request after that hour.\n\nIf something is wrong in the API request, the dot will turn red\nand the message will contain the msg.payload with the error. This\nhappens typically when the API is temporally down for maintenance.","category":"","in":[{"x":240,"y":100,"wires":[{"id":"c8dc6aa14b9f3e92"}]}],"out":[{"x":760,"y":260,"wires":[{"id":"2f42837904c91d73","port":0},{"id":"fcc8d69a3ab88e6d","port":0}]},{"x":770,"y":340,"wires":[{"id":"5b0a430fb61e70e7","port":0}]}],"env":[{"name":"latitude","type":"num","value":"51.3","ui":{"icon":"font-awesome/fa-location-arrow","label":{"en-US":"Latitude"},"type":"input","opts":{"types":["num"]}}},{"name":"longitude","type":"num","value":"5.6","ui":{"icon":"font-awesome/fa-location-arrow","label":{"en-US":"Longitude"},"type":"input","opts":{"types":["num"]}}},{"name":"declination","type":"num","value":"37","ui":{"icon":"font-awesome/fa-chevron-up","label":{"en-US":"Declination"},"type":"input","opts":{"types":["num"]}}},{"name":"azimuth","type":"num","value":"0","ui":{"icon":"font-awesome/fa-compass","label":{"en-US":"Azimuth"},"type":"spinner","opts":{"min":-180,"max":180}}},{"name":"modules power","type":"num","value":"1","ui":{"icon":"font-awesome/fa-power-off","label":{"en-US":"Modules power (kWp)"},"type":"input","opts":{"types":["num"]}}},{"name":"apikey","type":"cred","ui":{"icon":"font-awesome/fa-key","label":{"en-US":"API key"},"type":"input","opts":{"types":["cred"]}}},{"name":"type","type":"str","value":"estimate","ui":{"label":{"en-US":"Type"},"type":"select","opts":{"opts":[{"l":{"en-US":"Estimate"},"v":"estimate"},{"l":{"en-US":"History"},"v":"history"},{"l":{"en-US":"Clear sky"},"v":"clearsky"}]}}},{"name":"watt","type":"str","value":"watts","ui":{"icon":"font-awesome/fa-question-circle-o","label":{"en-US":"Watt"},"type":"select","opts":{"opts":[{"l":{"en-US":"Watts (power) average for the period"},"v":"watts"},{"l":{"en-US":"Watt hours (energy) for the period"},"v":"watthours/period"},{"l":{"en-US":"Watt hours (energy) summarized over the day"},"v":"watthours"},{"l":{"en-US":"Watt hours (energy) summarized for each day"},"v":"watthours/day"}]}}},{"name":"kwhoutput","type":"bool","value":"false","ui":{"label":{"en-US":"Output in kWh (in the graph)"},"type":"checkbox"}},{"name":"showtoday","type":"bool","value":"true","ui":{"label":{"en-US":"Show todays forecast"},"type":"checkbox"}},{"name":"daystoforecast","type":"str","value":"-1","ui":{"label":{"en-US":"Days to forecast"},"type":"select","opts":{"opts":[{"l":{"en-US":"Max"},"v":"-1"},{"l":{"en-US":"0"},"v":"0"},{"l":{"en-US":"1"},"v":"1"},{"l":{"en-US":"2"},"v":"2"},{"l":{"en-US":"3"},"v":"3"},{"l":{"en-US":"4"},"v":"4"},{"l":{"en-US":"5"},"v":"5"},{"l":{"en-US":"6"},"v":"6"}]}}},{"name":"widengraph","type":"bool","value":"true","ui":{"label":{"en-US":"Widen graph"},"type":"checkbox"}},{"name":"showday","type":"bool","value":"false","ui":{"label":{"en-US":"Show day instead of date"},"type":"checkbox"}},{"name":"horizon","type":"str","value":"","ui":{"icon":"font-awesome/fa-tree","label":{"en-US":"(optional) horizon"},"type":"input","opts":{"types":["str"]}}}],"meta":{"module":"Solar Forecast","version":"0.0.14","author":"[email protected]","desc":"Get solar forecasting per location","keywords":"solar,forecast,api","license":"GPL-3.0"},"color":"#FFCC66","inputLabels":["trigger"],"outputLabels":["output","graph"],"icon":"font-awesome/fa-sun-o","status":{"x":680,"y":560,"wires":[{"id":"1bfc1cde3ee94e4b","port":0},{"id":"a798fbe66cf133d5","port":0}]}},{"id":"c706820c0d61f023","type":"http request","z":"93f485839dfa27a8","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":390,"y":180,"wires":[["1b5ccaa05d54f7c3"]]},{"id":"b9488734852cd0ca","type":"function","z":"93f485839dfa27a8","name":"create forecast.solar url","func":"msg.url = 'https://api.forecast.solar/';\n\n//if (env.get('apikey')) {\n//    msg.url += env.get('apikey') + '/';\n//    }\n\nmsg.url += env.get('type') + '/';\n\nmsg.url += env.get('watt') + '/';\n\nmsg.url += env.get('latitude') + '/' +\n           env.get('longitude') + '/' +\n           env.get('declination') + '/' +\n           env.get('azimuth') + '/' +\n           env.get('modules power');\n\nif (env.get('horizon')) {\n    msg.url += '?horizon=' + env.get('horizon')\n}\nmsg.topic = 'solar forecast: '+(env.get('type') || '');\nmsg.topic += (' '+env.get('watt') || '');\nif (env.get('kwhoutput')) {\n    msg.topic += ' (kWh)';\n}\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":630,"y":100,"wires":[["975daf96f15cfb61"]]},{"id":"1b5ccaa05d54f7c3","type":"json","z":"93f485839dfa27a8","name":"Convert to json","property":"payload","action":"","pretty":false,"x":680,"y":180,"wires":[["e718a22973cc2864"]]},{"id":"559391d1288f762a","type":"function","z":"93f485839dfa27a8","name":"update ratelimit","func":"var remaining = msg.payload.message.ratelimit.remaining || 0;\nvar limit = msg.payload.message.ratelimit.limit;\n\nflow.set('forecast.solar.ratelimit.remaining', remaining)\nflow.set('forecast.solar.ratelimit.limit', limit)\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":480,"wires":[["e56826252134b93a"]]},{"id":"e718a22973cc2864","type":"link out","z":"93f485839dfa27a8","name":"link out 1","mode":"link","links":["3fa24f2d08195961","0a20e852662c8cec"],"x":815,"y":180,"wires":[]},{"id":"3fa24f2d08195961","type":"link in","z":"93f485839dfa27a8","name":"link in 1","links":["e718a22973cc2864"],"x":385,"y":480,"wires":[["559391d1288f762a"]]},{"id":"0a20e852662c8cec","type":"link in","z":"93f485839dfa27a8","name":"link in 2","links":["e718a22973cc2864"],"x":225,"y":260,"wires":[["fcc8d69a3ab88e6d"]]},{"id":"4734b6f403e1f03e","type":"inject","z":"93f485839dfa27a8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":530,"y":440,"wires":[["e56826252134b93a"]]},{"id":"1bfc1cde3ee94e4b","type":"function","z":"93f485839dfa27a8","name":"update status","func":"var remaining = flow.get('forecast.solar.ratelimit.remaining') || -1;\nvar limit = flow.get('forecast.solar.ratelimit.limit') || -1\n\nvar text = remaining.toString() + '/' + limit.toString();\nvar fill = \"green\";\n\nif (remaining == 0) {\n    fill = \"red\";\n    text = \"Limit used\";\n}\n\nif (remaining > 0 && remaining < limit / 2) {\n    fill = \"yellow\"\n}\n\nif (remaining == -1 ) {\n    fill = \"blue\"\n    text = \"Limits unknown\"\n}\n\nmsg.payload = ({ fill: fill, text: text });\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":580,"wires":[[]]},{"id":"a18e96179ec2d987","type":"function","z":"93f485839dfa27a8","name":"Create graph output","func":"var m = {};\nm.labels = [];\nm.data = [];\nm.series = [];\n\nfor (let j = 0; j <= msg.days; j++) {\n    m.data[j] = [];\n}\n\nif (msg.watt === 'watt_hours_day' || msg.watt === 'watthours/day') {\n    var i = 0;\n    if (msg.kwhoutput) {\n        m.series.push(\"kWh per day\");\n    } else {\n        m.series.push(\"Watt hours per day\");\n    }\n    for (const key in msg.payload.result) {\n        m.labels.push(key);\n        if (msg.kwhoutput) {\n            m.data[i] = +(Math.round(msg.payload.result[key]/100)*.1).toFixed(1);\n        } else {\n            m.data[i] = msg.payload.result[key];\n        }\n        i++;\n    }\n    m.data = [m.data];\n    return { payload: [m] };\n}\n\nfor (let i = 0; i <= 23; i++) {\n\n    m.labels.push(i.toString()+':00');\n    if (msg.resolution === 4) {\n       m.labels.push(i.toString()+':15');\n    }\n    if (msg.resolution === 2 || msg.resolution == 4) {\n       m.labels.push(i.toString()+':30');\n    }\n    if (msg.resolution === 4) {\n       m.labels.push(i.toString()+':45');\n    }\n\n    for (let j = 0; j <= msg.days; j++) {\n        m.data[j].push(0);\n        if (msg.resolution === 4) {\n           m.data[j].push(0)\n        }\n        if (msg.resolution === 2 || msg.resolution == 4) {\n           m.data[j].push(0)\n        }\n        if (msg.resolution === 4) {\n           m.data[j].push(0)\n        }\n\n    }\n}\n\nvar offset = 0;\nfor (const key in msg.payload.result) {\n    var d = new Date(key)\n    if (m.series.indexOf(d.toISOString().split('T')[0]) === -1) {\n        m.series.push(d.toISOString().split('T')[0])\n    }\n\n    var h = d.getHours();\n    var minutes = d.getMinutes();\n\n    if (minutes === 0 ) {\n        offset = 0;\n    } else {\n        offset++;\n    }\n\n    if (msg.kwhoutput) {\n        m.data[m.series.length - 1][h*msg.resolution+offset] = +(Math.round(msg.payload.result[key]/100)*.1).toFixed(1);\n    } else {\n        m.data[m.series.length - 1][h*msg.resolution+offset] = msg.payload.result[key];\n    }\n}\n\nif (msg.watt === 'watt_hours') {\n    for (const i in m.data) {\n        let x = m.data[i][0]\n        for (const d in m.data[i]) {\n            if ( x > m.data[i][d]) {\n                m.data[i][d] = x \n            }\n            x = m.data[i][d]\n        }\n    }\n}\n\nreturn { payload: [m] };\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":340,"wires":[["5b0a430fb61e70e7"]]},{"id":"975daf96f15cfb61","type":"link out","z":"93f485839dfa27a8","name":"link out 7","mode":"link","links":["14f2e68e572f4ef8"],"x":805,"y":100,"wires":[]},{"id":"14f2e68e572f4ef8","type":"link in","z":"93f485839dfa27a8","name":"link in 18","links":["975daf96f15cfb61"],"x":245,"y":180,"wires":[["c706820c0d61f023"]]},{"id":"c4307905e114824f","type":"catch","z":"93f485839dfa27a8","name":"","scope":null,"uncaught":false,"x":260,"y":440,"wires":[["f427f19392c399ce"]]},{"id":"e56826252134b93a","type":"link out","z":"93f485839dfa27a8","name":"link out 8","mode":"link","links":["dbaf8f5f5a920686"],"x":685,"y":480,"wires":[]},{"id":"dbaf8f5f5a920686","type":"link in","z":"93f485839dfa27a8","name":"link in 19","links":["e56826252134b93a"],"x":385,"y":580,"wires":[["1bfc1cde3ee94e4b"]]},{"id":"f427f19392c399ce","type":"link out","z":"93f485839dfa27a8","name":"link out 9","mode":"link","links":["2ded0c14a222b4d9","2f42837904c91d73"],"x":375,"y":440,"wires":[]},{"id":"2ded0c14a222b4d9","type":"link in","z":"93f485839dfa27a8","name":"link in 20","links":["f427f19392c399ce"],"x":385,"y":540,"wires":[["a798fbe66cf133d5"]]},{"id":"a798fbe66cf133d5","type":"function","z":"93f485839dfa27a8","name":"Set error status","func":"node.warn(msg.payload)\nmsg.payload = ({ fill: \"red\", text: msg.payload });\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":540,"wires":[[]]},{"id":"2f42837904c91d73","type":"link in","z":"93f485839dfa27a8","name":"link in 21","links":["f427f19392c399ce"],"x":665,"y":280,"wires":[[]]},{"id":"fcc8d69a3ab88e6d","type":"function","z":"93f485839dfa27a8","name":"Processed info","func":"msg.resolution = 60;\nmsg.days = 1;\nmsg.type = env.get('type');\nmsg.watt = env.get('watt');\nmsg.kwhoutput = env.get('kwhoutput');\n\nvar key1 = Object.keys(msg.payload.result)[1];\nvar key2 = Object.keys(msg.payload.result)[2];\nvar key3 = Object.keys(msg.payload.result)[Object.keys(msg.payload.result).length-1];\n\nvar d1 = new Date(key1);\nvar d2 = new Date(key2); \nvar d3 = new Date(key3);\nmsg.resolution = 3600000 / (d2.getTime() - d1.getTime());\n\nmsg.days = Math.floor((d3.getTime() - d1.getTime()) / (1000 * 3600 * 24));\n\nif (msg.watt === 'watt_hours_day' || msg.watt === 'watthours/day') {\n    msg.resolution = null;\n}\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":260,"wires":[["a18e96179ec2d987"]],"info":"Function to process the result from forecast.solar to add\nextra information, which is handy for either graphing or\nto store in a database.\n\n\nThe extra values added:\n- `msg.resolution` - The number of measurements per hour. If\nno API key is used, this will be 1. Other values may be 2 or 4.\n- `msg.days` - The number of days in the forcast. If no API\n- key is used this will be 1. Other values may be 3 or 6."},{"id":"5b0a430fb61e70e7","type":"function","z":"93f485839dfa27a8","name":"Filter graph","func":"\nif (env.get('showday')) {\n    const weekday = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"];\n    msg.payload[0].labels.forEach(function (/** @type {string | number | Date} */ date, /** @type {string | number} */ index, /** @type {{ [x: string]: string; }} */ array) {\n        const d = new Date(date)\n        if (!isNaN(d)) {\n            array[index] = weekday[d.getDay()]\n        }\n    })\n    msg.payload[0].series.forEach(function (/** @type {string | number | Date} */ date, /** @type {string | number} */ index, /** @type {{ [x: string]: string; }} */ array) {\n        const d = new Date(date)\n        if (!isNaN(d)) {\n            array[index] = weekday[d.getDay()]\n        }\n    })\n}\n\nif (env.get('watt') === 'watt_hours_day' || env.get('watt') === 'watthours/day') {\n    if (!env.get('showtoday')) {\n        msg.payload[0].data[0].shift();\n        msg.payload[0].labels.shift();\n    }\n    return msg;\n}\n\nif (!env.get('showtoday')) {\n    msg.payload[0].data.shift();\n    msg.payload[0].series.shift();\n}\n\nvar forecasted = msg.payload[0].series.length;\n\nif ((Number(env.get('daystoforecast')) > -1) && (Number(env.get('daystoforecast')) < forecasted)) {\n    for (let i = 1; i <= (forecasted - Number(env.get('daystoforecast'))); i++ ) {\n        msg.payload[0].data.pop();\n        msg.payload[0].series.pop();\n    }\n}\n\nif (env.get('widengraph')) {\n    var c = msg.payload[0].labels.length;\n    var x = 0;\n    for (let i = 0; i < c; i++) {\n        var remove = true;\n        for (let d = 0; d < msg.payload[0].data.length; d++) {\n            if (msg.payload[0].data[d][x] > 0) {\n                remove = false;\n            }\n        }\n        if (remove) {\n            msg.payload[0].labels.splice(x, 1);\n            for (let d = 0; d < msg.payload[0].data.length; d++) {\n                 msg.payload[0].data[d].splice(x, 1);\n            }\n            x--;\n        }\n        x++;\n    }\n    // Still the first and last datapoints should be zero, so\n    // add those again\n    msg.payload[0].labels.unshift('');\n    msg.payload[0].labels.push('');\n    for (let d = 0; d < msg.payload[0].data.length; d++) {\n         msg.payload[0].data[d].unshift(0);\n         msg.payload[0].data[d].push(0);\n    }   \n}\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":340,"wires":[[]]},{"id":"c8dc6aa14b9f3e92","type":"delay","z":"93f485839dfa27a8","name":"1 msg/15 minutes","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"15","rateUnits":"minute","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":390,"y":100,"wires":[["b9488734852cd0ca"]]},{"id":"a306e73feb83d4e7","type":"tab","label":"MaxChargeCurrent","disabled":false,"info":"","env":[{"name":"defaultAmps","value":"60","type":"num"},{"name":"wattsBuffer","value":"120","type":"num"},{"name":"batteryAh","value":"280","type":"num"},{"name":"batteryV","value":"60","type":"num"},{"name":"safetyAmps","value":"30","type":"num"},{"name":"fullspeedAmps","value":"100","type":"num"},{"name":"minBatteryTemp","value":"15","type":"num"},{"name":"safetyMin","value":"20","type":"num"},{"name":"minAmps","value":"5","type":"num"},{"name":"batteryAbsorptionV","value":"62.1","type":"num"},{"name":"cellAbsorptionV","value":"3.45","type":"num"},{"name":"useFullspeedFactor","value":"0.8","type":"num"},{"name":"shadowBeforeHour","value":"08","type":"num"}]},{"id":"5fdbadbaa356f79d","type":"subflow:93f485839dfa27a8","z":"a306e73feb83d4e7","name":"East - Watt hours (energy) summarized for each day","env":[{"name":"latitude","value":"48.25794","type":"num"},{"name":"longitude","value":"12.51803","type":"num"},{"name":"declination","value":"20","type":"num"},{"name":"modules power","value":"2.6","type":"num"},{"name":"apikey","type":"cred"},{"name":"watt","value":"watthours","type":"str"},{"name":"kwhoutput","type":"bool","value":"true"},{"name":"daystoforecast","value":"1","type":"str"},{"name":"showday","type":"bool","value":"true"}],"x":560,"y":120,"wires":[["049b5e4656a5ee5a","15752e70f740a1d4"],[]]},{"id":"049b5e4656a5ee5a","type":"debug","z":"a306e73feb83d4e7","name":"rawForecastSolar","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1110,"y":60,"wires":[]},{"id":"15752e70f740a1d4","type":"function","z":"a306e73feb83d4e7","name":"calcExpectedPvAmps","func":"flow.set('forecast_solar_result', msg.payload.result);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1100,"y":120,"wires":[[]]},{"id":"80e781da4ce7f93f","type":"victron-input-solarcharger","z":"a306e73feb83d4e7","service":"com.victronenergy.solarcharger/278","path":"/Yield/Power","serviceObj":{"service":"com.victronenergy.solarcharger/278","name":"SmartSolar MPPT"},"pathObj":{"path":"/Yield/Power","type":"float","name":"PV Power (W)"},"name":"","onlyChanges":false,"roundValues":"0","x":500,"y":400,"wires":[["9c5a5a226734d736"]]},{"id":"7406541c7dd46995","type":"victron-input-battery","z":"a306e73feb83d4e7","service":"com.victronenergy.battery/279","path":"/ConsumedAmphours","serviceObj":{"service":"com.victronenergy.battery/279","name":"SmartShunt"},"pathObj":{"path":"/ConsumedAmphours","type":"float","name":"Consumed Amphours (Ah)"},"name":"","onlyChanges":false,"roundValues":"0","x":510,"y":340,"wires":[["70bff919aa94fd23"]]},{"id":"f5a798878716e99d","type":"function","z":"a306e73feb83d4e7","name":"calcMinAmps","func":"const minWatts = parseInt(msg.payload) + env.get('wattsBuffer');\nconst minAmps = Math.max(Math.floor(minWatts / env.get('batteryV')), env.get('minAmps'));\n\nflow.set('min_amps', minAmps);\n\nmsg.payload = minAmps;\nnode.status({ fill: \"green\", shape: \"dot\", text: minAmps });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1110,"y":400,"wires":[[]]},{"id":"70bff919aa94fd23","type":"function","z":"a306e73feb83d4e7","name":"calcAvailableAmps","func":"const consumedBatteryAmps = Math.abs(msg.payload);\nconst availableBatteryAmps = env.get('batteryAh') - consumedBatteryAmps;\n\nflow.set('consumed_battery_amps', consumedBatteryAmps);\nflow.set('available_battery_amps', availableBatteryAmps);\n\nconst ntext = `Con: ${consumedBatteryAmps} Avp: ${availableBatteryAmps}`;\n\nnode.status({ fill: \"green\", shape: \"dot\", text: ntext });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1130,"y":340,"wires":[[]]},{"id":"3e34c9bf260ca404","type":"victron-input-ess","z":"a306e73feb83d4e7","service":"com.victronenergy.battery/1","path":"/System/MaxCellVoltage","serviceObj":{"service":"com.victronenergy.battery/1","name":"SerialBattery(Jkbms)"},"pathObj":{"path":"/System/MaxCellVoltage","type":"integer","name":"Maximum battery cell voltage"},"initial":"","name":"","onlyChanges":false,"x":550,"y":540,"wires":[["2c9646a14dd8469a","d48602358d22033b"]]},{"id":"89ae0f25d8d9b6cd","type":"victron-input-battery","z":"a306e73feb83d4e7","service":"com.victronenergy.battery/279","path":"/Dc/0/Voltage","serviceObj":{"service":"com.victronenergy.battery/279","name":"SmartShunt"},"pathObj":{"path":"/Dc/0/Voltage","type":"float","name":"Battery voltage (V)"},"name":"","onlyChanges":false,"roundValues":"1","x":490,"y":460,"wires":[["3ffddd4108f5db1c","44c01570e74d4567"]]},{"id":"5f43730c4bc9acd3","type":"function","z":"a306e73feb83d4e7","name":"setBatteryVoltage","func":"flow.set('battery_voltage', msg.payload);\nnode.status({ fill: \"green\", shape: \"dot\", text: msg.payload });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1130,"y":460,"wires":[[]]},{"id":"cec48e05a33d442b","type":"function","z":"a306e73feb83d4e7","name":"setMaxCellVoltage","func":"flow.set('max_cell_voltage', msg.payload);\nnode.status({ fill: \"green\", shape: \"dot\", text: msg.payload });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1130,"y":540,"wires":[[]]},{"id":"83e352c7d3c466b2","type":"inject","z":"a306e73feb83d4e7","name":"Every 2min","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"*/2 5-20 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":190,"y":680,"wires":[["1e118c3b22ffbe8f"]]},{"id":"1e118c3b22ffbe8f","type":"function","z":"a306e73feb83d4e7","name":"calcExpectedPvAmps","func":"const forecastSolarResult = flow.get('forecast_solar_result');\nconst currentDate = new Date();\nconst year = currentDate.getFullYear();\nconst month = String(currentDate.getMonth() + 1).padStart(2, '0');\nconst day = String(currentDate.getDate()).padStart(2, '0');\n\nlet hours = currentDate.getHours();\nlet mins = currentDate.getMinutes();\n\n// -----------------------------------------------------------------------------\n// get now and calc the partWatts\n// -----------------------------------------------------------------------------\n\nlet partWatts = 0;\nlet now = `${year}-${month}-${day} ${String(hours).padStart(2, '0')}:00:00`;\n\nif (hours < env.get('shadowBeforeHour')) {\n    hours = env.get('shadowBeforeHour');\n    now = `${year}-${month}-${day} ${String(hours).padStart(2, '0')}:00:00`;\n} else {\n    let oneHourLater = `${year}-${month}-${day} ${String(hours+1).padStart(2, '0')}:00:00`;\n    \n    // Calculate partWatts if we are somewhere in the middle of an hour from \n    // [data of hour +1] - [data of hour]\n    partWatts = forecastSolarResult[oneHourLater] - forecastSolarResult[now];\n    // we need to have the partWatts of the remaining hour-minutes\n    partWatts = Math.round(partWatts / 60 * (60-mins));\n    // as we have now calculated the partWatts, we need to calc from hour+1\n    now = oneHourLater;\n}\n\n// -----------------------------------------------------------------------------\n// get the afternoon\n// -----------------------------------------------------------------------------\n\nlet afternoon;\nlet afternoonHour;\nfor (let i = 19; i >= 16; i--) {\n    afternoon = `${year}-${month}-${day} ${String(i).padStart(2, '0')}:00:00`;\n    if (forecastSolarResult.hasOwnProperty(afternoon)) {\n        afternoonHour = i;\n        break;\n    }\n}\n\n// -----------------------------------------------------------------------------\n// calc the expected watts/amps\n// -----------------------------------------------------------------------------\n\nlet expectedPvWatts = 0;\nlet expectedPvAmps = 0;\n// only calculate if we need to do a prediction\nif (afternoonHour > hours) {\n    expectedPvWatts = forecastSolarResult[afternoon] - forecastSolarResult[now];\n    expectedPvWatts = forecastSolarResult[afternoon] - forecastSolarResult[now] + partWatts;\n    expectedPvAmps = Math.max(Math.round(expectedPvWatts / env.get('batteryV')), 0);\n}\n\nflow.set('expected_pv_amps', expectedPvAmps);\nflow.set('expected_pv_watts', expectedPvWatts);\nnode.status({ fill: \"green\", shape: \"dot\", text: expectedPvAmps });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":620,"wires":[["2baf393c6e171be8"]]},{"id":"4788bd02e3fe1432","type":"function","z":"a306e73feb83d4e7","name":"calcMaxChargeCurrent","func":"const currentTs = Date.now();\nconst safetyDuration = env.get('safetyMin') * 60 * 1000;\n\nconst batteryV = parseFloat(flow.get('battery_voltage'));\nconst maxCellV = parseFloat(flow.get('max_cell_voltage'));\nconst previous = parseInt(flow.get('previous_max_charge_current'));\n\nconst systemBatteryTemp = flow.get('system_battery_temp');\nconst jkBatteryTemp = flow.get('jk_battery_temp');\nconst batterySafetyTs = flow.get('battery_safety_ts');\n\n// by default, we use the proposed charge current\nlet maxChargeCurrent = parseInt(flow.get('proposed_charge_current'));\n\n// -----------------------------------------------------------------------------\n// calc maxChargeCurrent\n// -----------------------------------------------------------------------------\n\n// safety for the battery\nif (batteryV >= env.get('batteryAbsorptionV') || \n    maxCellV >= env.get('cellAbsorptionV') ||\n    systemBatteryTemp < env.get('minBatteryTemp') || \n    jkBatteryTemp < env.get('minBatteryTemp')) {\n    maxChargeCurrent = Math.min(maxChargeCurrent, env.get('safetyAmps'));\n    \n    const infoData = {\n        \"batteryV\": batteryV,\n        \"batteryAbsorptionV\": env.get('batteryAbsorptionV'),\n        \"maxCellV\": maxCellV,\n        \"cellAbsorptionV\": env.get('cellAbsorptionV'),\n        \"minBatteryTemp\": env.get('minBatteryTemp'),\n        \"systemBatteryTemp\": systemBatteryTemp,\n        \"jkBatteryTemp\": jkBatteryTemp\n    };\n    \n    if (batterySafetyTs <= 0) {\n        node.warn(`Turn on safe battery loading with ${maxChargeCurrent} amps!`);\n        flow.set('battery_safety_ts', currentTs);\n        node.warn(infoData);\n    }\n} else if (batterySafetyTs > 0 && ((batterySafetyTs + safetyDuration) <= currentTs)) {\n    node.warn(\"Turn off safe battery loading!\");\n    flow.set('battery_safety_ts', -1);\n}\n\n// -----------------------------------------------------------------------------\n// Check if we need to set the data\n// -----------------------------------------------------------------------------\n\nif (maxChargeCurrent == previous) {\n    return;\n} else if (maxChargeCurrent > previous) {\n    // set to a higher ChargeCurrent only if not in batterySafety mode\n    if (batterySafetyTs > 0) {\n        return;\n    }\n}\n\n// -----------------------------------------------------------------------------\n// set maxChargeCurrent\n// -----------------------------------------------------------------------------\n\nflow.set('previous_max_charge_current', maxChargeCurrent);\nmsg.payload = maxChargeCurrent;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":740,"wires":[["8f372f1053ef193b","e2a660ec19d37391"]]},{"id":"8f372f1053ef193b","type":"debug","z":"a306e73feb83d4e7","name":"maxChargeCurrent","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":790,"y":700,"wires":[]},{"id":"2c9646a14dd8469a","type":"link out","z":"a306e73feb83d4e7","name":"link out 10","mode":"link","links":["66d146b1abdc7953"],"x":805,"y":560,"wires":[]},{"id":"3ffddd4108f5db1c","type":"link out","z":"a306e73feb83d4e7","name":"link out 11","mode":"link","links":["66d146b1abdc7953"],"x":805,"y":480,"wires":[]},{"id":"66d146b1abdc7953","type":"link in","z":"a306e73feb83d4e7","name":"link in 22","links":["3ffddd4108f5db1c","2c9646a14dd8469a"],"x":155,"y":740,"wires":[["4788bd02e3fe1432"]]},{"id":"e5da402294a9f1b6","type":"inject","z":"a306e73feb83d4e7","name":"At 05:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 05 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":100,"wires":[["5fdbadbaa356f79d"]]},{"id":"98683be73c318c21","type":"inject","z":"a306e73feb83d4e7","name":"Once at 04:30","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"30 04 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":60,"wires":[["893beda0398abfea"]]},{"id":"893beda0398abfea","type":"function","z":"a306e73feb83d4e7","name":"resetFlow","func":"flow.set('expected_pv_amps', -1);\nflow.set('battery_safety_ts', -1);\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n\nflow.set('proposed_charge_current', env.get(\"defaultAmps\"))\nflow.set('previous_max_charge_current', 0);\nflow.set('expected_pv_amps', -1);\nflow.set('battery_safety_ts', -1);","finalize":"","libs":[],"x":400,"y":60,"wires":[[]]},{"id":"44c01570e74d4567","type":"rate-avg","z":"a306e73feb83d4e7","name":"","windowtype":"time","timewindow":"10","timeunits":"seconds","countwindow":"10","round":"3","x":940,"y":460,"wires":[["5f43730c4bc9acd3"]]},{"id":"d48602358d22033b","type":"rate-avg","z":"a306e73feb83d4e7","name":"","windowtype":"time","timewindow":"10","timeunits":"seconds","countwindow":"10","round":"3","x":940,"y":540,"wires":[["cec48e05a33d442b"]]},{"id":"e58d3e4800582609","type":"victron-input-system","z":"a306e73feb83d4e7","service":"com.victronenergy.system/0","path":"/Dc/Battery/Temperature","serviceObj":{"service":"com.victronenergy.system/0","name":"Venus system"},"pathObj":{"path":"/Dc/Battery/Temperature","type":"float","name":"Battery temperature (°C)"},"name":"","onlyChanges":false,"roundValues":"0","x":520,"y":200,"wires":[["7c49d9be4ad68e2b"]]},{"id":"884995d72d64a3df","type":"victron-input-ess","z":"a306e73feb83d4e7","service":"com.victronenergy.battery/1","path":"/System/MinCellTemperature","serviceObj":{"service":"com.victronenergy.battery/1","name":"SerialBattery(Jkbms)"},"pathObj":{"path":"/System/MinCellTemperature","type":"integer","name":"Minimum battery cell temperature"},"name":"","onlyChanges":false,"x":560,"y":260,"wires":[["86248ab958f98e45"]]},{"id":"11acd6566a6349ff","type":"function","z":"a306e73feb83d4e7","name":"setSystemBatteryTemp","func":"flow.set('system_battery_temp', msg.payload);\nnode.status({ fill: \"green\", shape: \"dot\", text: msg.payload });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1150,"y":200,"wires":[[]]},{"id":"7c49d9be4ad68e2b","type":"rate-avg","z":"a306e73feb83d4e7","name":"","windowtype":"time","timewindow":"10","timeunits":"seconds","countwindow":"10","round":"3","x":940,"y":200,"wires":[["11acd6566a6349ff"]]},{"id":"86248ab958f98e45","type":"rate-avg","z":"a306e73feb83d4e7","name":"","windowtype":"time","timewindow":"10","timeunits":"seconds","countwindow":"10","round":"3","x":940,"y":260,"wires":[["07388e5e675f6750"]]},{"id":"07388e5e675f6750","type":"function","z":"a306e73feb83d4e7","name":"SetJkBatteryTemp","func":"flow.set('jk_battery_temp', msg.payload);\nnode.status({ fill: \"green\", shape: \"dot\", text: msg.payload });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1130,"y":260,"wires":[[]]},{"id":"e2a660ec19d37391","type":"victron-output-settings","z":"a306e73feb83d4e7","service":"com.victronenergy.settings","path":"/Settings/SystemSetup/MaxChargeCurrent","serviceObj":{"service":"com.victronenergy.settings","name":"Venus settings"},"pathObj":{"path":"/Settings/SystemSetup/MaxChargeCurrent","type":"float","name":"DVCC system max charge current (A DC)","writable":true},"name":"","onlyChanges":false,"x":910,"y":740,"wires":[]},{"id":"2fb09b62eb71684b","type":"inject","z":"a306e73feb83d4e7","name":"At 09:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 09 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":180,"wires":[["5fdbadbaa356f79d"]]},{"id":"78cc76fbe47d8811","type":"inject","z":"a306e73feb83d4e7","name":"At 13:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 13 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":260,"wires":[["5fdbadbaa356f79d"]]},{"id":"58da664f972cd5ee","type":"inject","z":"a306e73feb83d4e7","name":"At 16:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 16 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":300,"wires":[["5fdbadbaa356f79d"]]},{"id":"3771e165b3d86e59","type":"debug","z":"a306e73feb83d4e7","name":"showAllValues","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1020,"y":620,"wires":[]},{"id":"9c5a5a226734d736","type":"rate-avg","z":"a306e73feb83d4e7","name":"","windowtype":"time","timewindow":"30","timeunits":"seconds","countwindow":"10","round":"3","x":940,"y":400,"wires":[["f5a798878716e99d"]]},{"id":"79876a921b958b45","type":"victron-input-ess","z":"a306e73feb83d4e7","service":"com.victronenergy.settings","path":"/Settings/SystemSetup/MaxChargeCurrent","serviceObj":{"service":"com.victronenergy.settings","name":"Venus settings"},"pathObj":{"path":"/Settings/SystemSetup/MaxChargeCurrent","type":"float","name":"DVCC Charge current limit (A)"},"name":"","onlyChanges":false,"x":880,"y":800,"wires":[[]]},{"id":"2baf393c6e171be8","type":"function","z":"a306e73feb83d4e7","name":"calcProposedChargeCurrent","func":"const consumedBatteryAmps = parseInt(flow.get('consumed_battery_amps'));\nconst availableBatteryAmps = parseInt(flow.get('available_battery_amps'));\nconst minAmps = parseInt(flow.get('min_amps'));\nconst expectedPvAmps = flow.get('expected_pv_amps');\nconst expectedPvWatts = flow.get('expected_pv_watts');\n\n// -----------------------------------------------------------------------------\n// calc the propsed charge current\n// -----------------------------------------------------------------------------\n\nlet proposedChargeCurrent = env.get('defaultAmps');\n\nif (expectedPvAmps > consumedBatteryAmps) {\n    proposedChargeCurrent = minAmps;\n    flow.set('battery_load_strategy', 'minAmps');\n} else if (availableBatteryAmps < Math.round(env.get('batteryAh') * env.get('useFullspeedFactor'))) {\n    proposedChargeCurrent = env.get('fullspeedAmps');\n    flow.set('battery_load_strategy', 'fullspeedAmps');\n} else {\n    proposedChargeCurrent = env.get('defaultAmps');\n    flow.set('battery_load_strategy', 'defaultAmps');\n}\nflow.set('proposed_charge_current', proposedChargeCurrent);\n\nconst ntext = `Strategy: ${flow.get('battery_load_strategy')} - ChargeCurrent: ${proposedChargeCurrent}`;\nnode.status({ fill: \"green\", shape: \"dot\", text: ntext });\n\n// -----------------------------------------------------------------------------\n// set data for debug output\n// -----------------------------------------------------------------------------\n\nmsg.payload = {\n    \"batteryLoadStrategy\": flow.get('battery_load_strategy'), \n    \"consumedBatteryAmps\": consumedBatteryAmps,\n    \"availableBatteryAmps\": availableBatteryAmps,\n    \"expectedPvAmps\": expectedPvAmps,\n    \"expectedPvWatts\": expectedPvWatts,\n    \"minAmps\": minAmps,\n    \"proposedChargeCurrent\": proposedChargeCurrent,\n    \"batteryVoltage\": flow.get('battery_voltage'),\n    \"maxCellVoltage\": flow.get('max_cell_voltage')\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":620,"wires":[["3771e165b3d86e59"]]},{"id":"e583dfd792e50dad","type":"inject","z":"a306e73feb83d4e7","name":"At 11:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 11 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":220,"wires":[["5fdbadbaa356f79d"]]},{"id":"5d3e370af89c7068","type":"inject","z":"a306e73feb83d4e7","name":"At 07:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 07 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":140,"wires":[["5fdbadbaa356f79d"]]}]

Collection Info

prev

Flow Info

Created 7 months, 2 weeks ago
Rating: 4 1

Owner

Actions

Rate:

Node Types

Core
  • catch (x1)
  • debug (x3)
  • delay (x1)
  • function (x18)
  • http request (x1)
  • inject (x9)
  • json (x1)
  • link in (x7)
  • link out (x6)
Other
  • rate-avg (x5)
  • subflow (x1)
  • subflow:93f485839dfa27a8 (x1)
  • tab (x1)
  • victron-input-battery (x2)
  • victron-input-ess (x3)
  • victron-input-solarcharger (x1)
  • victron-input-system (x1)
  • victron-output-settings (x1)

Tags

  • victron
  • cerbo
  • forecast.solar
  • solar
  • maxcharge
  • jkbms
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option