victron-tibber-ess-control

Charging and discharging of the battery on the basis of a dynamic electricity tariff, taking into account conversion losses and the expected solar yield.

Configuration

What needs to be configured?

Configuration nodes

  • config-vrm-api
  • pushbullet-config
  • tibber-api-endpoint

Global settings node

  • VRM site id
  • Battery capacity,
  • Min and max desired SoC
  • Charge hours
  • Desired charging time window
  • charging strategy
  • Price per kwh for solar power
  • Efficiency of the inverter (conversion losses)
  • Efficiency of the charger (conversion losses)
  • Storage costs per kwh (wear and tear on the battery)

Hints

The flow uses the hourly Tibber prices to determine the best possible time to charge the battery. In addition, the expected solar surplus, calculated from the predicted solar yield and predicted consumption, is also taken into account.

To charge the battery, the "SOC minimum discharge value" is set to the top or the ESS mode "Keep batteries charged" is set.

Discharging the battery is only permitted if this makes sense, taking into account conversion losses (inverter and charger). The predefined values of 0.90 and 0.92 are valid for Multiplus 48/3000 and 48/5000.

Everyone must decide for themselves whether storage costs should be included or not. The battery is already there anyway and will probably die of old age sooner than of wear and tear.

Changelog

  • v1.2.1 add "cover demand" charging strategy
  • v1.2.0 rewrite charge scheduling
  • v1.1.8 adapt for victron-vrm-api 0.2.1
  • v1.1.7 fix logic for "Hold" decision
  • v1.1.6 optimize discharge schedulung, add discharge history
  • v1.1.5 default to "Hold" in ess control logic
  • v1.1.4 rewrite discharge scheduling
  • v1.1.3 treat case of mains power loss
  • v1.1.2 fix edge case on surplus trigger, only notify when decision changes
  • v1.1.1 optimize surplus trigger, fix discharge hour calc when consumption fc data is unavailable
  • v1.1.0 add surplus detection, revise diagram to show actual unloading times
  • v1.0.11 switch Multiplus off overnight to save energy, switches on again automatically when necessary.
  • v1.0.10 determine hours with highest price to discharge the battery
  • v1.0.9 optimize max allowed price calculation
  • v1.0.8 remove paths for self consumption, causes more problems than it solves
  • v1.0.7 adapt for victron-vrm-api 0.0.10, use local time on vrm api nodes
  • v1.0.6 optimize solar excess calculation
  • v1.0.5 adapt for victron-vrm-api 0.0.9, fix missing energy data for transfer from previous day
  • v1.0.4 adapt for victron-vrm-api 0.0.8
  • v1.0.3 optimize average price calculation
  • v1.0.2 Fix problem where deglitch is blocking the whole flow
  • v1.0.1 fix error when solar to battery data does not exist
  • v1.0.0 Initial release
[{"id":"d1a79cb7a1857836","type":"tab","label":"Tibber","disabled":false,"info":"","env":[]},{"id":"ef1725482f2d21a8","type":"template","z":"d1a79cb7a1857836","name":"Current energy price","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n  viewer {\n    homes {\n      currentSubscription {\n        priceInfo {\n          current {\n            total\n            startsAt\n          }\n          today {\n            total\n            startsAt\n          }\n          tomorrow {\n            total\n            startsAt\n          }\n        }\n      }\n    }\n  }\n}","output":"str","x":560,"y":100,"wires":[["0547c5f9b86b4a0a"]]},{"id":"0547c5f9b86b4a0a","type":"tibber-query","z":"d1a79cb7a1857836","name":"","active":true,"apiEndpointRef":"073c94010f8c84e0","x":770,"y":100,"wires":[["a32cbfd24053aca5","2489c89b2cf826df","df121a781659dce6"]]},{"id":"93b0b1a9790769a3","type":"victron-output-ess","z":"d1a79cb7a1857836","service":"com.victronenergy.settings","path":"/Settings/CGwacs/BatteryLife/MinimumSocLimit","serviceObj":{"service":"com.victronenergy.settings","name":"Venus settings"},"pathObj":{"path":"/Settings/CGwacs/BatteryLife/MinimumSocLimit","type":"integer","name":"Minimum Discharge SOC (%)","writable":true},"name":"","onlyChanges":false,"x":2700,"y":380,"wires":[]},{"id":"a32cbfd24053aca5","type":"ps-receive-price","z":"d1a79cb7a1857836","name":"Price Receiver","x":960,"y":100,"wires":[["2ff24e4bea53ddec","63ce8d7211dd1baa","32deb253332c9a29"]]},{"id":"2489c89b2cf826df","type":"debug","z":"d1a79cb7a1857836","name":"tibber-query","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":40,"wires":[]},{"id":"2ff24e4bea53ddec","type":"debug","z":"d1a79cb7a1857836","name":"Price Receiver","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1160,"y":40,"wires":[]},{"id":"7886a9ba56d32feb","type":"function","z":"d1a79cb7a1857836","name":"Generate chart data","func":"const hours_charge = msg.payload.schedule_charge.hours;\nconst hours_discharge = msg.payload.schedule_discharge.hours;\n\nconst series = [\"Charge\", \"Discharge\", \"Price\"];\nconst labels = series;\nconst data = [[], [], []];\n\nfor (let i = 0; i < hours_charge.length; i++)\n{\n  if (hours_charge[i].onOff)\n  {\n      data[0][i] = { \"x\": hours_charge[i].start, \"y\": hours_charge[i].price};\n  }\n  else\n  {\n      data[0][i] = { \"x\": hours_charge[i].start, \"y\": 0};\n  }\n  \n  data[2][i] = { \"x\": hours_charge[i].start, \"y\": hours_charge[i].price};\n}\n\nfor (let i = 0; i < hours_discharge.length; i++)\n{\n  if (!hours_discharge[i].onOff)\n  {\n      data[1][i] = { \"x\": hours_discharge[i].start, \"y\": hours_discharge[i].price};\n  }\n  else\n  {\n      data[1][i] = { \"x\": hours_discharge[i].start, \"y\": 0};\n  }\n}\n\nmsg.payload=[{\n    \"series\": series,\n    \"data\": data,\n    \"labels\": labels\n}];\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":2640,"y":140,"wires":[["7fb781f15b39147a"]]},{"id":"7fb781f15b39147a","type":"ui_chart","z":"d1a79cb7a1857836","name":"","group":"ae94fc0e9881e8c0","order":7,"width":0,"height":0,"label":"Price & Schedule Chart","chartType":"line","legend":"true","xformat":"auto","interpolate":"step","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"6","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#ff0000","#0080ff","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":2890,"y":140,"wires":[[]]},{"id":"32deb253332c9a29","type":"function","z":"d1a79cb7a1857836","name":"max allowed price","func":"const CHARGER_EFFICIENCY_FACTOR = flow.get('charger_efficiency_factor') || 0.90;\nconst INVERTER_EFFICIENCY_FACTOR = flow.get('inverter_efficiency_factor') || 0.92;\nconst STORAGE_COSTS = flow.get('storage_costs') || 0.00;\n\nlet price_max_allowed = 0.16;\nconst current_hour = new Date().getHours();\nconst price_data = msg.payload.priceData;\nconst price_current = price_data[current_hour].value;\nflow.set('price_current', price_current);\n\nif (price_data.length >= 24)\n{\n    let price_low = price_data[0].value;\n    let price_high = price_data[0].value;\n\n    for (let i = 0; i < price_data.length; i++) \n    {\n        if (price_data[i].value < price_low)\n        {\n            price_low = price_data[i].value;\n        }\n        \n        if (price_data[i].value > price_high)\n        {\n            price_high = price_data[i].value;\n        }\n    }\n    \n    price_max_allowed = price_high * CHARGER_EFFICIENCY_FACTOR * INVERTER_EFFICIENCY_FACTOR - STORAGE_COSTS;\n    \n    flow.set('price_low', price_low);\n    flow.set('price_high', price_high);\n    flow.set('price_max_allowed', price_max_allowed);\n}\nelse\n{\n    node.warn('Tibber response contains less then 24 hours');\n}\n\nmsg.payload = Number(price_max_allowed);\nnode.status({text: `${Math.round(msg.payload * 1000) / 1000} €`});\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1170,"y":100,"wires":[["fd2431b376d968b2"]]},{"id":"7f18f0875225b2b3","type":"pushbullet","z":"d1a79cb7a1857836","config":"80c802fa5c3703dc","pushtype":"note","title":"Push from Tibber Flow","chan":"","name":"","x":2980,"y":840,"wires":[]},{"id":"cc05113d3431c7da","type":"cronplus","z":"d1a79cb7a1857836","name":"Every hour","outputField":"payload","timeZone":"","storeName":"","commandResponseMsgOutput":"output1","defaultLocation":"50.31687238278184 10.178041756153105","defaultLocationType":"fixed","outputs":1,"options":[{"name":"schedule1","topic":"topic1","payloadType":"default","payload":"","expressionType":"cron","expression":"0 2 * * * * *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"}],"x":110,"y":100,"wires":[["a571f168cae8d985"]]},{"id":"38555d9074420afd","type":"cronplus","z":"d1a79cb7a1857836","name":"7:05 / 20:05","outputField":"payload","timeZone":"","storeName":"","commandResponseMsgOutput":"output1","defaultLocation":"50.316866217047334 10.178051143884659","defaultLocationType":"fixed","outputs":1,"options":[{"name":"schedule1","topic":"topic1","payloadType":"default","payload":"","expressionType":"cron","expression":"0 5 7,20 * * * *","location":"","offset":"0","solarType":"selected","solarEvents":"sunrise,sunset"}],"x":2470,"y":860,"wires":[["93b17a4b3da6be6a"]]},{"id":"0b39e1bed02ad85f","type":"function","z":"d1a79cb7a1857836","name":"ESS control logic","func":"const ESS_STATE_KEEP_CHARGED = 9;\nconst ESS_STATE_OPTIMIZED = 10;\n\nconst SWITCH_CHARGER_ONLY = 1;\nconst SWITCH_INVERTER_ONLY = 2;\nconst SWITCH_ON = 3;\nconst SWITCH_OFF = 4;\n\nconst GRID_LOST = 2;\n\nconst BAT_CAPACITY = flow.get('bat_capacity'); // kWh\nconst SOC_MIN = flow.get('bat_soc_min'); // %\nconst SOC_MAX = flow.get('bat_soc_max'); // %\nconst CHARGE_STRATEGY = flow.get('charge_strategy'); // %\n\nconst CHARGER_EFFICIENCY_FACTOR = flow.get('charger_efficiency_factor') || 0.90;\n\nconst BAT_SOC = msg.payload.soc; // %\nconst BAT_CURRENT = msg.payload.battery_current; // A\n\nlet energy_deficit = msg.payload.energy_deficit;\nif (energy_deficit <= 0)\n{\n    energy_deficit = 0;\n}\n\nconst SOLAR_EXCESS_PERC = Math.round(((msg.payload.solar_excess / BAT_CAPACITY) * 100) * 100) / 100; // %\nconst ENERGY_DEFICIT_PERC = Math.round((((energy_deficit * CHARGER_EFFICIENCY_FACTOR) / BAT_CAPACITY) * 100) * 100) / 100; // %\n\nlet switch_position = msg.payload.switch_position;\n\nfunction setSwitchPosition(value)\n{\n    if (switch_position != value)\n    {\n        if (-1 <= BAT_CURRENT && BAT_CURRENT <= 1)\n        {\n            switch_position = value;\n        }\n        else\n        {\n            node.error(\"Not switching mode because of battery current: \" + Math.round(BAT_CURRENT * 100) / 100 + \" A\")\n        }\n    }\n}\n\nconst d = new Date();\nlet hour = d.getHours();\n\n// prices\nconst price_current = flow.get('price_current') || 0; // Eur\nconst price_low = flow.get('price_low') || 0; // Eur\nconst price_discharge_allowed = msg.payload.price_discharge_allowed; // Eur\n\nconst remaining_energy = Math.round((BAT_CAPACITY * BAT_SOC / 100) * 100) / 100; // kWh\nflow.set('remaining_energy', remaining_energy);\n\nconst missing_energy = Math.round((BAT_CAPACITY - remaining_energy) * 100) / 100; // kWh\nflow.set('missing_energy', missing_energy);\n\n// default values\nlet adaptive_soc = msg.payload.adaptive_soc; // %\nlet decision = 'Default';\n\nif (msg.payload.grid_lost_alarm == GRID_LOST)\n{\n    adaptive_soc = SOC_MIN;\n    decision = 'Grid lost';\n    setSwitchPosition(SWITCH_ON);\n}\nelse if (price_current < 0)\n{\n    adaptive_soc = SOC_MAX;\n    decision = 'Charge full';\n    setSwitchPosition(SWITCH_ON);\n}\nelse\n{\n    if (msg.payload.charge)\n    {\n        let soc_target = BAT_SOC;\n        if (CHARGE_STRATEGY == 2)\n        {\n            // Cover demand\n            soc_target = BAT_SOC + ENERGY_DEFICIT_PERC;\n        }\n        else\n        {\n            // Charge full\n            soc_target = SOC_MAX - SOLAR_EXCESS_PERC;\n        }\n\n        if (BAT_SOC < soc_target)\n        {\n            adaptive_soc = soc_target;\n            decision = 'Charge';\n            setSwitchPosition(SWITCH_ON);\n        }\n        else\n        {\n            adaptive_soc = BAT_SOC;\n            decision = 'Charge (Hold)';\n        }\n    }\n    else\n    {\n        if (msg.payload.discharge && \n            price_current > price_discharge_allowed &&\n            Math.round(BAT_SOC) > SOC_MIN)\n        {\n            adaptive_soc = SOC_MIN;\n            decision = 'Discharge';\n            setSwitchPosition(SWITCH_ON);\n        }\n        else if (msg.payload.surplus == true || \n                 msg.payload.solarcharger_limited == true)\n        {\n            adaptive_soc = BAT_SOC - 5;\n            decision = 'Surplus';\n            setSwitchPosition(SWITCH_ON);\n        }\n        else \n        {\n            adaptive_soc = BAT_SOC;\n            decision = 'Hold';\n            // Try to save some energy\n            if ((hour <= 10 || hour >= 18)\n                && BAT_SOC >= SOC_MIN)\n            {\n                setSwitchPosition(SWITCH_OFF);\n            }\n        }\n    }\n}\n\n// ensure we never get out of bounds\nif (adaptive_soc < SOC_MIN) { adaptive_soc = SOC_MIN; }\nif (adaptive_soc > SOC_MAX) { adaptive_soc = SOC_MAX; }\n\n// set ess state based on desired soc\nlet ess_state = ESS_STATE_OPTIMIZED;\nif (adaptive_soc >= SOC_MAX)\n{\n    ess_state = ESS_STATE_KEEP_CHARGED;\n}\n\nnode.status({text:decision + \" | \" + BAT_SOC + \"% -> \" + adaptive_soc + \"% | Dis: \" + Math.round(price_discharge_allowed * 1000) / 1000  + \" € | Cur: \" + Math.round(price_current * 1000) / 1000 + \" €\"});\n\nreturn [\n    {'payload': adaptive_soc},\n    {'payload': ess_state},\n    {'payload': switch_position},\n    {'payload': {\n            'adaptive_soc': adaptive_soc,\n            'decision': decision\n        }\n    }\n];","outputs":4,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":2170,"y":600,"wires":[["968404914029065c","ef01940a28b5c9dd"],["9617cc8d00c992c4","46c83f949670e056"],["eedc226eef4c1d41"],["adc8bfb0d4684295"]]},{"id":"0d4ee73769149161","type":"victron-input-system","z":"d1a79cb7a1857836","service":"com.victronenergy.system/0","path":"/Dc/Battery/Soc","serviceObj":{"service":"com.victronenergy.system/0","name":"Venus system"},"pathObj":{"path":"/Dc/Battery/Soc","type":"float","name":"Battery State of Charge (%)"},"name":"Get SoC","onlyChanges":false,"x":300,"y":860,"wires":[["cd877e00bb0d0227"]]},{"id":"cd877e00bb0d0227","type":"function","z":"d1a79cb7a1857836","name":"Store SoC","func":"flow.set('victron_soc', msg.payload);\nnode.status({text:\"\" + msg.payload + \" %\"});\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":860,"wires":[["04d5fc391c60e2db"]]},{"id":"63ce8d7211dd1baa","type":"change","z":"d1a79cb7a1857836","name":"topic = prices","rules":[{"t":"set","p":"topic","pt":"msg","to":"prices","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1160,"y":160,"wires":[["3891061b2e053a45","b8ef7fe4f2c7eec1","419227f8d2e8d4fd"]]},{"id":"3891061b2e053a45","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"3","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1570,"y":100,"wires":[["a2cddafcd9010905","ad493de0a9a147d7"]]},{"id":"fd2431b376d968b2","type":"change","z":"d1a79cb7a1857836","name":"topic = max_price","rules":[{"t":"set","p":"topic","pt":"msg","to":"max_price","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1390,"y":100,"wires":[["3891061b2e053a45"]]},{"id":"04d5fc391c60e2db","type":"change","z":"d1a79cb7a1857836","name":"topic = soc","rules":[{"t":"set","p":"topic","pt":"msg","to":"soc","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":860,"wires":[["b8ef7fe4f2c7eec1","2624dc40de5b4781","b0245ec37828d40c","419227f8d2e8d4fd","cd6a7f5c4a79d4d0"]]},{"id":"b30a95e9ee29a1d7","type":"change","z":"d1a79cb7a1857836","name":"topic = solar_excess","rules":[{"t":"set","p":"topic","pt":"msg","to":"solar_excess","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1660,"y":640,"wires":[["b8ef7fe4f2c7eec1"]]},{"id":"714bdde432f73713","type":"change","z":"d1a79cb7a1857836","name":"topic = charge","rules":[{"t":"set","p":"topic","pt":"msg","to":"charge","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2080,"y":140,"wires":[["b8ef7fe4f2c7eec1"]]},{"id":"b8ef7fe4f2c7eec1","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"13","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1970,"y":640,"wires":[["0b39e1bed02ad85f","4b51923829534099"]]},{"id":"fc05a5366de834cb","type":"inject","z":"d1a79cb7a1857836","name":"Startup","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"10","topic":"","payload":"","payloadType":"date","x":100,"y":160,"wires":[["a571f168cae8d985"]]},{"id":"46c83f949670e056","type":"victron-output-ess","z":"d1a79cb7a1857836","service":"com.victronenergy.settings","path":"/Settings/CGwacs/BatteryLife/State","serviceObj":{"service":"com.victronenergy.settings","name":"Venus settings"},"pathObj":{"path":"/Settings/CGwacs/BatteryLife/State","type":"enum","name":"ESS state","enum":{"1":"BatteryLife enabled (GUI controlled)","2":"Optimized Mode /w BatteryLife: self consumption","3":"Optimized Mode /w BatteryLife: self consumption, SoC exceeds 85%","4":"Optimized Mode /w BatteryLife: self consumption, SoC at 100%","5":"Optimized Mode /w BatteryLife: SoC below dynamic SoC limit","6":"Optimized Mode /w BatteryLife: SoC has been below SoC limit for more than 24 hours. Charging the battery (5A)","7":"Optimized Mode /w BatteryLife: Inverter/Charger is in sustain mode","8":"Optimized Mode /w BatteryLife: recharging, SoC dropped by 5% or more below the minimum SoC","9":"'Keep batteries charged' mode is enabled","10":"Optimized mode w/o BatteryLife: self consumption, SoC at or above minimum SoC","11":"Optimized mode w/o BatteryLife: self consumption, SoC is below minimum SoC","12":"Optimized mode w/o BatteryLife: recharging, SoC dropped by 5% or more below minimum SoC"},"writable":true},"initial":"10","name":"","onlyChanges":false,"x":2520,"y":500,"wires":[]},{"id":"9617cc8d00c992c4","type":"debug","z":"d1a79cb7a1857836","name":"ess_state","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":2460,"y":560,"wires":[]},{"id":"968404914029065c","type":"debug","z":"d1a79cb7a1857836","name":"adaptive_soc","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":2480,"y":440,"wires":[]},{"id":"a571f168cae8d985","type":"function","z":"d1a79cb7a1857836","name":"Global settings","func":"// VRM API\nflow.set(\"siteId\", XXXXXX); // VRM site id\n\n// Battery stats\nflow.set(\"bat_capacity\", 14.4);\nflow.set(\"bat_soc_min\", 20);\nflow.set(\"bat_soc_max\", 100);\n\n// Schedule settings\nflow.set(\"avg_charging_power\", 5); // kW\nflow.set(\"charge_hours_offset\", 0.5); // Time for balancing in h\nflow.set(\"max_charge_hours\", 4);\nflow.set(\"charge_after\", 21);\nflow.set(\"charge_before\", 15);\nflow.set(\"charge_strategy\", 2); // 1 = Charge full, 2 = Cover demand\n\n// Price per kwh solar power (Feed-in tariff)\nflow.set(\"price_pv\", 0.08); // €\n\n// Efficiency / conversion losses\nflow.set(\"charger_efficiency_factor\", 0.90);\nflow.set(\"inverter_efficiency_factor\", 0.92);\n\n// Storage costs per kwh\nflow.set(\"storage_costs\", 0.00);\n\n// Forecast multipliers\nflow.set('solar_yield_fc_multiplier', 1.00);\nflow.set('consumption_fc_multiplier', 1.00);\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":100,"wires":[["ef1725482f2d21a8","7828e71c7657f5c2","f1944ec2cff45ed0","bed83ac2c246fdbe","ca32f8223dadd84b","3f6e06198a0b6f6b","a2ef216fe7edb830","1c8172868801e1d1","b4d61848c4cd8744","5366c651a2ac02fd"]]},{"id":"adc8bfb0d4684295","type":"rbe","z":"d1a79cb7a1857836","name":"","func":"rbei","gap":"","start":"","inout":"out","septopics":true,"property":"payload.decision","topi":"topic","x":2450,"y":820,"wires":[["f0bfc8dbb6331638"]]},{"id":"f0bfc8dbb6331638","type":"function","z":"d1a79cb7a1857836","name":"Adaptive SoC message","func":"const adaptive_soc = msg.payload.adaptive_soc;\nconst decision = msg.payload.decision;\n\nconst soc = flow.get('victron_soc') || 0\nconst solar_forecast = flow.get('solar_forecast') || 0;\nconst solar_excess = flow.get('solar_excess') || 0;\nconst energy_deficit = flow.get('energy_deficit') || 0;\nconst remaining_energy = flow.get('remaining_energy') || 0;\nconst missing_energy = flow.get('missing_energy') || 0;\nconst price_current = flow.get('price_current') || 0;\nconst average_price = flow.get('average_price') || 0;\n\nmsg.payload = \"Remaining: \" + remaining_energy + \" kWh\\n\" +\n              \"Missing: \" + missing_energy + \" kWh\\n\" + \n              \"Forecast: \" + solar_forecast + \" kWh\\n\" +\n              \"Excess: \" + solar_excess + \" kWh\\n\" +\n              \"Energy deficit: \" + energy_deficit + \" kWh\\n\" +\n              \"Current SoC: \" + soc + \" %\\n\" +\n              \"Adaptive SoC: \" + adaptive_soc + \" %\\n\" +\n              \"Price current: \" + Math.round(price_current * 1000) / 1000 + \" Eur\\n\" +\n              \"Average price: \" + Math.round(average_price * 1000) / 1000 + \" Eur\\n\" +\n              \"Decision: \" + decision;\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":2710,"y":820,"wires":[["7f18f0875225b2b3"]]},{"id":"93b17a4b3da6be6a","type":"function","z":"d1a79cb7a1857836","name":"Status message","func":"const soc = flow.get('victron_soc') || 0\nconst solar_forecast = flow.get('solar_forecast') || 0;\nconst solar_excess = flow.get('solar_excess') || 0;\nconst energy_deficit = flow.get('energy_deficit') || 0;\nconst charge_hours = flow.get('charge_hours') || 0;\nconst remaining_energy = flow.get('remaining_energy') || 0;\nconst missing_energy = flow.get('missing_energy') || 0;\nconst price_low = flow.get('price_low') || 0;\nconst price_high = flow.get('price_high') || 0;\nconst price_max_allowed = flow.get('price_max_allowed') || 0;\nconst average_price = flow.get('average_price') || 0;\nconst price_discharge_allowed = flow.get('price_discharge_allowed') || 0;\n\nmsg.payload = \"Remaining: \" + remaining_energy + \" kWh\\n\" +\n              \"Missing: \" + missing_energy + \" kWh\\n\" + \n              \"Forecast: \" + solar_forecast + \" kWh\\n\" +\n              \"Excess: \" + solar_excess + \" kWh\\n\" +\n              \"Energy deficit: \" + energy_deficit + \" kWh\\n\" +\n              \"Charge hours: \" + charge_hours + \" h\\n\" +\n              \"Current SoC: \" + soc + \" %\\n\" +\n              \"Price low: \" + Math.round(price_low * 1000) / 1000 + \" Eur\\n\" +\n              \"Price high: \" + Math.round(price_high * 1000) / 1000 + \" Eur\\n\" +\n              \"Price max allowed: \" + Math.round(price_max_allowed * 1000) / 1000 + \" Eur\\n\" +\n              \"Average price: \" + Math.round(average_price * 1000) / 1000 + \" Eur\\n\" +\n              \"Price discharge allowed: \" + Math.round(price_discharge_allowed * 1000) / 1000 + \" Eur\";\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":2680,"y":860,"wires":[["7f18f0875225b2b3"]]},{"id":"8ee224baf35434a5","type":"victron-input-solarcharger","z":"d1a79cb7a1857836","service":"com.victronenergy.solarcharger/0","path":"/MppOperationMode","serviceObj":{"service":"com.victronenergy.solarcharger/0","name":"MPPT 450/100 HQ2220WRFXJ"},"pathObj":{"path":"/MppOperationMode","type":"enum","name":"MPP operation mode","enum":{"0":"Off","1":"Voltage or current limited","2":"MPPT Tracker active","255":"Not available"}},"initial":"","name":"","onlyChanges":false,"x":440,"y":1560,"wires":[["b5a267a99b6e1d17"]]},{"id":"c8b7bb0259c0186e","type":"victron-input-solarcharger","z":"d1a79cb7a1857836","service":"com.victronenergy.solarcharger/279","path":"/MppOperationMode","serviceObj":{"service":"com.victronenergy.solarcharger/279","name":"MPPT 150/35 HQ2151AXHZN"},"pathObj":{"path":"/MppOperationMode","type":"enum","name":"MPP operation mode","enum":{"0":"Off","1":"Voltage or current limited","2":"MPPT Tracker active","255":"Not available"}},"initial":"","name":"","onlyChanges":false,"x":430,"y":1500,"wires":[["428d22459c0cef45"]]},{"id":"428d22459c0cef45","type":"change","z":"d1a79cb7a1857836","name":"topic = solarcharger_1","rules":[{"t":"set","p":"topic","pt":"msg","to":"solarcharger_1","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":1500,"wires":[["b0245ec37828d40c"]]},{"id":"b5a267a99b6e1d17","type":"change","z":"d1a79cb7a1857836","name":"topic = solarcharger_2","rules":[{"t":"set","p":"topic","pt":"msg","to":"solarcharger_2","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":1560,"wires":[["b0245ec37828d40c"]]},{"id":"6afbb245e16b4cf1","type":"function","z":"d1a79cb7a1857836","name":"Check Solarcharger limited","func":"if (msg.payload.soc >= 90 \n    && (msg.payload.solarcharger_1 == 1 || msg.payload.solarcharger_2 == 1))\n{\n    msg.payload = true;\n}\nelse\n{\n    msg.payload = false;\n}\n\nnode.status({text:\"Limited: \" + msg.payload});\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1240,"y":1500,"wires":[["3ec10e12fbcb43d3","c6e6493a808b19e0"]]},{"id":"b0245ec37828d40c","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"3","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1010,"y":1500,"wires":[["6afbb245e16b4cf1"]]},{"id":"60707cc15061236b","type":"change","z":"d1a79cb7a1857836","name":"topic = solarcharger_limited","rules":[{"t":"set","p":"topic","pt":"msg","to":"solarcharger_limited","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1740,"y":1500,"wires":[["b8ef7fe4f2c7eec1","2624dc40de5b4781","aef45317c7adebcc"]]},{"id":"18036ad8fe0cc827","type":"victron-input-ess","z":"d1a79cb7a1857836","service":"com.victronenergy.settings","path":"/Settings/CGwacs/BatteryLife/MinimumSocLimit","serviceObj":{"service":"com.victronenergy.settings","name":"Venus settings"},"pathObj":{"path":"/Settings/CGwacs/BatteryLife/MinimumSocLimit","type":"integer","name":"Minimum Discharge SOC (%)"},"name":"","onlyChanges":false,"x":410,"y":1620,"wires":[["310cf3d3991435e3"]]},{"id":"222c2a5d5f7cc02d","type":"function","z":"d1a79cb7a1857836","name":"Surplus trigger","func":"if (msg.payload.solarcharger_limited == true && \n    msg.payload.soc - msg.payload.min_discharge_soc < 5)\n{\n    let now = new Date();\n    msg.payload = now.getTime();\n    node.status({text:\"Trigger\"});\n    return msg;\n}\n\nnode.status({text:\"Do nothing\"});\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2160,"y":1620,"wires":[["dd5d17e5305a6982"]]},{"id":"2624dc40de5b4781","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"3","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1970,"y":1620,"wires":[["222c2a5d5f7cc02d"]]},{"id":"310cf3d3991435e3","type":"change","z":"d1a79cb7a1857836","name":"topic = min_discharge_soc","rules":[{"t":"set","p":"topic","pt":"msg","to":"min_discharge_soc","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":1620,"wires":[["2624dc40de5b4781"]]},{"id":"1ce6916d7ff51198","type":"function","z":"d1a79cb7a1857836","name":"calculate average price","func":"const BAT_CAPACITY = flow.get('bat_capacity'); // kWh\nconst PRICE_PV = flow.get('price_pv'); // Eur\n\nconst CHARGER_EFFICIENCY_FACTOR = flow.get('charger_efficiency_factor') || 0.90;\nconst INVERTER_EFFICIENCY_FACTOR = flow.get('inverter_efficiency_factor') || 0.92;\nconst STORAGE_COSTS = flow.get('storage_costs') || 0.00;\n\nconst today = new Date();\nlet day = today.getDay();\nlet dom_today = today.getDate();\n\nlet yesterday = new Date();\nyesterday.setDate(yesterday.getDate() - 1);\nlet dom_yesterday = yesterday.getDate();\n\nlet average_price_template = [\n    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n    0\n];\n\nlet average_price_days = flow.get('average_price_days', 'file') || average_price_template;\nlet average_price = 0;\n//average_price_days[dom_yesterday] = 0.23;\nlet average_price_initial = average_price_days[dom_yesterday];\nlet costs = 0;\nlet kwh = 0;\nlet kwh_initial = 0;\nlet price_discharge_allowed = 0;\n\nif (\"bs\" in msg.payload.battery_soc.records)\n{\n    if (23 in msg.payload.battery_soc.records.bs)\n    {\n        kwh_initial = BAT_CAPACITY / 100 * msg.payload.battery_soc.records.bs[23][3];\n    }\n}\n\nlet tibber_hours = [\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0\n];\n\nlet bat_to_consumption_hours = [\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0\n];\n\nlet bat_to_grid_hours = [\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0\n];\n\nlet grid_to_bat_hours = [\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0\n];\n\nlet pv_to_bat_hours = [\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0\n];\n\nfunction parseRecords(hours, records)\n{\n    for(let i = 0; i < records.length; i++)\n    {\n        const d = new Date(records[i][0]);\n        const hour = d.getHours();\n        hours[hour] += records[i][1];\n    }\n}\n\nlet tibber_records = msg.payload.tibber.viewer.homes[0].currentSubscription.priceInfo.today;\nfor (let i = 0; i < tibber_records.length; i++)\n{\n    tibber_hours[i] += tibber_records[i].total;\n}\n\nif (\"Bc\" in msg.payload.battery_direct_use.records)\n{\n    parseRecords(bat_to_consumption_hours, msg.payload.battery_direct_use.records.Bc);\n}\n\nif (\"Bg\" in msg.payload.battery_to_grid.records)\n{\n    parseRecords(bat_to_grid_hours, msg.payload.battery_to_grid.records.Bg);\n}\n\nif (\"Gb\" in msg.payload.grid_to_battery.records)\n{\n    parseRecords(grid_to_bat_hours, msg.payload.grid_to_battery.records.Gb);\n}\n\nif (\"Pb\" in msg.payload.solar_to_battery.records)\n{\n    parseRecords(pv_to_bat_hours, msg.payload.solar_to_battery.records.Pb);\n}\n\n// subtract consumption from battery from transfer from previous day\n// first in, first out ;-)\nfor (let i = 0; i < 24; i++)\n{\n    // Bat to consumption\n    kwh_initial -= bat_to_consumption_hours[i];\n    // Bat to grid\n    kwh_initial -= bat_to_grid_hours[i];\n}\n\n// take over remaining capacity from yesterday\nif (kwh_initial > 0)\n{\n    costs += average_price_initial * kwh_initial;\n    kwh += kwh_initial;\n}\n\n// calculate average price\nfor (let i = 0; i < 24; i++)\n{\n    // From grid\n    costs += tibber_hours[i] * grid_to_bat_hours[i];\n    kwh += grid_to_bat_hours[i];\n    // From pv\n    costs += PRICE_PV * pv_to_bat_hours[i];\n    kwh += pv_to_bat_hours[i];\n}\naverage_price = costs / kwh;\nflow.set('average_price', average_price);\n\n// Store average price\naverage_price_days[dom_today] = average_price;\nflow.set('average_price_days', average_price_days, 'file');\n\n// Calculate the price at which discharging is permitted\nprice_discharge_allowed = average_price / (CHARGER_EFFICIENCY_FACTOR * INVERTER_EFFICIENCY_FACTOR) + STORAGE_COSTS;\nflow.set('price_discharge_allowed', price_discharge_allowed);\n\nmsg.payload = price_discharge_allowed;\n\nnode.status({text:\"Avg: \" + Math.round(average_price * 1000) / 1000 + \" € (\" + Math.round(average_price_initial * 1000) / 1000 + \" €) | Dis: \" + Math.round(price_discharge_allowed * 1000) / 1000 + \" €\"});\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1330,"y":560,"wires":[["207bbb0d590fb41b"]]},{"id":"b2a8df0aafa2c8dd","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"6","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1110,"y":560,"wires":[["1ce6916d7ff51198","e8419f65b2fa9bae"]]},{"id":"df121a781659dce6","type":"change","z":"d1a79cb7a1857836","name":"topic = tibber","rules":[{"t":"set","p":"topic","pt":"msg","to":"tibber","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":950,"y":160,"wires":[["b2a8df0aafa2c8dd"]]},{"id":"207bbb0d590fb41b","type":"change","z":"d1a79cb7a1857836","name":"topic = price_discharge_allowed","rules":[{"t":"set","p":"topic","pt":"msg","to":"price_discharge_allowed","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1690,"y":560,"wires":[["b8ef7fe4f2c7eec1","419227f8d2e8d4fd"]]},{"id":"92712bec52625aa2","type":"comment","z":"d1a79cb7a1857836","name":"Version: 1.2.1","info":"Last edited: 2024-03-21","x":110,"y":40,"wires":[]},{"id":"7828e71c7657f5c2","type":"vrm-api","z":"d1a79cb7a1857836","vrm":"ce0b2bd85123a79a","name":"","api_type":"installations","idUser":"","idSite":"{{flow.siteId}}","installations":"stats","attribute":"solar_yield_forecast","stats_interval":"hours","show_instance":false,"stats_start":"0","stats_end":"86400","use_utc":false,"instance":"","vrm_id":"","b_max":"","tb_max":"","fb_max":"","tg_max":"","fg_max":"","b_cycle_cost":"","buy_price_formula":"","sell_price_formula":"","feed_in_possible":true,"feed_in_control_on":true,"store_in_global_context":false,"verbose":false,"x":400,"y":740,"wires":[["3e6a7912a38bedfa"]]},{"id":"3e6a7912a38bedfa","type":"change","z":"d1a79cb7a1857836","name":"topic = solar_yield_fc","rules":[{"t":"set","p":"topic","pt":"msg","to":"solar_yield_fc","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":740,"wires":[["cd6a7f5c4a79d4d0"]]},{"id":"cd6a7f5c4a79d4d0","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"3","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1110,"y":660,"wires":[["9f0e9463ee0f7313","1de20f34fc04bbf9"]]},{"id":"9f0e9463ee0f7313","type":"function","z":"d1a79cb7a1857836","name":"calculate solar excess / energy deficit","func":"const BAT_CAPACITY = flow.get('bat_capacity');\nconst SOC_MIN = flow.get('bat_soc_min');\nconst BAT_SOC = msg.payload.soc; // %\n\nconst INVERTER_EFFICIENCY_FACTOR = flow.get('inverter_efficiency_factor') || 0.92;\n\nconst AVG_CHARGING_POWER = flow.get(\"avg_charging_power\");\nconst CHARGE_HOURS_OFFSET = flow.get(\"charge_hours_offset\");\nconst MAX_CHARGE_HOURS = flow.get(\"max_charge_hours\");\n\nconst remaining_energy = (BAT_CAPACITY - (BAT_CAPACITY / 100 * SOC_MIN)) * BAT_SOC / 100; // kWh\n\nconst d = new Date();\nlet day = d.getDay();\n\nlet est_consumption_hours = [\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0\n];\n\nlet solar_hours = [\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0,\n    0\n];\n\nfunction parseRecords(hours, records) {\n    for (let i = 0; i < records.length; i++)\n    {\n        const d = new Date(records[i][0]);\n        const hour = d.getHours();\n        hours[hour] += records[i][1] / 1000;\n    }\n}\n\nparseRecords(est_consumption_hours, msg.payload.consumption_fc.records.vrm_consumption_fc);\nparseRecords(solar_hours, msg.payload.solar_yield_fc.records.solar_yield_forecast);\n\nconst est_consumption_total = Math.round((msg.payload.consumption_fc.totals.vrm_consumption_fc) / 1000 * 100) / 100; // kWh\nconst solar_total = Math.round((msg.payload.solar_yield_fc.totals.solar_yield_forecast) / 1000 * 100) / 100; // kWh\n\nlet solar_excess = 0;\nfor (let i = 0; i < solar_hours.length; i++)\n{\n    if (solar_hours[i] > est_consumption_hours[i])\n    {\n        solar_excess += solar_hours[i] - est_consumption_hours[i];\n    }\n}\nsolar_excess = Math.round(solar_excess * 100) / 100;\n\nlet uncovered_consumption = 0;\nfor (let i = 0; i < est_consumption_hours.length; i++)\n{\n    if (est_consumption_hours[i] > solar_hours[i])\n    {\n        uncovered_consumption += est_consumption_hours[i] - solar_hours[i];\n    }\n}\n\nlet energy_deficit = (Math.round(((remaining_energy * INVERTER_EFFICIENCY_FACTOR) - uncovered_consumption) * 100) / 100) * -1;\n\nlet charge_hours = 0;\nif (energy_deficit > 0)\n{\n    charge_hours = Math.ceil(energy_deficit / AVG_CHARGING_POWER + CHARGE_HOURS_OFFSET);\n}\n\nif (charge_hours > MAX_CHARGE_HOURS)\n{\n    charge_hours = MAX_CHARGE_HOURS;\n}\n\nflow.set('solar_forecast', solar_total);\nflow.set('solar_excess', solar_excess);\nflow.set('energy_deficit', energy_deficit);\nflow.set('charge_hours', charge_hours);\n\nnode.status({text: `Con: ${est_consumption_total} kWh | Sol: ${solar_total} kWh | Exc/Def: ${solar_excess}/${energy_deficit} kWh`});\n\nreturn [\n    {\n        payload: charge_hours\n    },\n    {\n        payload: solar_excess\n    },\n    {\n        payload: energy_deficit\n    }\n];","outputs":3,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1370,"y":620,"wires":[["6a8428e9f363b394"],["b30a95e9ee29a1d7"],["d816d14e0ce49a1a"]]},{"id":"4b51923829534099","type":"debug","z":"d1a79cb7a1857836","name":"ess control logic input","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":2180,"y":680,"wires":[]},{"id":"bed83ac2c246fdbe","type":"vrm-api","z":"d1a79cb7a1857836","vrm":"ce0b2bd85123a79a","name":"","api_type":"installations","idUser":"","idSite":"{{flow.siteId}}","installations":"stats","attribute":"vrm_consumption_fc","stats_interval":"hours","show_instance":false,"stats_start":"0","stats_end":"86400","use_utc":false,"instance":"","vrm_id":"","b_max":"","tb_max":"","fb_max":"","tg_max":"","fg_max":"","b_cycle_cost":"","buy_price_formula":"","sell_price_formula":"","feed_in_possible":true,"feed_in_control_on":true,"store_in_global_context":false,"verbose":false,"x":400,"y":800,"wires":[["f7dce2730550cdf9"]]},{"id":"f7dce2730550cdf9","type":"change","z":"d1a79cb7a1857836","name":"topic = consumption_fc","rules":[{"t":"set","p":"topic","pt":"msg","to":"consumption_fc","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":800,"wires":[["cd6a7f5c4a79d4d0"]]},{"id":"f1944ec2cff45ed0","type":"vrm-api","z":"d1a79cb7a1857836","vrm":"ce0b2bd85123a79a","name":"","api_type":"installations","idUser":"","idSite":"{{flow.siteId}}","installations":"stats","attribute":"Bc","stats_interval":"hours","show_instance":false,"stats_start":"bod","stats_end":"eod","use_utc":false,"instance":"","vrm_id":"","b_max":"","tb_max":"","fb_max":"","tg_max":"","fg_max":"","b_cycle_cost":"","buy_price_formula":"","sell_price_formula":"","feed_in_possible":true,"feed_in_control_on":true,"store_in_global_context":false,"verbose":false,"x":340,"y":500,"wires":[["8b365e308b481388"]]},{"id":"8b365e308b481388","type":"change","z":"d1a79cb7a1857836","name":"topic = battery_direct_use","rules":[{"t":"set","p":"topic","pt":"msg","to":"battery_direct_use","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":840,"y":500,"wires":[["b2a8df0aafa2c8dd"]]},{"id":"ca32f8223dadd84b","type":"vrm-api","z":"d1a79cb7a1857836","vrm":"ce0b2bd85123a79a","name":"","api_type":"installations","idUser":"","idSite":"{{flow.siteId}}","installations":"stats","attribute":"Bg","stats_interval":"hours","show_instance":false,"stats_start":"bod","stats_end":"eod","use_utc":false,"instance":"","vrm_id":"","b_max":"","tb_max":"","fb_max":"","tg_max":"","fg_max":"","b_cycle_cost":"","buy_price_formula":"","sell_price_formula":"","feed_in_possible":true,"feed_in_control_on":true,"store_in_global_context":false,"verbose":false,"x":350,"y":560,"wires":[["3a38cbf946a8ce91"]]},{"id":"3f6e06198a0b6f6b","type":"vrm-api","z":"d1a79cb7a1857836","vrm":"ce0b2bd85123a79a","name":"","api_type":"installations","idUser":"","idSite":"{{flow.siteId}}","installations":"stats","attribute":"Pb","stats_interval":"hours","show_instance":false,"stats_start":"bod","stats_end":"eod","use_utc":false,"instance":"","vrm_id":"","b_max":"","tb_max":"","fb_max":"","tg_max":"","fg_max":"","b_cycle_cost":"","buy_price_formula":"","sell_price_formula":"","feed_in_possible":true,"feed_in_control_on":true,"store_in_global_context":false,"verbose":false,"x":350,"y":680,"wires":[["d1eb4f8d0705e882"]]},{"id":"3a38cbf946a8ce91","type":"change","z":"d1a79cb7a1857836","name":"topic = battery_to_grid","rules":[{"t":"set","p":"topic","pt":"msg","to":"battery_to_grid","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":560,"wires":[["b2a8df0aafa2c8dd"]]},{"id":"d1eb4f8d0705e882","type":"change","z":"d1a79cb7a1857836","name":"topic = solar_to_battery","rules":[{"t":"set","p":"topic","pt":"msg","to":"solar_to_battery","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":680,"wires":[["b2a8df0aafa2c8dd"]]},{"id":"e8419f65b2fa9bae","type":"debug","z":"d1a79cb7a1857836","name":"calculate average price input","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1340,"y":520,"wires":[]},{"id":"a2ef216fe7edb830","type":"vrm-api","z":"d1a79cb7a1857836","vrm":"ce0b2bd85123a79a","name":"","api_type":"installations","idUser":"","idSite":"{{flow.siteId}}","installations":"stats","attribute":"bs","stats_interval":"hours","show_instance":false,"stats_start":"boy","stats_end":"eoy","use_utc":false,"instance":"","vrm_id":"","b_max":"","tb_max":"","fb_max":"","tg_max":"","fg_max":"","b_cycle_cost":"","buy_price_formula":"","sell_price_formula":"","feed_in_possible":true,"feed_in_control_on":true,"store_in_global_context":false,"verbose":false,"x":340,"y":440,"wires":[["0eb0ecb4299d0976"]]},{"id":"0eb0ecb4299d0976","type":"change","z":"d1a79cb7a1857836","name":"topic = battery_soc","rules":[{"t":"set","p":"topic","pt":"msg","to":"battery_soc","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":810,"y":440,"wires":[["b2a8df0aafa2c8dd"]]},{"id":"480e154f5f614d07","type":"comment","z":"d1a79cb7a1857836","name":"energy from yesterday","info":"energy carried over from the previous day","x":580,"y":440,"wires":[]},{"id":"ce1a236997f721e8","type":"comment","z":"d1a79cb7a1857836","name":"Please remove if you are not using a Victron Solar Charger","info":"","x":460,"y":1460,"wires":[]},{"id":"6a2eb5107bfa721f","type":"comment","z":"d1a79cb7a1857836","name":"Please remove if you do not use Pushbullet","info":"","x":2570,"y":780,"wires":[]},{"id":"1c8172868801e1d1","type":"vrm-api","z":"d1a79cb7a1857836","vrm":"ce0b2bd85123a79a","name":"","api_type":"installations","idUser":"","idSite":"{{flow.siteId}}","installations":"stats","attribute":"Gb","stats_interval":"hours","show_instance":false,"stats_start":"bod","stats_end":"eod","use_utc":false,"instance":"","vrm_id":"","b_max":"","tb_max":"","fb_max":"","tg_max":"","fg_max":"","b_cycle_cost":"","buy_price_formula":"","sell_price_formula":"","feed_in_possible":true,"feed_in_control_on":true,"store_in_global_context":false,"verbose":false,"x":350,"y":620,"wires":[["e1ad4bf768dc04e4"]]},{"id":"e1ad4bf768dc04e4","type":"change","z":"d1a79cb7a1857836","name":"topic = grid_to_battery","rules":[{"t":"set","p":"topic","pt":"msg","to":"grid_to_battery","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":620,"wires":[["b2a8df0aafa2c8dd"]]},{"id":"1de20f34fc04bbf9","type":"debug","z":"d1a79cb7a1857836","name":"solar yield forecast input","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1330,"y":680,"wires":[]},{"id":"b4d61848c4cd8744","type":"vrm-api","z":"d1a79cb7a1857836","vrm":"ce0b2bd85123a79a","name":"","api_type":"installations","idUser":"","idSite":"{{flow.siteId}}","installations":"stats","attribute":"vrm_consumption_fc","stats_interval":"hours","show_instance":false,"stats_start":"0","stats_end":"eod","use_utc":false,"instance":"","vrm_id":"","b_max":"","tb_max":"","fb_max":"","tg_max":"","fg_max":"","b_cycle_cost":"","buy_price_formula":"","sell_price_formula":"","feed_in_possible":true,"feed_in_control_on":true,"store_in_global_context":false,"verbose":false,"x":400,"y":280,"wires":[["779c3a8b1d9ed41d"]]},{"id":"779c3a8b1d9ed41d","type":"change","z":"d1a79cb7a1857836","name":"topic = consumption_fc","rules":[{"t":"set","p":"topic","pt":"msg","to":"consumption_fc","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":280,"wires":[["419227f8d2e8d4fd"]]},{"id":"419227f8d2e8d4fd","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"5","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1570,"y":200,"wires":[["60274e06a2c53d4e","c632cb78cc313a42"]]},{"id":"88f4d59907d02511","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":2450,"y":140,"wires":[["7886a9ba56d32feb"]]},{"id":"dd3de5b805c628dd","type":"change","z":"d1a79cb7a1857836","name":"topic = schedule_charge","rules":[{"t":"set","p":"topic","pt":"msg","to":"schedule_charge","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2110,"y":100,"wires":[["88f4d59907d02511"]]},{"id":"c70c60143ab75625","type":"change","z":"d1a79cb7a1857836","name":"topic = schedule_discharge","rules":[{"t":"set","p":"topic","pt":"msg","to":"schedule_discharge","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2120,"y":200,"wires":[["88f4d59907d02511"]]},{"id":"a744cfad78f0702d","type":"change","z":"d1a79cb7a1857836","name":"topic = discharge","rules":[{"t":"set","p":"topic","pt":"msg","to":"discharge","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2090,"y":240,"wires":[["b8ef7fe4f2c7eec1"]]},{"id":"41e5f026fa3c0ae1","type":"victron-output-vebus","z":"d1a79cb7a1857836","service":"com.victronenergy.vebus/276","path":"/Mode","serviceObj":{"service":"com.victronenergy.vebus/276","name":"MultiPlus-II 48/5000/70-50"},"pathObj":{"path":"/Mode","type":"enum","name":"Switch Position","remarks":"<p>Note that <tt>/ModeIsAdjustable</tt> needs to be set to 1.</p> ","enum":{"1":"Charger Only","2":"Inverter Only","3":"On","4":"Off"},"writable":true},"initial":"","name":"","onlyChanges":false,"x":2810,"y":580,"wires":[]},{"id":"a5908a24dad4772b","type":"debug","z":"d1a79cb7a1857836","name":"switch_position","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":2720,"y":660,"wires":[]},{"id":"3b354650bdbce5cc","type":"victron-input-vebus","z":"d1a79cb7a1857836","service":"com.victronenergy.vebus/276","path":"/Mode","serviceObj":{"service":"com.victronenergy.vebus/276","name":"MultiPlus-II 48/5000/70-50"},"pathObj":{"path":"/Mode","type":"enum","name":"Switch Position","enum":{"1":"Charger Only","2":"Inverter Only","3":"On","4":"Off"}},"initial":"","name":"","onlyChanges":false,"x":400,"y":1040,"wires":[["b559980afec365e4"]]},{"id":"a57d27a3a979ccf7","type":"victron-input-system","z":"d1a79cb7a1857836","service":"com.victronenergy.system/0","path":"/Ac/Grid/L1/Power","serviceObj":{"service":"com.victronenergy.system/0","name":"Venus system"},"pathObj":{"path":"/Ac/Grid/L1/Power","type":"float","name":"Grid L1 (W)"},"name":"","onlyChanges":false,"x":360,"y":1240,"wires":[["7e69b910514ce3fd"]]},{"id":"0c98c3287ab8e089","type":"victron-input-system","z":"d1a79cb7a1857836","service":"com.victronenergy.system/0","path":"/Ac/Grid/L2/Power","serviceObj":{"service":"com.victronenergy.system/0","name":"Venus system"},"pathObj":{"path":"/Ac/Grid/L2/Power","type":"float","name":"Grid L2 (W)"},"name":"","onlyChanges":false,"x":360,"y":1300,"wires":[["1c823d53d246b8d2"]]},{"id":"36388eb3077c3cf0","type":"victron-input-system","z":"d1a79cb7a1857836","service":"com.victronenergy.system/0","path":"/Ac/Grid/L3/Power","serviceObj":{"service":"com.victronenergy.system/0","name":"Venus system"},"pathObj":{"path":"/Ac/Grid/L3/Power","type":"float","name":"Grid L3 (W)"},"name":"","onlyChanges":false,"x":360,"y":1360,"wires":[["8d220898fe02f447"]]},{"id":"34a1620fd1fcca06","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1750,"y":1300,"wires":[["3e5e33aaf12d44ed"]]},{"id":"b559980afec365e4","type":"change","z":"d1a79cb7a1857836","name":"topic = switch_position","rules":[{"t":"set","p":"topic","pt":"msg","to":"switch_position","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":1040,"wires":[["34a1620fd1fcca06","b8ef7fe4f2c7eec1","4dd6852eca6eced0"]]},{"id":"7e69b910514ce3fd","type":"change","z":"d1a79cb7a1857836","name":"topic = grid_l1","rules":[{"t":"set","p":"topic","pt":"msg","to":"grid_l1","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":1240,"wires":[["3eb3ffe8812ad760"]]},{"id":"1c823d53d246b8d2","type":"change","z":"d1a79cb7a1857836","name":"topic = grid_l2","rules":[{"t":"set","p":"topic","pt":"msg","to":"grid_l2","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":1300,"wires":[["3eb3ffe8812ad760"]]},{"id":"8d220898fe02f447","type":"change","z":"d1a79cb7a1857836","name":"topic = grid_l3","rules":[{"t":"set","p":"topic","pt":"msg","to":"grid_l3","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":1360,"wires":[["3eb3ffe8812ad760"]]},{"id":"3e5e33aaf12d44ed","type":"function","z":"d1a79cb7a1857836","name":"Surplus trigger","func":"const SWITCH_ON = 3;\n\nif (msg.payload.switch_position != SWITCH_ON\n    && msg.payload.surplus ==  true)\n{\n    let now = new Date();\n    msg.payload = now.getTime();\n    node.status({text:\"Trigger\"});\n    return msg;\n}\nelse\n{\n    node.status({text:\"Do nothing\"});\n}\nreturn null;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1940,"y":1300,"wires":[["2aa20581108dc4d8"]]},{"id":"3eb3ffe8812ad760","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"3","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":930,"y":1300,"wires":[["8b699c8287dcbfb1"]]},{"id":"8b699c8287dcbfb1","type":"function","z":"d1a79cb7a1857836","name":"Detect grid feed-in","func":"let grid_total = Math.round(msg.payload.grid_l1 + msg.payload.grid_l2 + msg.payload.grid_l3);\n\nif (grid_total <= -100)\n{\n    msg.payload = true;\n}\nelse\n{\n    msg.payload = false;\n}\n\nnode.status({text:\"Grid: \" + grid_total + \" W | Surplus: \" + msg.payload});\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1130,"y":1300,"wires":[["fbf1a30c49434d44","81379ed981c3ed2b"]]},{"id":"c8ceeaf753e7f12d","type":"change","z":"d1a79cb7a1857836","name":"topic = surplus","rules":[{"t":"set","p":"topic","pt":"msg","to":"surplus","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1300,"wires":[["b8ef7fe4f2c7eec1","34a1620fd1fcca06","f86cd6d9f915105a"]]},{"id":"e38a347e2be8b3ae","type":"change","z":"d1a79cb7a1857836","name":"topic = battery_current","rules":[{"t":"set","p":"topic","pt":"msg","to":"battery_current","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":1100,"wires":[["b8ef7fe4f2c7eec1"]]},{"id":"a8dfe3de956867e8","type":"victron-input-vebus","z":"d1a79cb7a1857836","service":"com.victronenergy.vebus/276","path":"/Dc/0/Current","serviceObj":{"service":"com.victronenergy.vebus/276","name":"MultiPlus-II 48/5000/70-50"},"pathObj":{"path":"/Dc/0/Current","type":"float","name":"Battery current (A)"},"initial":"","name":"","onlyChanges":false,"x":420,"y":1100,"wires":[["e38a347e2be8b3ae"]]},{"id":"3a3905c9adb6f705","type":"comment","z":"d1a79cb7a1857836","name":"Surplus detection","info":"","x":320,"y":1200,"wires":[]},{"id":"eedc226eef4c1d41","type":"delay","z":"d1a79cb7a1857836","name":"","pauseType":"delay","timeout":"10","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":2490,"y":620,"wires":[["a5908a24dad4772b","41e5f026fa3c0ae1"]]},{"id":"ef01940a28b5c9dd","type":"rbe","z":"d1a79cb7a1857836","name":"","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":2450,"y":380,"wires":[["93b0b1a9790769a3"]]},{"id":"fbf1a30c49434d44","type":"trigger","z":"d1a79cb7a1857836","name":"","op1":"","op2":"","op1type":"nul","op2type":"pay","duration":"30","extend":false,"overrideDelay":false,"units":"s","reset":"false","bytopic":"all","topic":"topic","outputs":1,"x":1350,"y":1260,"wires":[["c8ceeaf753e7f12d"]]},{"id":"3ec10e12fbcb43d3","type":"trigger","z":"d1a79cb7a1857836","name":"","op1":"","op2":"","op1type":"nul","op2type":"pay","duration":"30","extend":false,"overrideDelay":false,"units":"s","reset":"false","bytopic":"all","topic":"topic","outputs":1,"x":1490,"y":1460,"wires":[["60707cc15061236b"]]},{"id":"f86cd6d9f915105a","type":"debug","z":"d1a79cb7a1857836","name":"surplus","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1760,"y":1340,"wires":[]},{"id":"aef45317c7adebcc","type":"debug","z":"d1a79cb7a1857836","name":"limited","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1970,"y":1500,"wires":[]},{"id":"81379ed981c3ed2b","type":"function","z":"d1a79cb7a1857836","name":"filter true","func":"if (msg.payload == false)\n{\n    return msg;\n}\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1340,"y":1340,"wires":[["c8ceeaf753e7f12d"]]},{"id":"c6e6493a808b19e0","type":"function","z":"d1a79cb7a1857836","name":"filter true","func":"if (msg.payload == false)\n{\n    return msg;\n}\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1480,"y":1520,"wires":[["60707cc15061236b"]]},{"id":"714ffe19b466f379","type":"victron-input-vebus","z":"d1a79cb7a1857836","service":"com.victronenergy.vebus/276","path":"/Alarms/GridLost","serviceObj":{"service":"com.victronenergy.vebus/276","name":"MultiPlus-II 48/5000/70-50"},"pathObj":{"path":"/Alarms/GridLost","type":"enum","name":"Grid lost alarm","enum":{"0":"Ok","2":"Alarm"}},"initial":"","name":"","onlyChanges":false,"x":400,"y":980,"wires":[["d91652f8ef35da61"]]},{"id":"d91652f8ef35da61","type":"change","z":"d1a79cb7a1857836","name":"topic = grid_lost_alarm","rules":[{"t":"set","p":"topic","pt":"msg","to":"grid_lost_alarm","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":980,"wires":[["4dd6852eca6eced0","b8ef7fe4f2c7eec1"]]},{"id":"4dd6852eca6eced0","type":"join","z":"d1a79cb7a1857836","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1890,"y":1020,"wires":[["49875497d0f3e1b4"]]},{"id":"49875497d0f3e1b4","type":"function","z":"d1a79cb7a1857836","name":"Check if grid lost","func":"const GRID_LOST = 2;\nconst SWITCH_ON = 3;\n\nif (msg.payload.grid_lost_alarm == GRID_LOST &&\n    msg.payload.switch_position != SWITCH_ON)\n{\n    node.status({text:\"Turn Multiplus on\"});\n    msg.payload = SWITCH_ON;\n    return msg;\n}\nelse\n{\n    node.status({text:\"Do nothing\"});\n}\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2090,"y":1020,"wires":[["eedc226eef4c1d41"]]},{"id":"5366c651a2ac02fd","type":"vrm-api","z":"d1a79cb7a1857836","vrm":"ce0b2bd85123a79a","name":"","api_type":"installations","idUser":"","idSite":"{{flow.siteId}}","installations":"stats","attribute":"solar_yield_forecast","stats_interval":"hours","show_instance":false,"stats_start":"0","stats_end":"eod","use_utc":false,"instance":"","vrm_id":"","b_max":"","tb_max":"","fb_max":"","tg_max":"","fg_max":"","b_cycle_cost":"","buy_price_formula":"","sell_price_formula":"","feed_in_possible":true,"feed_in_control_on":true,"store_in_global_context":false,"verbose":false,"x":400,"y":340,"wires":[["add442626a0491ca"]]},{"id":"add442626a0491ca","type":"change","z":"d1a79cb7a1857836","name":"topic = solar_yield_fc","rules":[{"t":"set","p":"topic","pt":"msg","to":"solar_yield_fc","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":340,"wires":[["419227f8d2e8d4fd"]]},{"id":"60274e06a2c53d4e","type":"function","z":"d1a79cb7a1857836","name":"calculate discharge schedule","func":"const TODAY = new Date(new Date().setHours(0, 0, 0, 0));\nconst YESTERDAY = new Date(new Date(TODAY).setHours(-24));\nconst TOMORROW = new Date(new Date(TODAY).setHours(24));\n\nconst BAT_CAPACITY = flow.get('bat_capacity'); // kWh\nconst SOC_MIN = flow.get('bat_soc_min'); // %\nconst SOC_MAX = flow.get('bat_soc_max'); // %\nconst INVERTER_EFFICIENCY_FACTOR = flow.get('inverter_efficiency_factor');\nconst CHARGER_EFFICIENCY_FACTOR = flow.get('charger_efficiency_factor');\nconst BAT_USABLE_CAPACITY = BAT_CAPACITY / 100 * (SOC_MAX - SOC_MIN);\nconst SOLAR_YIELD_FC_MULTIPLIER = flow.get('solar_yield_fc_multiplier');\nconst CONSUMPTION_FC_MULTIPLIER = flow.get('consumption_fc_multiplier');\nlet history = context.get('history') || [];\n\nconst soc_bat = msg.payload.soc; // %\nconst price_data = msg.payload.prices.priceData;\nconst price_discharge_allowed = msg.payload.price_discharge_allowed;\n\nlet bat_current_capacity = 0;\nif(soc_bat > SOC_MIN)\n{\n    bat_current_capacity = BAT_CAPACITY / 100 * (soc_bat - SOC_MIN);\n}\n\nif(!('vrm_consumption_fc' in msg.payload.consumption_fc.records) ||\n   !('solar_yield_forecast' in msg.payload.solar_yield_fc.records))\n{\n    node.status({text: `Discharge: true | FC NOT AVAILABLE`});\n    \n    return [\n        {\n            payload: {\n                hours: []\n            }\n        },\n        {\n            payload: true\n        }\n    ];    \n}\n\nconst consumption_fc = msg.payload.consumption_fc.records.vrm_consumption_fc;\nconst solar_yield_fc = msg.payload.solar_yield_fc.records.solar_yield_forecast;\n\nconst effective_consumption = [];\n\nfor (let i = 0; i < Math.min(consumption_fc.length, solar_yield_fc.length); i++)\n{\n    const date = new Date(consumption_fc[i][0]);\n    const hour = date.getHours();\n    \n    const start = date.toISOString();\n    const price = price_data[hour].value;\n\n    const consumption = consumption_fc[i][1] * CONSUMPTION_FC_MULTIPLIER;\n    const solar_yield = solar_yield_fc[i][1] * SOLAR_YIELD_FC_MULTIPLIER;\n    let value = (consumption - solar_yield) / 1000; // kWh\n    \n    if(value < 0)\n    {\n        //value *= CHARGER_EFFICIENCY_FACTOR;\n        bat_current_capacity += value * -1;\n    }\n\n    effective_consumption.push(\n        {\n            start,\n            value,\n            price,\n            onOff: true // no discharge\n        }\n    );\n}\n\nif(bat_current_capacity > BAT_USABLE_CAPACITY)\n{\n    bat_current_capacity = BAT_USABLE_CAPACITY;\n}\n\nlet bat_capacity_used = 0;\nlet discharge = false;\n\nArray.from(effective_consumption).sort((a, b) => {\n    return b.price - a.price;\n}).filter((entry) => {\n    return entry.price >= price_discharge_allowed;\n}).forEach((entry) => {\n    if((bat_current_capacity - bat_capacity_used) > 0)\n    {\n        if(entry.value > 0)\n        {\n            bat_capacity_used += entry.value;\n        }\n        entry.onOff = false; // discharge\n        \n        const hour = (new Date()).getHours()\n        if(hour == (new Date(entry.start)).getHours())\n        {\n            discharge = true;\n        }\n    }\n});\n\n// add end of today entry so the schedule looks nicer\neffective_consumption.push(\n    {\n        start: TOMORROW.toISOString(),\n        value: 0,\n        price: (24 in price_data) ? price_data[24].value : price_data[23].value,\n        onOff: true // no discharge\n    }\n);\n\n// remove entries older then yesterday from the history\nhistory = history.filter((entry) => {\n    return new Date(entry.start) >= YESTERDAY;\n});\n\n// add/update current/future hours in history\neffective_consumption.forEach((entry) => {\n    const i = history.findIndex((e) => e.start == entry.start);\n    if(i >= 0)\n    {\n        // update\n        history[i] = entry;\n    }\n    else\n    {\n        // add\n        history.push(entry);\n        \n    }\n});\n\ncontext.set('history', history);\n\nnode.status({text: `Discharge: ${discharge}`});\n\nreturn [\n    {\n        payload: {\n            hours: history\n        }\n    },\n    {\n        payload: discharge\n    }\n];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1780,"y":220,"wires":[["c70c60143ab75625"],["a744cfad78f0702d"]]},{"id":"c632cb78cc313a42","type":"debug","z":"d1a79cb7a1857836","name":"calculate discharge schedule input","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1800,"y":180,"wires":[]},{"id":"9075c65ba96eacb2","type":"link in","z":"d1a79cb7a1857836","name":"trigger","links":["2aa20581108dc4d8","dd5d17e5305a6982"],"x":145,"y":220,"wires":[["a571f168cae8d985"]]},{"id":"2aa20581108dc4d8","type":"link out","z":"d1a79cb7a1857836","name":"link out 1","mode":"link","links":["9075c65ba96eacb2"],"x":2095,"y":1300,"wires":[]},{"id":"dd5d17e5305a6982","type":"link out","z":"d1a79cb7a1857836","name":"link out 2","mode":"link","links":["9075c65ba96eacb2"],"x":2315,"y":1620,"wires":[]},{"id":"996b73d47b2c338e","type":"victron-input-ess","z":"d1a79cb7a1857836","service":"com.victronenergy.settings","path":"/Settings/CGwacs/BatteryLife/MinimumSocLimit","serviceObj":{"service":"com.victronenergy.settings","name":"Venus settings"},"pathObj":{"path":"/Settings/CGwacs/BatteryLife/MinimumSocLimit","type":"integer","name":"Minimum Discharge SOC (%)"},"name":"","onlyChanges":false,"x":410,"y":920,"wires":[["eac117cf29973e1a"]]},{"id":"eac117cf29973e1a","type":"change","z":"d1a79cb7a1857836","name":"topic = adaptive_soc","rules":[{"t":"set","p":"topic","pt":"msg","to":"adaptive_soc","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":920,"wires":[["b8ef7fe4f2c7eec1"]]},{"id":"a2cddafcd9010905","type":"function","z":"d1a79cb7a1857836","name":"calculate charge schedule","func":"const CONTEXT = 'file';\n\nconst CHARGE_AFTER = flow.get(\"charge_after\");\nconst CHARGE_BEFORE = flow.get(\"charge_before\");\n\nconst today = new Date(new Date().setHours(0, 0, 0, 0));\nconst yesterday = new Date(new Date(today).setHours(-24));\nconst tomorrow = new Date(new Date(today).setHours(24));\nconst current_hour = (new Date()).getHours();\n\nlet history = context.get('history', CONTEXT) || [];\n\nconst price_data = msg.payload.prices.priceData;\nconst max_price = msg.payload.max_price;\nconst charge_hours = msg.payload.charge_hours;\n\n// remove entries older then yesterday from the history\nhistory = history.filter((entry) => {\n    return new Date(entry.start) >= yesterday;\n});\n\n// add/update current/future prices in history\nprice_data.forEach((entry) => {\n    // don't touch hours that are in the past\n    if(Date.parse(entry.start) < new Date().setMinutes(0, 0, 0))\n    {\n        return;\n    }\n\n    const i = history.findIndex((e) => Date.parse(e.start) == Date.parse(entry.start));\n    if (i >= 0)\n    {\n        // update\n        history[i].price = entry.value;\n        history[i].onOff = false;\n    }\n    else\n    {\n        // add\n        history.push({\n            start: entry.start,\n            price: entry.value,\n            onOff: false\n        });\n    }\n});\n\nlet charge_after_day;\nlet charge_before_day;\n\nif (CHARGE_AFTER < CHARGE_BEFORE) // charge time does not cross day boundary\n{\n    if (current_hour > CHARGE_AFTER && current_hour >= CHARGE_BEFORE)\n    {\n        // charge time has already passed for today\n        charge_after_day = tomorrow;\n        charge_before_day = tomorrow;\n    }\n    else\n    {\n        // charge time is sometime today or we are inside it\n        charge_after_day = today;\n        charge_before_day = today;\n    }\n}\nelse // charge time crosses day boundary\n{\n    if (current_hour >= CHARGE_AFTER)\n    {\n        // we are inside the charge time before midnight\n        charge_after_day = today;\n        charge_before_day = tomorrow;\n    }\n    else if (current_hour < CHARGE_BEFORE)\n    {\n        // we are inside the charge time after midnight\n        charge_after_day = yesterday;\n        charge_before_day = today;\n    }\n    else\n    {\n        // next charge time is later today\n        charge_after_day = today;\n        charge_before_day = tomorrow;\n    }\n}\n\nconst charge_after_date = new Date(new Date(charge_after_day).setHours(CHARGE_AFTER));\nconst charge_before_date = new Date(new Date(charge_before_day).setHours(CHARGE_BEFORE));\n\nconst charge_after_idx = history.findIndex((e) => Date.parse(e.start) == charge_after_date.getTime());\nconst charge_before_idx = history.findIndex((e) => Date.parse(e.start) == charge_before_date.getTime());\n\nlet charge = false;\n\nif ((charge_after_idx >= 0) && (charge_before_idx >= 0))\n{\n    let best_start_idx = -1;\n    let duration = 0;\n    let best_price_sum = 1000000;\n    for (let consecutive_hours = charge_hours; consecutive_hours > 0 && duration == 0; consecutive_hours--)\n    {\n        for (let start_idx = charge_after_idx; start_idx <= (charge_before_idx - consecutive_hours); start_idx++)\n        {\n            let charge_allowed = true;\n            let price_sum = 0;\n            for (let i = start_idx; i < start_idx + consecutive_hours; i++)\n            {\n                const price = history[i].price;\n                price_sum += price;\n                if(price >= max_price)\n                {\n                    charge_allowed = false;\n                    break;\n                }\n            }\n            if (charge_allowed && price_sum < best_price_sum)\n            {\n                best_price_sum = price_sum;\n                duration = consecutive_hours;\n                best_start_idx = start_idx;\n            }\n        }\n    }\n    \n    if (best_start_idx >= 0)\n    {\n        for (let i = best_start_idx; i < best_start_idx + duration; i++)\n        {\n            // don't touch hours that are in the past\n            if(new Date(history[i].start) < new Date(new Date().setMinutes(0, 0, 0)))\n            {\n                continue;\n            }\n            \n            history[i].onOff = true;\n            \n            if (new Date(history[i].start).getHours() == current_hour)\n            {\n                charge = true;\n            }\n        }\n    }\n}\n\ncontext.set('history', history, CONTEXT);\n\nnode.status({text: `Charge: ${charge} | Charge hours: ${charge_hours}`});\n\nreturn [\n    {\n        payload: {\n            hours: history\n        }\n    },\n    {\n        payload: charge\n    }\n];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1770,"y":120,"wires":[["dd3de5b805c628dd"],["714bdde432f73713"]]},{"id":"ad493de0a9a147d7","type":"debug","z":"d1a79cb7a1857836","name":"calculate charge schedule input","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1790,"y":80,"wires":[]},{"id":"6a8428e9f363b394","type":"change","z":"d1a79cb7a1857836","name":"topic = charge_hours","rules":[{"t":"set","p":"topic","pt":"msg","to":"charge_hours","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1660,"y":600,"wires":[["3891061b2e053a45"]]},{"id":"d816d14e0ce49a1a","type":"change","z":"d1a79cb7a1857836","name":"topic = energy_deficit","rules":[{"t":"set","p":"topic","pt":"msg","to":"energy_deficit","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1660,"y":680,"wires":[["b8ef7fe4f2c7eec1"]]},{"id":"073c94010f8c84e0","type":"tibber-api-endpoint","queryUrl":"https://api.tibber.com/v1-beta/gql","feedConnectionTimeout":"30","feedTimeout":"60","queryRequestTimeout":"30","name":"Tibber"},{"id":"ae94fc0e9881e8c0","type":"ui_group","name":"Visualisations","tab":"9e3dc13a1610987c","order":2,"disp":true,"width":"24","collapse":false,"className":""},{"id":"80c802fa5c3703dc","type":"pushbullet-config","name":"Pushbullet"},{"id":"ce0b2bd85123a79a","type":"config-vrm-api","name":"VRM"},{"id":"9e3dc13a1610987c","type":"ui_tab","name":"Tibber","icon":"dashboard","disabled":false,"hidden":false}]

Flow Info

Created 5 months ago
Updated 1 month, 2 weeks ago
Rating: not yet rated

Owner

Actions

Rate:

Node Types

Core
  • change (x33)
  • comment (x5)
  • debug (x12)
  • delay (x1)
  • function (x18)
  • inject (x1)
  • join (x11)
  • link in (x1)
  • link out (x2)
  • rbe (x2)
  • template (x1)
  • trigger (x2)
Other

Tags

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