diff --git a/docs/04-setup-self-consumption.md b/docs/04-setup-self-consumption.md
index 922249f..c609459 100644
--- a/docs/04-setup-self-consumption.md
+++ b/docs/04-setup-self-consumption.md
@@ -56,7 +56,7 @@ Use the [Ziegler-Nichols method](https://en.wikipedia.org/wiki/Proportional%E2%8
- increase Kp when not resonating
- decrease Kp if the system runs off. (stay alert, not to damage your system)
1. Determine the resonant frequency. E.g. by using [HA-history-graph-csv-export-analysis
-](https://github.com/gitcodebob/HA-history-graph-csv-export-analysis)
+](https://github.com/gitcodebob/HA-history-graph-csv-export-analysis). You need to download these CSV data first from the History dashboard, P1 Meter Power sensor. Only use a period there the battery management has actually been able to minimize P1 (i.e., not full stop, charge only, empty or full battery etc).
1. Tu = 1 / `` and Ku = your current Kp during resonance
1. Use the table of the [Ziegler-Nichols method](https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller#Ziegler%E2%80%93Nichols_method) to get a baseline.
- This baseline can be a bit aggressive.
diff --git a/home assistant/packages/house_battery_control_config.yaml b/home assistant/packages/house_battery_control_config.yaml
index 7572993..403a3d6 100644
--- a/home assistant/packages/house_battery_control_config.yaml
+++ b/home assistant/packages/house_battery_control_config.yaml
@@ -2,6 +2,11 @@
# It should be included in the main configuration.yaml file as a `package` via `!include_dir_named` https://www.home-assistant.io/docs/configuration/packages/.
# Use the values below as a `template` for your specific needs
+recorder:
+ exclude:
+ entities:
+ - sensor.l1_meter_power
+
template:
- sensor:
# Example: sensor for single-phase power meter (split into consumption and production)
@@ -9,7 +14,6 @@ template:
unique_id: "l1_meter_power"
unit_of_measurement: "W"
device_class: "power"
- state_class: "measurement"
state: >
{% set consumption = states('sensor.electricity_meter_power_consumption_phase_l1') | float(0) %}
{% set production = states('sensor.electricity_meter_power_production_phase_l1') | float(0) %}
diff --git a/node-red/02 strategy-self-consumption.json b/node-red/02 strategy-self-consumption.json
index 3897722..6e939c3 100644
--- a/node-red/02 strategy-self-consumption.json
+++ b/node-red/02 strategy-self-consumption.json
@@ -1 +1 @@
-[{"id":"2dc255585c9021ad","type":"tab","label":"Strategy Self-consumption v4.4.2","disabled":false,"info":"","env":[]},{"id":"ba3cab11cfa58369","type":"group","z":"2dc255585c9021ad","name":"Strategy start","style":{"fill":"#ffffff","label":true},"nodes":["653d5ecae07626a1","c685bb5d5b350a17","6333a8d8470cd4f5"],"x":54,"y":79,"w":212,"h":162},{"id":"481e254058a8227d","type":"group","z":"2dc255585c9021ad","name":"Battery solutions","style":{"label":true,"fill":"#ffffff"},"nodes":["188b6d74052d7b75","d6ba85caf21c6c7a","02843f3df46ffe5b"],"x":64,"y":1139,"w":202,"h":142},{"id":"4bd04882ac08f740","type":"group","z":"2dc255585c9021ad","name":"Bumpless operation","style":{"label":true,"stroke":"#bfdbef"},"nodes":["8319c2bf362f5d55","c07fd441a19ddcd6","751cc0061f48cf83","1526a33d800732ac"],"x":1614,"y":19,"w":252,"h":242},{"id":"fa53931ece8d9e5c","type":"group","z":"2dc255585c9021ad","name":"Bumpless operation - switching control modes","style":{"stroke":"#bfdbef","label":true},"nodes":["af9242d4df5098ea","2d4aaea3163ee0b0","3c7a17a46ed568bb","b67191747e0bfe44","bdcb744226755ff7","2a5a379c33bdc594","ce822e5f44a30bc2","a98d3ba5387274d6","e2f0589bab3add2b","85a1019efd05b4bd","bc18c2754c562a1a","332d68169bd129ff"],"x":1614,"y":279,"w":592,"h":289.5},{"id":"b9b7c6e5106b3a7c","type":"group","z":"2dc255585c9021ad","name":"Inputs","style":{"fill":"#7fb7df","label":true,"color":"#ffffff"},"nodes":["d4bd6010b2a82002","b753f4385c7d13bd"],"x":288,"y":13,"w":904,"h":234},{"id":"f9161a30652a1e37","type":"group","z":"2dc255585c9021ad","name":"Pre processing","style":{"fill":"#bfdbef","label":true,"color":"#ffffff"},"nodes":["289b3a4f3ed9895e","15a8430ec8fd32cd","085064404e672245"],"x":288,"y":273,"w":1104,"h":294},{"id":"e90c3db5ee8c6a36","type":"group","z":"2dc255585c9021ad","name":"Control","style":{"fill":"#7fb7df","label":true,"color":"#ffffff"},"nodes":["0538d5e767988695","81378689693e940b"],"x":288,"y":593,"w":924,"h":634},{"id":"ff7db384260f1b22","type":"group","z":"2dc255585c9021ad","name":"Unhandled exceptions","style":{"label":true,"stroke":"#d88a8a","color":"#d88a8a"},"nodes":["916c17ae8b44a945","768fa0dcf7e8e3b5","30ea27bc433988a5"],"x":54,"y":279,"w":182,"h":82},{"id":"d4bd6010b2a82002","type":"group","z":"2dc255585c9021ad","g":"b9b7c6e5106b3a7c","name":"PID control inputs","style":{"label":true,"fill":"#ffffff"},"nodes":["89c318c6c8728833","e5144d3a238687bb","8ce405166bb8ae24","ae7e3834fba26c76","c7386c2b9a7d1cc0","edbd6dfed6ba7652","8bc9658f77284e22","800b817e2d30700f","fd84a7d1648928c8"],"x":314,"y":59,"w":572,"h":162},{"id":"b753f4385c7d13bd","type":"group","z":"2dc255585c9021ad","g":"b9b7c6e5106b3a7c","name":"Advanced features","style":{"label":true,"fill":"#ffffff"},"nodes":["f6c435539e7250e3","fe2a55ed5fe6bed5","58b564f75d3c21c3"],"x":914,"y":39,"w":252,"h":182},{"id":"289b3a4f3ed9895e","type":"group","z":"2dc255585c9021ad","g":"f9161a30652a1e37","name":"","style":{"fill":"#ffffff","label":true},"nodes":["858221520cee3dc5","735a9b3b250cd260","cd0dc47e2ff890dd","7726f1429c179f68"],"x":314,"y":299,"w":292,"h":202},{"id":"15a8430ec8fd32cd","type":"group","z":"2dc255585c9021ad","g":"f9161a30652a1e37","name":"Derivative - incl. bumpless operation","style":{"label":true,"stroke":"#a4a4a4","fill":"#ffffff"},"nodes":["7976d4a42e873093","823be9b66feeb0da","72ac88c55f926b58","e002c8dde1136e44","700e6af7e2972644"],"x":614,"y":399,"w":752,"h":142},{"id":"085064404e672245","type":"group","z":"2dc255585c9021ad","g":"f9161a30652a1e37","name":"Processing required? ","style":{"label":true,"fill":"#ffffff"},"nodes":["ea326ab0246b8c13","deae32c2aaf84154","2f97f335f207820a"],"x":614,"y":299,"w":562,"h":82},{"id":"0538d5e767988695","type":"group","z":"2dc255585c9021ad","g":"e90c3db5ee8c6a36","name":"PID controller","style":{"label":true,"fill":"#ffffff","color":"#3f3f3f"},"nodes":["a826eba0ed0ac7fc","e0644847be635f58","ad085261cf8a2cc1","2dc42d364f58d67f","7ede0bd5857ee7a0","5370c30d8cdab4b8","ed568c00a0795773","e05c810323db1ceb","16582ddbc62d0305","303bb8cea00177f9","829124787188e159","d4422f8539cc8ecf","71d8d3b4f6ef9310","4563c0e2adab2fee","9de6d5774b6b0bfa","729b998cf3b87cc7","83a1a6be0e294782","3db366538fbac912","f41cc8bd3b060241","817e304f8bb80d64"],"x":314,"y":619,"w":872,"h":482},{"id":"81378689693e940b","type":"group","z":"2dc255585c9021ad","g":"e90c3db5ee8c6a36","name":"Distribution","style":{"label":true,"fill":"#ffffff","color":"#3f3f3f"},"nodes":["3acf8bf116f857e1","9197f431cd22f55c"],"x":314,"y":1119,"w":452,"h":82},{"id":"edbd6dfed6ba7652","type":"junction","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","x":460,"y":100,"wires":[["800b817e2d30700f"]]},{"id":"800b817e2d30700f","type":"junction","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","x":700,"y":100,"wires":[["8bc9658f77284e22","fd84a7d1648928c8"]]},{"id":"fd84a7d1648928c8","type":"junction","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","x":720,"y":120,"wires":[["e5144d3a238687bb"]]},{"id":"653d5ecae07626a1","type":"link in","z":"2dc255585c9021ad","g":"ba3cab11cfa58369","name":"Self-consumption","links":[],"x":160,"y":160,"wires":[["6333a8d8470cd4f5"]],"l":true},{"id":"c685bb5d5b350a17","type":"comment","z":"2dc255585c9021ad","g":"ba3cab11cfa58369","name":"Start (readme)","info":"# Setting up your battery strategy flow\n\n## Setup\nName the Link In in this start group exactly after the option configured in your\nselect_input.house_battery_strategy\n\nfound in the file:\ninput_select_house_battery_control.yaml\n\n### example\n`house_battery_strategy:\n name: House Battery Strategy\n options:\n - AcmE example`\n\nWill point to the flow containing a Link In node\ncalled \u0027AcmE example\u0027 (case sensitive).","x":150,"y":120,"wires":[]},{"id":"9b4c669af44fc4a8","type":"comment","z":"2dc255585c9021ad","name":"Home Battery Strategy","info":"The `Home Battery Start` flow will call this strategy flow\nby matching the strategy name with the Link In name.\n\nConfigure the Link In node in the Start group.\n\nDon\u0027t modify other parts of the Start and End groups.\nThey handle the calling and returning for you.","x":120,"y":40,"wires":[]},{"id":"8319c2bf362f5d55","type":"server-state-changed","z":"2dc255585c9021ad","g":"4bd04882ac08f740","name":"User Ki change","server":"176d29a.6f648d6","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["input_number.house_battery_control_ki"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":true,"ignorePrevStateUnknown":true,"ignorePrevStateUnavailable":true,"ignoreCurrentStateUnknown":true,"ignoreCurrentStateUnavailable":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":1720,"y":100,"wires":[["c07fd441a19ddcd6"]]},{"id":"c07fd441a19ddcd6","type":"function","z":"2dc255585c9021ad","g":"4bd04882ac08f740","name":"Integral term adjust","func":"// On change of Ki setting\nlet logdata = \"bumpless \";\nlet I_sum = flow.get(\"I_integral_sum\");\nlet Ki = msg.payload || 0; // Integral gain\nlet Ki_last = context.get(\"house_battery_control_ki_last\")||0;\n\n// if Ki was or has become zero\nif(Ki == 0 || Ki_last == 0){\n // passify the integral-sum\n flow.set(\"I_integral_sum\",0);\n context.set(\"house_battery_control_ki_last\", Ki);\n logdata += `to/from 0, Ki=${Ki_last} -\u003e ${Ki} | `;\n // done\n return { payload: logdata };\n}\n\n// change in Ki\nlet dKi = parseFloat(Ki/Ki_last);\nif(dKi \u003c= 0) dKi = 1; // ignore erroneous changes\n\n// keep I-term constant by compensating the integral-sum for the change in Ki\n// e.g. if Ki got 10% smaller, then integral-sum should increase 10%, to keep the I-term constant\nlet I_sum_new = I_sum / dKi;\nlogdata += `change of I: from ${Ki_last} to ${Ki} (${dKi * 100}%) | `;\n\n// OUTFLOW\nflow.set(\"I_integral_sum\", I_sum_new);\ncontext.set(\"house_battery_control_ki_last\", Ki);\n\n// OUTPUT\nreturn { payload: logdata };","outputs":1,"timeout":0,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n\n// init last state\nlet Ki = flow.get(\"house_battery_control_ki\") || 0;\ncontext.set(\"house_battery_control_ki_last\", Ki);","finalize":"","libs":[],"x":1730,"y":160,"wires":[["1526a33d800732ac"]]},{"id":"751cc0061f48cf83","type":"comment","z":"2dc255585c9021ad","g":"4bd04882ac08f740","name":"Bumpless Ki changes","info":"Recalc I-term whenever Ki changes\n\nNote: always be mindful of making changes to control parameters of systems in operation.","x":1740,"y":60,"wires":[]},{"id":"1526a33d800732ac","type":"debug","z":"2dc255585c9021ad","g":"4bd04882ac08f740","name":"Logdata","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1700,"y":220,"wires":[]},{"id":"af9242d4df5098ea","type":"switch","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Check for custom/auto mode","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"Manual control","vt":"str"},{"t":"eq","v":"Marstek control","vt":"str"},{"t":"eq","v":"Full control","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":1855,"y":520,"wires":[["2a5a379c33bdc594","2d4aaea3163ee0b0"],["2a5a379c33bdc594","2d4aaea3163ee0b0"],["2d4aaea3163ee0b0"]],"l":false},{"id":"2d4aaea3163ee0b0","type":"debug","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Master control switch","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2060,"y":520,"wires":[]},{"id":"3c7a17a46ed568bb","type":"server-state-changed","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Master control mode","server":"176d29a.6f648d6","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["input_select.marstek_master_battery_mode"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":1730,"y":520,"wires":[["af9242d4df5098ea"]]},{"id":"b67191747e0bfe44","type":"api-call-service","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Integral PID to zero","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_i_term"],"labelId":[],"data":"{\"value\": \"0\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":2050,"y":360,"wires":[["bdcb744226755ff7"]]},{"id":"bdcb744226755ff7","type":"api-call-service","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Differential PID to zero","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_d_term"],"labelId":[],"data":"{\"value\": \"0\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":2060,"y":400,"wires":[["ce822e5f44a30bc2"]]},{"id":"2a5a379c33bdc594","type":"api-call-service","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Proportinal PID to zero","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_p_term"],"labelId":[],"data":"{\"value\": \"0\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":2060,"y":320,"wires":[["b67191747e0bfe44"]]},{"id":"ce822e5f44a30bc2","type":"api-call-service","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"PID Output to zero","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_pid_output"],"labelId":[],"data":"{\"value\": \"0\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":2050,"y":440,"wires":[["a98d3ba5387274d6"]]},{"id":"a98d3ba5387274d6","type":"change","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Integral sum to zero","rules":[{"t":"set","p":"I_integral_sum","pt":"flow","to":"0","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2060,"y":480,"wires":[[]]},{"id":"89c318c6c8728833","type":"api-current-state","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"Target grid consumption","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_target_grid_consumption_in_w","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"string","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_target_grid_consumption_in_w","propertyType":"msg","value":"string","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":570,"y":120,"wires":[["800b817e2d30700f"]],"info":"Set at 0 to strive for zero consumption"},{"id":"58b564f75d3c21c3","type":"api-current-state","z":"2dc255585c9021ad","g":"b753f4385c7d13bd","name":"Hysteresis","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_hysteresis_in_w","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_hysteresis_in_w","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":1010,"y":120,"wires":[["f6c435539e7250e3"]]},{"id":"e5144d3a238687bb","type":"api-current-state","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"PID P-value","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_kp","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_kp","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":410,"y":180,"wires":[["8ce405166bb8ae24"]]},{"id":"8ce405166bb8ae24","type":"api-current-state","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"PID I-value","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_ki","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_ki","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":590,"y":180,"wires":[["ae7e3834fba26c76"]]},{"id":"ae7e3834fba26c76","type":"api-current-state","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"PID D-value","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_kd","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_kd","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":770,"y":180,"wires":[["fe2a55ed5fe6bed5"]]},{"id":"f6c435539e7250e3","type":"function","z":"2dc255585c9021ad","g":"b753f4385c7d13bd","name":"Advanced settings","func":"// 1. Hard coded advanced settings (flow level)\nflow.set(\"has_soc_charging_limiter\", true); // slows charging from 90% to 100% to improve battery life\nflow.set(\"has_reverse_priority_discharge\", true); // Prioritize discharging and charging the same battery when possible\nflow.set(\"master_gain\", 1); // Gain scheduling. Not Implemented!\n\n// 2. Configurable advanced settings (msg level)\nmsg.advanced_settings ||= {}; // creates \u0027advanced_settings\u0027 if it does net yet exist\nlet mas = msg.advanced_settings;\n\n// disable charge or dicharge solutions and changes them to stop solutions\nRED.util.setMessageProperty(msg,\"advanced_settings.discharge_disabled\", mas.discharge_disabled ?? false);\nRED.util.setMessageProperty(msg,\"advanced_settings.charge_disabled\", mas.charge_disabled ?? false);\n// The nullish coalescing (??) operator is a logical operator that returns its right-hand side operand when its left-hand side operand is null or undefined\n\n// 3. continue\nreturn msg;\n\n// Notes for home battery tinkerer friends:\n// Using RED.util.getMessageProperty / RED.util.setMessageProperty is more performant and safe.\n// If any part of the path (\"foo.bar\") is missing, it will return \u0027undefined\u0027 without throwing an error.","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1030,"y":180,"wires":[["735a9b3b250cd260"]]},{"id":"fe2a55ed5fe6bed5","type":"api-current-state","z":"2dc255585c9021ad","g":"b753f4385c7d13bd","name":"Idle time before Stop","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_idle_time","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"idle_time_minutes","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":1040,"y":80,"wires":[["58b564f75d3c21c3"]]},{"id":"858221520cee3dc5","type":"comment","z":"2dc255585c9021ad","g":"289b3a4f3ed9895e","name":"Setpoint ramping / damping","info":"","x":460,"y":460,"wires":[]},{"id":"735a9b3b250cd260","type":"api-current-state","z":"2dc255585c9021ad","g":"289b3a4f3ed9895e","name":"Error signal dampening","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_error_signal_dampening","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_error_signal_dampening","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":450,"y":340,"wires":[["cd0dc47e2ff890dd"]]},{"id":"cd0dc47e2ff890dd","type":"api-current-state","z":"2dc255585c9021ad","g":"289b3a4f3ed9895e","name":"Output signal dampening","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_pid_output_dampening","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_pid_output_dampening","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":450,"y":380,"wires":[["7726f1429c179f68"]]},{"id":"7726f1429c179f68","type":"function","z":"2dc255585c9021ad","g":"289b3a4f3ed9895e","name":"Set: Error signal","func":"// INPUTS\nlet P1_error_last = Number(context.get(\"p1_error_last\"))||0; // last error signal level in W\nlet dampening = Number(flow.get(\"house_battery_control_error_signal_dampening\")) || 0; // 0% - 100%\ndampening = dampening / 100; // to Number\n\n// Process Variable (PV) currently measured grid power, Setpoint (SP) desired grid power, set 0 for NoM\nvar P1_power = msg.grid_power; // PV: consume is positive, supply to grid is negative\nvar P1_setpoint = Number(RED.util.getMessageProperty(msg,\"house_target_grid_consumption_in_w\")); // SP: target value\n\n// Calc error signal\nlet P1_error_unfiltered = P1_setpoint - P1_power;\n\n// Simple averaging filter\nlet P1_error_filtered = (1 - dampening) * P1_error_unfiltered + (dampening) * P1_error_last;\n\n// OUTFLOW\ncontext.set(\"p1_error_last\", P1_error_filtered);\n\n// OUTPUT\nmsg.grid_error = P1_error_filtered; // W (watt), error signal = SP - PV\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":420,"wires":[["ea326ab0246b8c13"]]},{"id":"7976d4a42e873093","type":"comment","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Bumpless target grid consumption","info":"The error value is used by default to calculate the derivative.\nWhen the target grid consumption (tgc) is changed, the system is takes the derivative of the process variable instead.\nThis prevents irratic behavior during sudden changes of the error or setpoint value.\n\ne.g. you change the setpoint from 0 to 100W \nThe error would make an instantanious jump and cause a huge derivative-term.\nThe pv stays continous and fluent.\n\nAfter 1 control tick the derivative is taken from error again.\nUntil the `tgc` is changed again.","x":780,"y":440,"wires":[]},{"id":"823be9b66feeb0da","type":"function","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Derivative PV","func":"var P1_power = msg.grid_power||0;\nvar P1_last_power = Number(context.get(\"p1_last_power\")) ||0;\n\n// PV derivative\nvar p1_derivative = P1_power - P1_last_power; // devided by unity seconds\n\n// OUTFLOW\ncontext.set(\"p1_last_power\", P1_power);\n\n// OUTPUT\nmsg.grid_derivative = p1_derivative;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":500,"wires":[["700e6af7e2972644"]]},{"id":"72ac88c55f926b58","type":"switch","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Target grid consumption unchanged","property":"house_target_grid_consumption_in_w","propertyType":"flow","rules":[{"t":"eq","v":"","vt":"prev"},{"t":"neq","v":"","vt":"prev"}],"checkall":"false","repair":false,"outputs":2,"x":780,"y":480,"wires":[["e002c8dde1136e44"],["823be9b66feeb0da"]]},{"id":"e002c8dde1136e44","type":"function","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Derivative Error","func":"var P1_error = msg.grid_error||0;\nvar P1_last_error = Number(context.get(\"p1_last_error\")) ||0;\n\n// Error derivative\n// note: error = -PV\n// note: for simplicity we assume 1 second time steps\nlet p1_derivative = -(P1_error - P1_last_error); // devided by unity seconds\n\n// OUTFLOW\ncontext.set(\"p1_last_error\", P1_error);\n\n// OUTPUT\nmsg.grid_derivative = p1_derivative;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":460,"wires":[["700e6af7e2972644"]]},{"id":"700e6af7e2972644","type":"function","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Derivative final value","func":"\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1240,"y":480,"wires":[["a826eba0ed0ac7fc"]]},{"id":"ea326ab0246b8c13","type":"function","z":"2dc255585c9021ad","g":"085064404e672245","name":"Deadband (15W)","func":"// Deadband\n// Stop processing if error is within .. Watt of target grid consumption.\n// This to reduce CPU loads \n\n// Logger\nconst log = global.get(\"logger\");\n\n// P1\nlet P1_error = msg.grid_error; // Watt\nlet P1_deadband = 15; // Watt\n\n// TODO\n// based on last \u0027assignable power\u0027 and current error, if (error \u003e assignable power) the system will never enter the deadband. And keep calculating each interation.\n// however since we don\u0027t know if the is the assignable power will change, until we calculate the Control Value / load balancer\n// we could end up in a deadlock if we simply stop execution here, based on assignable power alone.\n\n// Within Deadband\nif(Math.abs(P1_error) \u003c P1_deadband) {\n // stop processing\n node.status({fill:\"yellow\",shape:\"dot\",text:`Halt, ${Math.round(Math.abs(P1_error))} W`});\n log(this, `**Stopped processing** 🌿power saving, ${Math.round(Math.abs(P1_error))} W is within deadband of ${P1_deadband} W`);\n msg.payload = \u0027halted by deadband\u0027;\n return msg;\n} \n\n// Outside deadband\nnode.status({fill:\"green\",shape:\"dot\",text:`Pass, ${Math.round(Math.abs(P1_error))} W`});\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":340,"wires":[["deae32c2aaf84154"]]},{"id":"a826eba0ed0ac7fc","type":"function","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Calculate corrections","func":"// Logger\nvar logdata = \"\"; // deprecated\nconst log = global.get(\"logger\");\n\n// Timing\nlet time_last = context.get(\u0027time_last\u0027) || Date.now(); // Milliseconds\nlet time_current = Date.now(); // Milliseconds\nlet time_delta = (time_current - time_last) / 1000; // Convert to seconds\n// note: due to inherent 1 sec intervals of P1 meter we omit dt terms. This is mostly for bug checking and optimizations.\n\n// Batteries\nvar B_power = msg.batteries_total_power; // charging is positive, discharging negative\nvar anti_windup_threshold = Number(flow.get(\"batteries_total_assignable_power\")) || 0; // max available charge/discharge power.\n\n// Process Variable (PV) currently measured grid power, Setpoint (SP) desired grid power, set 0 for NoM\nvar P1_power = msg.grid_power; // PV: consume is positive, supply to grid is negative\nvar P1_setpoint = flow.get(\"house_target_grid_consumption_in_w\"); // SP: target value\nvar P1_error = msg.grid_error; // W (watt), error signal = SP - PV\nvar P1_derivative = msg.grid_derivative; // Derivative of PV, not the error\n\n// PID values\nvar Kp = flow.get(\"house_battery_control_kp\") || 0.75; // Proportional gain\nvar Ki = flow.get(\"house_battery_control_ki\") || 0; // Integral gain Ki = Kp/Ti\nvar Kd = flow.get(\"house_battery_control_kd\") || 0; // Derivative gain Kd = Kp*Td\n\n// helpers\nvar I_integral_sum = context.get(\u0027I_integral_sum\u0027) || 0;\nvar Gm = flow.get(\"master_gain\")||1; // gain scheduling\n\n// optimizations\nvar hysteresis = flow.get(\"house_battery_control_hysteresis_in_w\"); // W (watt)\n\n// advanced settings\nconst isDischargeDisabled = RED.util.getMessageProperty(msg,\"advanced_settings.discharge_disabled\");\nconst isChargeDisabled = RED.util.getMessageProperty(msg,\"advanced_settings.charge_disabled\");\n\n// -- PID regulator --\n// Proportional term\nlet p_term = Gm * Kp * P1_error;\n\n// Integral term\nlet integral_max = 0;\nif (Ki \u003e 0) {\n // Integral term | integrate\n I_integral_sum += P1_error; \n // Integral term | apply anti-windup\n integral_max = anti_windup_threshold / Ki; // the anti-windup value is set to the current controlable range of the batteries, given by the previous load balancer calculations\n I_integral_sum = Math.min(Math.max(I_integral_sum, -integral_max), integral_max);\n} else {\n // If Ki is 0, keep everything 0\n I_integral_sum = 0;\n integral_max = 0;\n}\n\n// Integral term | the term\nlet i_term = Ki * I_integral_sum;\n\n// Integral term | explain\nlogdata += `anti_windup_threshold=${anti_windup_threshold}, integral_max=${integral_max}| `; // deprecated\n\n// Differential term\nlet d_term = Gm * Kd * (-P1_derivative); // omitted \u0027/time_delta\u0027. inherent P1 refresh fequency.\n\n// Total PID output\nvar PID_output = p_term + i_term + d_term; // W (watt), the control input for the battery packs\n\n// Charging or Discharging states\nvar B_was_charging = context.get(\"batteries_charging_last\") || false;\nvar B_is_charging = PID_output \u003e 0 ? true : false;\n\n// Explain\nlogdata += `U(${time_delta}s)[${PID_output}] = P(${Kp})[${p_term}] + I(${Ki})[${i_term}] + D(${Kd})[${d_term}] | `; // deprecated\nlog(this,`**PID Controller**`);\nlog(this,`Step size: ${time_delta} s`);\nif(Kp == 0 \u0026\u0026 Ki == 0 \u0026\u0026 Kd == 0) {\n log(this, `\u003cfont color=\"orange\"\u003e**Controller disabled**\u003c/font\u003e All PID gains are 0. Choose a PID preset or set gains \u003e 0.`,\"warn\");\n} else {\n log(this, `P-term: ${Math.floor(p_term)} W @ P-gain ${Kp}`);\n log(this, `I-term: ${Math.floor(i_term)} W @ I-gain ${Ki} (max. ${anti_windup_threshold} W)`);\n log(this, `D-term: ${Math.floor(d_term)} W @ D-gain ${Kd}`);\n log(this, `**PID ${B_is_charging ? \u0027Charge\u0027 : \u0027Discharge\u0027}:** ${Math.floor(p_term)} + ${Math.floor(i_term)} + ${Math.floor(d_term)} = **${Math.round(PID_output)} Watt**`);\n}\n\n// Only one of the advanced settings below can be active at once.\n// Advanced setting | discharge disabled\nif(isDischargeDisabled \u0026\u0026 !B_is_charging) {\n // log explain\n logdata += `Discharging disabled, PID unwound U(${time_delta})[0]=0+0+0 | `;\n log(this,\u0027**PID set to 0**. Discharge was disabled via `msg.advanced_settings`.\u0027);\n // Set all PID terms to 0 and unwind integral sum\n PID_output = 0;\n p_term = 0;\n i_term = 0;\n d_term = 0;\n I_integral_sum = 0; // unwind\n // maintain charge scenario\n B_is_charging = true;\n \n// Advanced setting | charge disabled\n} else if (isChargeDisabled \u0026\u0026 B_is_charging) {\n // log explain\n logdata += `Charging disabled, PID unwound U(${time_delta})[0]=0+0+0 | `;\n log(this,\u0027**PID set to 0**. Charge was disabled via `msg.advanced_settings`.\u0027);\n // Set all PID terms to 0 and unwind integral sum\n PID_output = 0;\n p_term = 0;\n i_term = 0;\n d_term = 0;\n I_integral_sum = 0; // unwind\n // maintain discharge scenario\n B_is_charging = false;\n\n// Advanced setting | hysteresis\n} else if \n// prevents excessive switching between (dis)charge mode around the zero point.\n// if new PID_output lies within hysteresis, it will not switch charge mode. \n// set hysteresis to 0, to apply no hysteresis\n(B_is_charging !== B_was_charging \u0026\u0026 Math.abs(PID_output) \u003c hysteresis){\n // log explain\n logdata += `Hysteresis prevented charge mode switch from ${B_was_charging ? \"charging\" : \"discharging\"} to ${B_is_charging ? \"charging\" : \"discharging\"} as ${Math.abs(PID_output)}W \u003c= ${hysteresis}(hyst) | `;\n log(this,`Hysteresis prevented charge mode switch from ${B_was_charging ? \"charging\" : \"discharging\"} to ${B_is_charging ? \"charging\" : \"discharging\"} as ${Math.abs(PID_output)}W \u003c= ${hysteresis}(hyst)`);\n // prevent negative charging or positive discharging values (not allowed)\n PID_output = (PID_output \u003c 0 \u0026\u0026 B_is_charging) || ((PID_output \u003e 0 \u0026\u0026 !B_is_charging)) ? 0 : PID_output;\n // maintain previous scenario\n B_is_charging = B_was_charging;\n} \n\n// save for next iteration\ncontext.set(\"batteries_charging_last\", B_is_charging); //boolean\n\n// OUTFLOW\ncontext.set(\"time_last\", Date.now());\ncontext.set(\"I_integral_sum\", I_integral_sum);\n\n// OUTPUT\nRED.util.setMessageProperty(msg, \"batteries_charging\", B_is_charging, true);\nRED.util.setMessageProperty(msg, \"I_integral_sum\", I_integral_sum,true);\nmsg.payload = Number(PID_output);\n\n\nreturn [msg, // payload = PID_output\n { payload: Number(P1_error)},\n { payload: parseFloat(p_term)},\n { payload: parseFloat(i_term)},\n { payload: parseFloat(d_term)},\n { payload: B_is_charging},\n { payload: logdata }];","outputs":7,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":700,"wires":[["71d8d3b4f6ef9310"],["ad085261cf8a2cc1"],["3db366538fbac912"],["9de6d5774b6b0bfa"],["f41cc8bd3b060241"],["817e304f8bb80d64"],["e0644847be635f58"]],"inputLabels":["battery_array"]},{"id":"e0644847be635f58","type":"debug","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Logdata","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":720,"y":1060,"wires":[]},{"id":"ad085261cf8a2cc1","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Error Signal","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_error_signal"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":730,"y":760,"wires":[["829124787188e159"]]},{"id":"2dc42d364f58d67f","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"PID Output","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_pid_output"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":730,"y":700,"wires":[["16582ddbc62d0305"]]},{"id":"7ede0bd5857ee7a0","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Proportinal term","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_p_term"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":920,"y":820,"wires":[[]]},{"id":"5370c30d8cdab4b8","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Integral term","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_i_term"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":910,"y":880,"wires":[[]]},{"id":"ed568c00a0795773","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Differential term","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_d_term"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":920,"y":940,"wires":[[]]},{"id":"e05c810323db1ceb","type":"debug","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Charging","active":false,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":900,"y":1060,"wires":[]},{"id":"16582ddbc62d0305","type":"smooth","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"","property":"payload","action":"sd","count":"8","round":"","mult":"single","reduce":false,"x":900,"y":700,"wires":[["303bb8cea00177f9"]]},{"id":"303bb8cea00177f9","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"PID deviation","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_pid_output_deviation"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":1070,"y":700,"wires":[[]]},{"id":"829124787188e159","type":"smooth","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"","property":"payload","action":"sd","count":"8","round":"","mult":"single","reduce":false,"x":900,"y":760,"wires":[["d4422f8539cc8ecf"]]},{"id":"d4422f8539cc8ecf","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Error deviation","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_error_deviation"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":1080,"y":760,"wires":[[]]},{"id":"71d8d3b4f6ef9310","type":"function","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Output dampening","func":"// INPUT\nlet PID_unfiltered = msg.payload; // W Watt\nlet PID_last = context.get(\"PID_last\")||0; // W watt\nlet PID_damp = Number(flow.get(\"house_battery_control_pid_output_dampening\"))||0; // 0% - 100%\nPID_damp = PID_damp/100; // to Number\n\n// simple averaging filter\nlet PID_filtered = (1-PID_damp)*PID_unfiltered + (PID_damp)*PID_last;\n\n// OUTFLOW\ncontext.set(\"PID_last\", PID_filtered);\n\n// OUTPUT\nmsg.payload = Number(PID_filtered)\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":750,"y":660,"wires":[["4563c0e2adab2fee"]]},{"id":"4563c0e2adab2fee","type":"function","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Output failsafe","func":"// INFLOW\nlet maxCharge = msg.batteries_max_charge_power||undefined;\nlet maxDischarge = msg.batteries_max_discharge_power||undefined;\nlet isCharging = msg.batteries_charging||undefined;\nlet PID_output = msg.payload; // Watt\n\n// No failsafe\nif (maxCharge === undefined || maxDischarge == undefined) {\n node.status({ fill: \"red\", shape: \"dot\", text: \"disabled\" });\n node.warn(`Output Failsafe is INACTIVE. Max charge or discharge limits are undefined`);\n // continue without safeguard\n return msg;\n} \n\n// limit\nlet limit = 0;\n\n// charging state unknown\nif(isCharging === undefined) {\n // limit on lowest of charge / discharge\n limit = Math.min(maxCharge,maxDischarge); \n}\n// charging state known\nlimit = isCharging ? maxCharge : maxDischarge;\n\n// Safe output\nif (PID_output \u003c= limit){\n node.status({ fill: \"green\", shape: \"dot\", text: \"safe\" });\n return msg;\n} \n\n// Unsafe, limit output\nnode.status({ fill: \"yellow\", shape: \"ring\", text: \"power limiting\" });\nmsg.payload = Number(limit);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1080,"y":660,"wires":[["3acf8bf116f857e1","2dc42d364f58d67f"]]},{"id":"9de6d5774b6b0bfa","type":"rbe","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":730,"y":880,"wires":[["5370c30d8cdab4b8"]]},{"id":"3acf8bf116f857e1","type":"function","z":"2dc255585c9021ad","g":"81378689693e940b","name":"Load distribution","func":"// Logger\nconst log = global.get(\"logger\");\nlog(this,\"**PID load distribution**\");\n\n// INPUT\nvar batteries = msg.batteries; // Battery configuration as provided by `Home Batteries IO` flow\nvar isCharging = msg.batteries_charging; // Are we in a battery charging scenario?\nvar hasChargingLimiter = flow.get(\"has_soc_charging_limiter\") || false; // Battery life improvement\nvar hasReverseDischargePriority = flow.get(\"has_reverse_priority_discharge\") || false; // Prioritize (dis)charging the same battery\nconst isDischargeDisabled = RED.util.getMessageProperty(msg,\"advanced_settings.discharge_disabled\"); // Disallow discharging?\n\n// how much power do the batteries need to compensate?\nvar unassigned_power = Math.round(Math.abs(msg.payload)); // Power in W to compensate using batteries\nlog(this,`How much power do the batteries need to compensate: ${unassigned_power} W`);\n\n// inits \nvar solution_array = []; // load distribution solutions\nvar batteries_total_assignable_power = 0; // Current max. available total (dis)charge power. Exceeding this max should trigger the anti-windup routine for the Integral terms\n\n// enums\nconst CMODE = {\n STOP: \"stop\", // Marstek batteries disconnect a relay when this is set or Power is 0W\n CHARGE: \"charge\",\n DISCHARGE: \"discharge\"\n}\n\n/**\n** Battery life improvement (slow charge near max SoC)\n* @param {number} soc\n* @param {number} max_power\n*/\nfunction chargingLimiter(soc, max_power) {\n // Must be greater than zero\n let limitedPower = Math.max(0, max_power); \n\n // Limit charge at high SoC\n if (hasChargingLimiter) {\n if (soc \u003e= 98) limitedPower = Math.min(300, limitedPower);\n else if (soc \u003e= 95) limitedPower = Math.min(500, limitedPower);\n else if (soc \u003e= 85) limitedPower = Math.min(1500, limitedPower);\n }\n return limitedPower;\n}\n\n/**\n* battery life improvement (disconnects battery only if the battery is not used for ... seconds)\n* @param {string | number} batteryID\n* @returns {{id: string|number, mode: string, power: number}} battery solution\n*/\nfunction getStopSolution(batteryID) {\n // idle time, before stop is given to inverter relay.\n let time_idle_minimum = flow.get(\"idle_time_minutes\")*60*1000||0; // Milliseconds\n\n // retrieve last registered active times for each battery based on their ids\n let lasttime_active = context.get(\"lasttime_active\");\n \n // battery exists in register \n if(lasttime_active[batteryID] !== undefined){\n // timing\n let time_last = lasttime_active[batteryID]; // Milliseconds \n let time_now = Date.now(); // Milliseconds \n let time_idle = (time_now - time_last); // Milliseconds\n \n // battery is ready for STOP\n if (time_idle \u003e= time_idle_minimum) {\n log(this,`Battery ${batteryID} [STOP]: 0 Watt and disconnect relay.`);\n return {id: batteryID, mode: CMODE.STOP, power:0}; // STOP or 0 Watt disconnects relay\n }\n \n // battery is kept IDLE, while awaiting minimum inactive time\n log(this,`Battery ${batteryID} [IDLE]: 1 Watt for ${Math.round((time_idle_minimum-time_idle)/1000)}s. Keep relay engaged.`);\n return { id: batteryID, mode: isCharging ? CMODE.CHARGE : CMODE.DISCHARGE, power: 1 }; // 1 Watt keeps battery IDLE, keeping relay engaged\n }\n\n // battery missing in register\n log(this,`Battery ${batteryID} [IDLE]: unknown last active time`);\n return getActiveSolution(batteryID, 1); // set a last active time and engage idle state\n}\n\n/**\n* Get battery solution and register a last active time.\n* @param {any} batteryID\n* @param {number} assigned_power\n* @returns {{id: string|number, mode: string, power: number}} battery solution\n*/\nfunction getActiveSolution(batteryID, assigned_power){\n // register battery as active\n let lasttime_active = context.get(\"lasttime_active\")||[]; // last registered active times for each battery based on their ids\n lasttime_active[batteryID] = Date.now();\n context.set(\"lasttime_active\", lasttime_active);\n\n // solution\n return {\n id: batteryID,\n mode: isCharging ? CMODE.CHARGE : CMODE.DISCHARGE,\n power: Math.round(assigned_power)\n };\n}\n\n// -- BATTERY DISCHARGE PRIORITY\nif (!isCharging \u0026\u0026 hasReverseDischargePriority) {\n batteries.reverse();\n}\n\n// -- LOAD BALANCER --\nbatteries.forEach((/** @type {{ id: any; soc: number; soc_min: number; soc_max: number; rs485: string; charging_max: number; discharging_max: any; }} */ battery) =\u003e {\n \n // Explain\n log(this, `Battery ${battery.id}: ${battery.soc}% (min: ${battery.soc_min}%, max: ${battery.soc_max}%) ; RS485: ${battery.rs485==\u0027enable\u0027?\u0027OK\u0027:\u0027Nok\u0027}`);\n\n // track if the battery should be skipped and why\n let skipReason = null;\n if (battery.rs485 !== \"enable\") {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because RS485 is not \u0027enable\u0027`;\n } else if (isCharging \u0026\u0026 battery.soc \u003e= battery.soc_max) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because SoC (${battery.soc}%) \u003e= Max (${battery.soc_max}%) while charging`;\n } else if (!isCharging \u0026\u0026 battery.soc \u003c= battery.soc_min) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because SoC (${battery.soc}%) \u003c= Min (${battery.soc_min}%) while discharging`;\n } else if (!isCharging \u0026\u0026 isDischargeDisabled) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped, discharging was disabled via msg property`;\n }\n\n // battery NOT AVAIABLE, skip via soft stop\n if (skipReason !== null) {\n log(this, skipReason); // communicate reason\n let solution = getStopSolution(battery.id); // get a soft stop solution for this battery\n solution_array.push(solution); // add solution, do not update last active time.\n // next battery\n return; \n }\n\n // battery is AVAILABLE, assign power\n let battery_assignable_power = isCharging ? chargingLimiter(battery.soc, battery.charging_max) : battery.discharging_max;\n let assign = Math.min(unassigned_power, battery_assignable_power);\n \n // select solution\n var solution;\n if (assign \u003c= 0 ) {\n // no power solution\n solution = getStopSolution(battery.id); // a soft stop solution\n assign = 0;\n } else {\n // assigned power solution\n solution = getActiveSolution(battery.id, Math.round(assign));\n }\n solution_array.push(solution);\n \n // Explain: charge limiting\n if(unassigned_power \u003e battery_assignable_power \u0026\u0026 battery_assignable_power \u003c battery.charging_max) {\n log(this,`Charging limited to protect battery (${battery.soc}%): ${battery.charging_max}W -\u003e ${battery_assignable_power}W`);\n }\n // Explain: solution result\n log(this, `\u003cfont color=\"#009ac7\"\u003eBattery ${solution.id}\u003c/font\u003e: assigned to ${solution.mode} with ${solution.power}W / ${isCharging ? battery.charging_max : battery.discharging_max }W max.`)\n\n // remaining power to assign\n unassigned_power -= assign;\n batteries_total_assignable_power += Number(battery_assignable_power);\n});\n\n// battery discharge priority - put solutions back in initial order\nif (!isCharging \u0026\u0026 hasReverseDischargePriority) {\n solution_array.reverse();\n}\n\n// store solutions in msg\nRED.util.setMessageProperty(msg, \"solutions\", solution_array,true);\n\n// OUTFLOW\nflow.set(\"batteries_total_assignable_power\", batteries_total_assignable_power); // this is set for the anti-windup calculation in the earlier \u0027calculate corrections\u0027 node, which will pick this up during next iteration.\n\n// OUTPUT\nreturn msg; // to Home Battery flow","outputs":1,"timeout":0,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n\ncontext.set(\"lasttime_active\",[]);","finalize":"","libs":[],"x":420,"y":1160,"wires":[["9197f431cd22f55c","188b6d74052d7b75"]],"inputLabels":["isCharging (boolean)"]},{"id":"9197f431cd22f55c","type":"debug","z":"2dc255585c9021ad","g":"81378689693e940b","name":"Load distribution","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"solutions","targetType":"msg","statusVal":"","statusType":"auto","x":640,"y":1160,"wires":[]},{"id":"deae32c2aaf84154","type":"switch","z":"2dc255585c9021ad","g":"085064404e672245","name":"Pass or Halt","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"halted by deadband","vt":"str"},{"t":"neq","v":"halted by deadband","vt":"str"}],"checkall":"false","repair":false,"outputs":2,"x":910,"y":340,"wires":[["2f97f335f207820a"],["72ac88c55f926b58"]]},{"id":"188b6d74052d7b75","type":"link out","z":"2dc255585c9021ad","g":"481e254058a8227d","name":"Return","mode":"return","links":[],"x":225,"y":1240,"wires":[]},{"id":"d6ba85caf21c6c7a","type":"comment","z":"2dc255585c9021ad","g":"481e254058a8227d","name":"End (readme)","info":"Should return a solution_array of battery objects\n\n## battery object format\n`{{id: string|number, mode: string, power: number}} battery solution`\n- id is an arbitrary battery ID\n- mode is \"stop\", \"charge\", \"discharge\" for Marstek\n- power in Watts\n\n### example array\nreturn this type of solution_array via msg.solutions\n` \nlet solution_array = [];\nsolution_array.push({id:\"M1\", mode: \"charge\", power: 100}); // per battery\nreturn {solutions: solution_array};\n` ","x":170,"y":1180,"wires":[]},{"id":"2f97f335f207820a","type":"link out","z":"2dc255585c9021ad","g":"085064404e672245","name":"go to End","mode":"link","links":["02843f3df46ffe5b"],"x":1090,"y":340,"wires":[],"inputLabels":["Halt"],"l":true},{"id":"02843f3df46ffe5b","type":"link in","z":"2dc255585c9021ad","g":"481e254058a8227d","name":"link to End","links":["2f97f335f207820a"],"x":105,"y":1240,"wires":[["188b6d74052d7b75"]]},{"id":"729b998cf3b87cc7","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Is charging","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"","floorId":[],"areaId":[],"deviceId":[],"entityId":[],"labelId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":false,"domain":"","service":"","x":1070,"y":1000,"wires":[[]]},{"id":"83a1a6be0e294782","type":"function","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Config action","func":"const value = msg.payload;\nconst target = \"input_boolean.house_battery_control_is_charging\";\n\nmsg.payload = {\n \"action\": value?\"input_boolean.turn_on\":\"input_boolean.turn_off\",\n \"target\": {\n \"entity_id\": [target]\n },\n \"data\": {}\n}\n\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":910,"y":1000,"wires":[["729b998cf3b87cc7"]]},{"id":"c7386c2b9a7d1cc0","type":"switch","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"Target","property":"house_target_grid_consumption_in_w","propertyType":"msg","rules":[{"t":"nnull"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":390,"y":120,"wires":[["edbd6dfed6ba7652"],["89c318c6c8728833"]]},{"id":"8bc9658f77284e22","type":"debug","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"Target","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"house_target_grid_consumption_in_w","targetType":"msg","statusVal":"house_target_grid_consumption_in_w","statusType":"auto","x":790,"y":100,"wires":[]},{"id":"e2f0589bab3add2b","type":"server-state-changed","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Strategy changes","server":"176d29a.6f648d6","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["input_text.house_battery_strategy_active_sub_strategy","input_select.house_battery_strategy"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"string","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":1720,"y":340,"wires":[["85a1019efd05b4bd"]]},{"id":"85a1019efd05b4bd","type":"switch","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"Full stop","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1855,"y":340,"wires":[["2a5a379c33bdc594"]],"l":false},{"id":"bc18c2754c562a1a","type":"server-state-changed","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Full stop trigger","server":"176d29a.6f648d6","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["sensor.ev_is_charging"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"string","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":1720,"y":420,"wires":[["332d68169bd129ff"]]},{"id":"332d68169bd129ff","type":"switch","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"on","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1855,"y":420,"wires":[["2a5a379c33bdc594"]],"l":false},{"id":"3db366538fbac912","type":"rbe","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":730,"y":820,"wires":[["7ede0bd5857ee7a0"]]},{"id":"f41cc8bd3b060241","type":"rbe","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":730,"y":940,"wires":[["ed568c00a0795773"]]},{"id":"817e304f8bb80d64","type":"rbe","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":730,"y":1000,"wires":[["83a1a6be0e294782","e05c810323db1ceb"]]},{"id":"6333a8d8470cd4f5","type":"change","z":"2dc255585c9021ad","g":"ba3cab11cfa58369","name":"Trace","rules":[{"t":"set","p":"strategy.trace","pt":"msg","to":"[\t strategy.trace,\t {\t \"flow\": target,\t \"flow_settings\": advanced_settings,\t \"timestamp\": $now()\t } \t]","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":190,"y":200,"wires":[["c7386c2b9a7d1cc0"]],"info":"Attaches a breadcrumb trace to the msg\r\nso the user can reconstruct which route has\r\nbeen traveled"},{"id":"916c17ae8b44a945","type":"catch","z":"2dc255585c9021ad","g":"ff7db384260f1b22","name":"","scope":null,"uncaught":false,"x":95,"y":320,"wires":[["768fa0dcf7e8e3b5"]],"l":false},{"id":"768fa0dcf7e8e3b5","type":"function","z":"2dc255585c9021ad","g":"ff7db384260f1b22","name":"Unhandled Exception","func":"const handle = global.get(\u0027unhandledException\u0027);\n\nhandle(this);\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":145,"y":320,"wires":[["30ea27bc433988a5"]],"l":false},{"id":"30ea27bc433988a5","type":"link out","z":"2dc255585c9021ad","g":"ff7db384260f1b22","name":"link out 7","mode":"return","links":[],"x":195,"y":320,"wires":[]},{"id":"176d29a.6f648d6","type":"server","name":"Home Assistant","addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"","connectionDelay":false,"cacheJson":false,"heartbeat":false,"heartbeatInterval":"","statusSeparator":"","enableGlobalContextStore":false},{"id":"4daf6e7bcce195d0","type":"global-config","env":[],"modules":{"node-red-contrib-home-assistant-websocket":"0.80.3","node-red-node-smooth":"0.1.2"}}]
+[{"id":"2dc255585c9021ad","type":"tab","label":"Strategy Self-consumption v4.4.2","disabled":false,"info":"","env":[]},{"id":"ba3cab11cfa58369","type":"group","z":"2dc255585c9021ad","name":"Strategy start","style":{"fill":"#ffffff","label":true},"nodes":["653d5ecae07626a1","c685bb5d5b350a17","6333a8d8470cd4f5"],"x":54,"y":79,"w":212,"h":162},{"id":"481e254058a8227d","type":"group","z":"2dc255585c9021ad","name":"Battery solutions","style":{"label":true,"fill":"#ffffff"},"nodes":["188b6d74052d7b75","d6ba85caf21c6c7a","02843f3df46ffe5b"],"x":64,"y":1139,"w":202,"h":142},{"id":"4bd04882ac08f740","type":"group","z":"2dc255585c9021ad","name":"Bumpless operation","style":{"label":true,"stroke":"#bfdbef"},"nodes":["8319c2bf362f5d55","c07fd441a19ddcd6","751cc0061f48cf83","1526a33d800732ac"],"x":1614,"y":19,"w":252,"h":242},{"id":"fa53931ece8d9e5c","type":"group","z":"2dc255585c9021ad","name":"Bumpless operation - switching control modes","style":{"stroke":"#bfdbef","label":true},"nodes":["af9242d4df5098ea","2d4aaea3163ee0b0","3c7a17a46ed568bb","b67191747e0bfe44","bdcb744226755ff7","2a5a379c33bdc594","ce822e5f44a30bc2","a98d3ba5387274d6","e2f0589bab3add2b","85a1019efd05b4bd","bc18c2754c562a1a","332d68169bd129ff"],"x":1614,"y":279,"w":592,"h":289.5},{"id":"b9b7c6e5106b3a7c","type":"group","z":"2dc255585c9021ad","name":"Inputs","style":{"fill":"#7fb7df","label":true,"color":"#ffffff"},"nodes":["d4bd6010b2a82002","b753f4385c7d13bd"],"x":288,"y":13,"w":904,"h":234},{"id":"f9161a30652a1e37","type":"group","z":"2dc255585c9021ad","name":"Pre processing","style":{"fill":"#bfdbef","label":true,"color":"#ffffff"},"nodes":["289b3a4f3ed9895e","15a8430ec8fd32cd","085064404e672245"],"x":288,"y":273,"w":1104,"h":294},{"id":"e90c3db5ee8c6a36","type":"group","z":"2dc255585c9021ad","name":"Control","style":{"fill":"#7fb7df","label":true,"color":"#ffffff"},"nodes":["0538d5e767988695","81378689693e940b"],"x":288,"y":593,"w":924,"h":634},{"id":"ff7db384260f1b22","type":"group","z":"2dc255585c9021ad","name":"Unhandled exceptions","style":{"label":true,"stroke":"#d88a8a","color":"#d88a8a"},"nodes":["916c17ae8b44a945","768fa0dcf7e8e3b5","30ea27bc433988a5"],"x":54,"y":279,"w":182,"h":82},{"id":"d4bd6010b2a82002","type":"group","z":"2dc255585c9021ad","g":"b9b7c6e5106b3a7c","name":"PID control inputs","style":{"label":true,"fill":"#ffffff"},"nodes":["89c318c6c8728833","e5144d3a238687bb","8ce405166bb8ae24","ae7e3834fba26c76","c7386c2b9a7d1cc0","edbd6dfed6ba7652","8bc9658f77284e22","800b817e2d30700f","fd84a7d1648928c8"],"x":314,"y":59,"w":572,"h":162},{"id":"b753f4385c7d13bd","type":"group","z":"2dc255585c9021ad","g":"b9b7c6e5106b3a7c","name":"Advanced features","style":{"label":true,"fill":"#ffffff"},"nodes":["f6c435539e7250e3","fe2a55ed5fe6bed5","58b564f75d3c21c3"],"x":914,"y":39,"w":252,"h":182},{"id":"289b3a4f3ed9895e","type":"group","z":"2dc255585c9021ad","g":"f9161a30652a1e37","name":"","style":{"fill":"#ffffff","label":true},"nodes":["858221520cee3dc5","735a9b3b250cd260","cd0dc47e2ff890dd","7726f1429c179f68"],"x":314,"y":299,"w":292,"h":202},{"id":"15a8430ec8fd32cd","type":"group","z":"2dc255585c9021ad","g":"f9161a30652a1e37","name":"Derivative - incl. bumpless operation","style":{"label":true,"stroke":"#a4a4a4","fill":"#ffffff"},"nodes":["7976d4a42e873093","823be9b66feeb0da","72ac88c55f926b58","e002c8dde1136e44","700e6af7e2972644"],"x":614,"y":399,"w":752,"h":142},{"id":"085064404e672245","type":"group","z":"2dc255585c9021ad","g":"f9161a30652a1e37","name":"Processing required? ","style":{"label":true,"fill":"#ffffff"},"nodes":["ea326ab0246b8c13","deae32c2aaf84154","2f97f335f207820a"],"x":614,"y":299,"w":562,"h":82},{"id":"0538d5e767988695","type":"group","z":"2dc255585c9021ad","g":"e90c3db5ee8c6a36","name":"PID controller","style":{"label":true,"fill":"#ffffff","color":"#3f3f3f"},"nodes":["a826eba0ed0ac7fc","e0644847be635f58","ad085261cf8a2cc1","2dc42d364f58d67f","7ede0bd5857ee7a0","5370c30d8cdab4b8","ed568c00a0795773","e05c810323db1ceb","16582ddbc62d0305","303bb8cea00177f9","829124787188e159","d4422f8539cc8ecf","71d8d3b4f6ef9310","4563c0e2adab2fee","9de6d5774b6b0bfa","729b998cf3b87cc7","83a1a6be0e294782","3db366538fbac912","f41cc8bd3b060241","817e304f8bb80d64"],"x":314,"y":619,"w":872,"h":482},{"id":"81378689693e940b","type":"group","z":"2dc255585c9021ad","g":"e90c3db5ee8c6a36","name":"Distribution","style":{"label":true,"fill":"#ffffff","color":"#3f3f3f"},"nodes":["3acf8bf116f857e1","9197f431cd22f55c"],"x":314,"y":1119,"w":452,"h":82},{"id":"edbd6dfed6ba7652","type":"junction","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","x":460,"y":100,"wires":[["800b817e2d30700f"]]},{"id":"800b817e2d30700f","type":"junction","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","x":700,"y":100,"wires":[["8bc9658f77284e22","fd84a7d1648928c8"]]},{"id":"fd84a7d1648928c8","type":"junction","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","x":720,"y":120,"wires":[["e5144d3a238687bb"]]},{"id":"653d5ecae07626a1","type":"link in","z":"2dc255585c9021ad","g":"ba3cab11cfa58369","name":"Self-consumption","links":[],"x":160,"y":160,"wires":[["6333a8d8470cd4f5"]],"l":true},{"id":"c685bb5d5b350a17","type":"comment","z":"2dc255585c9021ad","g":"ba3cab11cfa58369","name":"Start (readme)","info":"# Setting up your battery strategy flow\n\n## Setup\nName the Link In in this start group exactly after the option configured in your\nselect_input.house_battery_strategy\n\nfound in the file:\ninput_select_house_battery_control.yaml\n\n### example\n`house_battery_strategy:\n name: House Battery Strategy\n options:\n - AcmE example`\n\nWill point to the flow containing a Link In node\ncalled \u0027AcmE example\u0027 (case sensitive).","x":150,"y":120,"wires":[]},{"id":"9b4c669af44fc4a8","type":"comment","z":"2dc255585c9021ad","name":"Home Battery Strategy","info":"The `Home Battery Start` flow will call this strategy flow\nby matching the strategy name with the Link In name.\n\nConfigure the Link In node in the Start group.\n\nDon\u0027t modify other parts of the Start and End groups.\nThey handle the calling and returning for you.","x":120,"y":40,"wires":[]},{"id":"8319c2bf362f5d55","type":"server-state-changed","z":"2dc255585c9021ad","g":"4bd04882ac08f740","name":"User Ki change","server":"176d29a.6f648d6","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["input_number.house_battery_control_ki"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":true,"ignorePrevStateUnknown":true,"ignorePrevStateUnavailable":true,"ignoreCurrentStateUnknown":true,"ignoreCurrentStateUnavailable":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":1720,"y":100,"wires":[["c07fd441a19ddcd6"]]},{"id":"c07fd441a19ddcd6","type":"function","z":"2dc255585c9021ad","g":"4bd04882ac08f740","name":"Integral term adjust","func":"// On change of Ki setting\nlet logdata = \"bumpless \";\nlet I_sum = flow.get(\"I_integral_sum\");\nlet Ki = msg.payload || 0; // Integral gain\nlet Ki_last = context.get(\"house_battery_control_ki_last\")||0;\n\n// if Ki was or has become zero\nif(Ki == 0 || Ki_last == 0){\n // passify the integral-sum\n flow.set(\"I_integral_sum\",0);\n context.set(\"house_battery_control_ki_last\", Ki);\n logdata += `to/from 0, Ki=${Ki_last} -\u003e ${Ki} | `;\n // done\n return { payload: logdata };\n}\n\n// change in Ki\nlet dKi = parseFloat(Ki/Ki_last);\nif(dKi \u003c= 0) dKi = 1; // ignore erroneous changes\n\n// keep I-term constant by compensating the integral-sum for the change in Ki\n// e.g. if Ki got 10% smaller, then integral-sum should increase 10%, to keep the I-term constant\nlet I_sum_new = I_sum / dKi;\nlogdata += `change of I: from ${Ki_last} to ${Ki} (${dKi * 100}%) | `;\n\n// OUTFLOW\nflow.set(\"I_integral_sum\", I_sum_new);\ncontext.set(\"house_battery_control_ki_last\", Ki);\n\n// OUTPUT\nreturn { payload: logdata };","outputs":1,"timeout":0,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n\n// init last state\nlet Ki = flow.get(\"house_battery_control_ki\") || 0;\ncontext.set(\"house_battery_control_ki_last\", Ki);","finalize":"","libs":[],"x":1730,"y":160,"wires":[["1526a33d800732ac"]]},{"id":"751cc0061f48cf83","type":"comment","z":"2dc255585c9021ad","g":"4bd04882ac08f740","name":"Bumpless Ki changes","info":"Recalc I-term whenever Ki changes\n\nNote: always be mindful of making changes to control parameters of systems in operation.","x":1740,"y":60,"wires":[]},{"id":"1526a33d800732ac","type":"debug","z":"2dc255585c9021ad","g":"4bd04882ac08f740","name":"Logdata","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1700,"y":220,"wires":[]},{"id":"af9242d4df5098ea","type":"switch","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Check for custom/auto mode","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"Manual control","vt":"str"},{"t":"eq","v":"Marstek control","vt":"str"},{"t":"eq","v":"Full control","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":1855,"y":520,"wires":[["2a5a379c33bdc594","2d4aaea3163ee0b0"],["2a5a379c33bdc594","2d4aaea3163ee0b0"],["2d4aaea3163ee0b0"]],"l":false},{"id":"2d4aaea3163ee0b0","type":"debug","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Master control switch","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2060,"y":520,"wires":[]},{"id":"3c7a17a46ed568bb","type":"server-state-changed","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Master control mode","server":"176d29a.6f648d6","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["input_select.marstek_master_battery_mode"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":1730,"y":520,"wires":[["af9242d4df5098ea"]]},{"id":"b67191747e0bfe44","type":"api-call-service","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Integral PID to zero","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_i_term"],"labelId":[],"data":"{\"value\": \"0\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":2050,"y":360,"wires":[["bdcb744226755ff7"]]},{"id":"bdcb744226755ff7","type":"api-call-service","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Differential PID to zero","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_d_term"],"labelId":[],"data":"{\"value\": \"0\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":2060,"y":400,"wires":[["ce822e5f44a30bc2"]]},{"id":"2a5a379c33bdc594","type":"api-call-service","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Proportional PID to zero","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_p_term"],"labelId":[],"data":"{\"value\": \"0\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":2060,"y":320,"wires":[["b67191747e0bfe44"]]},{"id":"ce822e5f44a30bc2","type":"api-call-service","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"PID Output to zero","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_pid_output"],"labelId":[],"data":"{\"value\": \"0\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":2050,"y":440,"wires":[["a98d3ba5387274d6"]]},{"id":"a98d3ba5387274d6","type":"change","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Integral sum to zero","rules":[{"t":"set","p":"I_integral_sum","pt":"flow","to":"0","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2060,"y":480,"wires":[[]]},{"id":"89c318c6c8728833","type":"api-current-state","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"Target grid consumption","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_target_grid_consumption_in_w","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"string","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_target_grid_consumption_in_w","propertyType":"msg","value":"string","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":570,"y":120,"wires":[["800b817e2d30700f"]],"info":"Set at 0 to strive for zero consumption"},{"id":"58b564f75d3c21c3","type":"api-current-state","z":"2dc255585c9021ad","g":"b753f4385c7d13bd","name":"Hysteresis","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_hysteresis_in_w","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_hysteresis_in_w","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":1010,"y":120,"wires":[["f6c435539e7250e3"]]},{"id":"e5144d3a238687bb","type":"api-current-state","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"PID P-value","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_kp","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_kp","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":410,"y":180,"wires":[["8ce405166bb8ae24"]]},{"id":"8ce405166bb8ae24","type":"api-current-state","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"PID I-value","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_ki","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_ki","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":590,"y":180,"wires":[["ae7e3834fba26c76"]]},{"id":"ae7e3834fba26c76","type":"api-current-state","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"PID D-value","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_kd","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_kd","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":770,"y":180,"wires":[["fe2a55ed5fe6bed5"]]},{"id":"f6c435539e7250e3","type":"function","z":"2dc255585c9021ad","g":"b753f4385c7d13bd","name":"Advanced settings","func":"// 1. Hard coded advanced settings (flow level)\nflow.set(\"has_soc_charging_limiter\", true); // slows charging from 90% to 100% to improve battery life\nflow.set(\"has_reverse_priority_discharge\", true); // Prioritize discharging and charging the same battery when possible\nflow.set(\"master_gain\", 1); // Gain scheduling. Not Implemented!\n\n// 2. Configurable advanced settings (msg level)\nmsg.advanced_settings ||= {}; // creates \u0027advanced_settings\u0027 if it does net yet exist\nlet mas = msg.advanced_settings;\n\n// disable charge or dicharge solutions and changes them to stop solutions\nRED.util.setMessageProperty(msg,\"advanced_settings.discharge_disabled\", mas.discharge_disabled ?? false);\nRED.util.setMessageProperty(msg,\"advanced_settings.charge_disabled\", mas.charge_disabled ?? false);\n// The nullish coalescing (??) operator is a logical operator that returns its right-hand side operand when its left-hand side operand is null or undefined\n\n// 3. continue\nreturn msg;\n\n// Notes for home battery tinkerer friends:\n// Using RED.util.getMessageProperty / RED.util.setMessageProperty is more performant and safe.\n// If any part of the path (\"foo.bar\") is missing, it will return \u0027undefined\u0027 without throwing an error.","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1030,"y":180,"wires":[["735a9b3b250cd260"]]},{"id":"fe2a55ed5fe6bed5","type":"api-current-state","z":"2dc255585c9021ad","g":"b753f4385c7d13bd","name":"Idle time before Stop","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_idle_time","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"idle_time_minutes","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":1040,"y":80,"wires":[["58b564f75d3c21c3"]]},{"id":"858221520cee3dc5","type":"comment","z":"2dc255585c9021ad","g":"289b3a4f3ed9895e","name":"Setpoint ramping / damping","info":"","x":460,"y":460,"wires":[]},{"id":"735a9b3b250cd260","type":"api-current-state","z":"2dc255585c9021ad","g":"289b3a4f3ed9895e","name":"Error signal dampening","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_error_signal_dampening","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_error_signal_dampening","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":450,"y":340,"wires":[["cd0dc47e2ff890dd"]]},{"id":"cd0dc47e2ff890dd","type":"api-current-state","z":"2dc255585c9021ad","g":"289b3a4f3ed9895e","name":"Output signal dampening","server":"176d29a.6f648d6","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_number.house_battery_control_pid_output_dampening","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"house_battery_control_pid_output_dampening","propertyType":"flow","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":450,"y":380,"wires":[["7726f1429c179f68"]]},{"id":"7726f1429c179f68","type":"function","z":"2dc255585c9021ad","g":"289b3a4f3ed9895e","name":"Set: Error signal","func":"// INPUTS\nlet P1_error_last = Number(context.get(\"p1_error_last\"))||0; // last error signal level in W\nlet dampening = Number(flow.get(\"house_battery_control_error_signal_dampening\")) || 0; // 0% - 100%\ndampening = dampening / 100; // to Number\n\n// Process Variable (PV) currently measured grid power, Setpoint (SP) desired grid power, set 0 for NoM\nvar P1_power = msg.grid_power; // PV: consume is positive, supply to grid is negative\nvar P1_setpoint = Number(RED.util.getMessageProperty(msg,\"house_target_grid_consumption_in_w\")); // SP: target value\n\n// Calc error signal\nlet P1_error_unfiltered = P1_setpoint - P1_power;\n\n// Simple averaging filter\nlet P1_error_filtered = (1 - dampening) * P1_error_unfiltered + (dampening) * P1_error_last;\n\n// OUTFLOW\ncontext.set(\"p1_error_last\", P1_error_filtered);\n\n// OUTPUT\nmsg.grid_error = P1_error_filtered; // W (watt), error signal = SP - PV\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":420,"wires":[["ea326ab0246b8c13"]]},{"id":"7976d4a42e873093","type":"comment","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Bumpless target grid consumption","info":"The error value is used by default to calculate the derivative.\nWhen the target grid consumption (tgc) is changed, the system is takes the derivative of the process variable instead.\nThis prevents irratic behavior during sudden changes of the error or setpoint value.\n\ne.g. you change the setpoint from 0 to 100W \nThe error would make an instantanious jump and cause a huge derivative-term.\nThe pv stays continous and fluent.\n\nAfter 1 control tick the derivative is taken from error again.\nUntil the `tgc` is changed again.","x":780,"y":440,"wires":[]},{"id":"823be9b66feeb0da","type":"function","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Derivative PV","func":"var P1_power = msg.grid_power||0;\nvar P1_last_power = Number(context.get(\"p1_last_power\")) ||0;\n\n// PV derivative\nvar p1_derivative = P1_power - P1_last_power; // devided by unity seconds\n\n// OUTFLOW\ncontext.set(\"p1_last_power\", P1_power);\n\n// OUTPUT\nmsg.grid_derivative = p1_derivative;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":500,"wires":[["700e6af7e2972644"]]},{"id":"72ac88c55f926b58","type":"switch","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Target grid consumption unchanged","property":"house_target_grid_consumption_in_w","propertyType":"flow","rules":[{"t":"eq","v":"","vt":"prev"},{"t":"neq","v":"","vt":"prev"}],"checkall":"false","repair":false,"outputs":2,"x":780,"y":480,"wires":[["e002c8dde1136e44"],["823be9b66feeb0da"]]},{"id":"e002c8dde1136e44","type":"function","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Derivative Error","func":"var P1_error = msg.grid_error||0;\nvar P1_last_error = Number(context.get(\"p1_last_error\")) ||0;\n\n// Error derivative\n// note: error = -PV\n// note: for simplicity we assume 1 second time steps\nlet p1_derivative = -(P1_error - P1_last_error); // devided by unity seconds\n\n// OUTFLOW\ncontext.set(\"p1_last_error\", P1_error);\n\n// OUTPUT\nmsg.grid_derivative = p1_derivative;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":460,"wires":[["700e6af7e2972644"]]},{"id":"700e6af7e2972644","type":"function","z":"2dc255585c9021ad","g":"15a8430ec8fd32cd","name":"Derivative final value","func":"\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1240,"y":480,"wires":[["a826eba0ed0ac7fc"]]},{"id":"ea326ab0246b8c13","type":"function","z":"2dc255585c9021ad","g":"085064404e672245","name":"Deadband (15W)","func":"// Deadband\n// Stop processing if error is within .. Watt of target grid consumption.\n// This to reduce CPU loads \n\n// Logger\nconst log = global.get(\"logger\");\n\n// P1\nlet P1_error = msg.grid_error; // Watt\nlet P1_deadband = 15; // Watt\n\n// TODO\n// based on last \u0027assignable power\u0027 and current error, if (error \u003e assignable power) the system will never enter the deadband. And keep calculating each interation.\n// however since we don\u0027t know if the is the assignable power will change, until we calculate the Control Value / load balancer\n// we could end up in a deadlock if we simply stop execution here, based on assignable power alone.\n\n// Within Deadband\nif(Math.abs(P1_error) \u003c P1_deadband) {\n // stop processing\n node.status({fill:\"yellow\",shape:\"dot\",text:`Halt, ${Math.round(Math.abs(P1_error))} W`});\n log(this, `**Stopped processing** 🌿power saving, ${Math.round(Math.abs(P1_error))} W is within deadband of ${P1_deadband} W`);\n msg.payload = \u0027halted by deadband\u0027;\n return msg;\n} \n\n// Outside deadband\nnode.status({fill:\"green\",shape:\"dot\",text:`Pass, ${Math.round(Math.abs(P1_error))} W`});\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":340,"wires":[["deae32c2aaf84154"]]},{"id":"a826eba0ed0ac7fc","type":"function","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Calculate corrections","func":"// Logger\nvar logdata = \"\"; // deprecated\nconst log = global.get(\"logger\");\n\n// Timing\nlet time_last = context.get(\u0027time_last\u0027) || Date.now(); // Milliseconds\nlet time_current = Date.now(); // Milliseconds\nlet time_delta = (time_current - time_last) / 1000; // Convert to seconds\n// note: due to inherent 1 sec intervals of P1 meter we omit dt terms. This is mostly for bug checking and optimizations.\n\n// Batteries\nvar B_power = msg.batteries_total_power; // charging is positive, discharging negative\nvar anti_windup_threshold = Number(flow.get(\"batteries_total_assignable_power\")) || 0; // max available charge/discharge power.\n\n// Process Variable (PV) currently measured grid power, Setpoint (SP) desired grid power, set 0 for NoM\nvar P1_power = msg.grid_power; // PV: consume is positive, supply to grid is negative\nvar P1_setpoint = flow.get(\"house_target_grid_consumption_in_w\"); // SP: target value\nvar P1_error = msg.grid_error; // W (watt), error signal = SP - PV\nvar P1_derivative = msg.grid_derivative; // Derivative of PV, not the error\n\n// PID values\nvar Kp = flow.get(\"house_battery_control_kp\") || 0.75; // Proportional gain\nvar Ki = flow.get(\"house_battery_control_ki\") || 0; // Integral gain Ki = Kp/Ti\nvar Kd = flow.get(\"house_battery_control_kd\") || 0; // Derivative gain Kd = Kp*Td\n\n// helpers\nvar I_integral_sum = context.get(\u0027I_integral_sum\u0027) || 0;\nvar Gm = flow.get(\"master_gain\")||1; // gain scheduling\n\n// optimizations\nvar hysteresis = flow.get(\"house_battery_control_hysteresis_in_w\"); // W (watt)\n\n// advanced settings\nconst isDischargeDisabled = RED.util.getMessageProperty(msg,\"advanced_settings.discharge_disabled\");\nconst isChargeDisabled = RED.util.getMessageProperty(msg,\"advanced_settings.charge_disabled\");\n\n// -- PID regulator --\n// Proportional term\nlet p_term = Gm * Kp * P1_error;\n\n// Integral term\nlet integral_max = 0;\nif (Ki \u003e 0) {\n // Integral term | integrate\n I_integral_sum += P1_error; \n // Integral term | apply anti-windup\n integral_max = anti_windup_threshold / Ki; // the anti-windup value is set to the current controlable range of the batteries, given by the previous load balancer calculations\n I_integral_sum = Math.min(Math.max(I_integral_sum, -integral_max), integral_max);\n} else {\n // If Ki is 0, keep everything 0\n I_integral_sum = 0;\n integral_max = 0;\n}\n\n// Integral term | the term\nlet i_term = Ki * I_integral_sum;\n\n// Integral term | explain\nlogdata += `anti_windup_threshold=${anti_windup_threshold}, integral_max=${integral_max}| `; // deprecated\n\n// Differential term\nlet d_term = Gm * Kd * (-P1_derivative); // omitted \u0027/time_delta\u0027. inherent P1 refresh fequency.\n\n// Total PID output\nvar PID_output = p_term + i_term + d_term; // W (watt), the control input for the battery packs\n\n// Charging or Discharging states\nvar B_was_charging = context.get(\"batteries_charging_last\") || false;\nvar B_is_charging = PID_output \u003e 0 ? true : false;\n\n// Explain\nlogdata += `U(${time_delta}s)[${PID_output}] = P(${Kp})[${p_term}] + I(${Ki})[${i_term}] + D(${Kd})[${d_term}] | `; // deprecated\nlog(this,`**PID Controller**`);\nlog(this,`Step size: ${time_delta} s`);\nif(Kp == 0 \u0026\u0026 Ki == 0 \u0026\u0026 Kd == 0) {\n log(this, `\u003cfont color=\"orange\"\u003e**Controller disabled**\u003c/font\u003e All PID gains are 0. Choose a PID preset or set gains \u003e 0.`,\"warn\");\n} else {\n log(this, `P-term: ${Math.floor(p_term)} W @ P-gain ${Kp}`);\n log(this, `I-term: ${Math.floor(i_term)} W @ I-gain ${Ki} (max. ${anti_windup_threshold} W)`);\n log(this, `D-term: ${Math.floor(d_term)} W @ D-gain ${Kd}`);\n log(this, `**PID ${B_is_charging ? \u0027Charge\u0027 : \u0027Discharge\u0027}:** ${Math.floor(p_term)} + ${Math.floor(i_term)} + ${Math.floor(d_term)} = **${Math.round(PID_output)} Watt**`);\n}\n\n// Only one of the advanced settings below can be active at once.\n// Advanced setting | discharge disabled\nif(isDischargeDisabled \u0026\u0026 !B_is_charging) {\n // log explain\n logdata += `Discharging disabled, PID unwound U(${time_delta})[0]=0+0+0 | `;\n log(this,\u0027**PID set to 0**. Discharge was disabled via `msg.advanced_settings`.\u0027);\n // Set all PID terms to 0 and unwind integral sum\n PID_output = 0;\n p_term = 0;\n i_term = 0;\n d_term = 0;\n I_integral_sum = 0; // unwind\n // maintain charge scenario\n B_is_charging = true;\n \n// Advanced setting | charge disabled\n} else if (isChargeDisabled \u0026\u0026 B_is_charging) {\n // log explain\n logdata += `Charging disabled, PID unwound U(${time_delta})[0]=0+0+0 | `;\n log(this,\u0027**PID set to 0**. Charge was disabled via `msg.advanced_settings`.\u0027);\n // Set all PID terms to 0 and unwind integral sum\n PID_output = 0;\n p_term = 0;\n i_term = 0;\n d_term = 0;\n I_integral_sum = 0; // unwind\n // maintain discharge scenario\n B_is_charging = false;\n\n// Advanced setting | hysteresis\n} else if \n// prevents excessive switching between (dis)charge mode around the zero point.\n// if new PID_output lies within hysteresis, it will not switch charge mode. \n// set hysteresis to 0, to apply no hysteresis\n(B_is_charging !== B_was_charging \u0026\u0026 Math.abs(PID_output) \u003c hysteresis){\n // log explain\n logdata += `Hysteresis prevented charge mode switch from ${B_was_charging ? \"charging\" : \"discharging\"} to ${B_is_charging ? \"charging\" : \"discharging\"} as ${Math.abs(PID_output)}W \u003c= ${hysteresis}(hyst) | `;\n log(this,`Hysteresis prevented charge mode switch from ${B_was_charging ? \"charging\" : \"discharging\"} to ${B_is_charging ? \"charging\" : \"discharging\"} as ${Math.abs(PID_output)}W \u003c= ${hysteresis}(hyst)`);\n // prevent negative charging or positive discharging values (not allowed)\n PID_output = (PID_output \u003c 0 \u0026\u0026 B_is_charging) || ((PID_output \u003e 0 \u0026\u0026 !B_is_charging)) ? 0 : PID_output;\n // maintain previous scenario\n B_is_charging = B_was_charging;\n} \n\n// save for next iteration\ncontext.set(\"batteries_charging_last\", B_is_charging); //boolean\n\n// OUTFLOW\ncontext.set(\"time_last\", Date.now());\ncontext.set(\"I_integral_sum\", I_integral_sum);\n\n// OUTPUT\nRED.util.setMessageProperty(msg, \"batteries_charging\", B_is_charging, true);\nRED.util.setMessageProperty(msg, \"I_integral_sum\", I_integral_sum,true);\nmsg.payload = Number(PID_output);\n\n\nreturn [msg, // payload = PID_output\n { payload: Number(P1_error)},\n { payload: parseFloat(p_term)},\n { payload: parseFloat(i_term)},\n { payload: parseFloat(d_term)},\n { payload: B_is_charging},\n { payload: logdata }];","outputs":7,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":700,"wires":[["71d8d3b4f6ef9310"],["ad085261cf8a2cc1"],["3db366538fbac912"],["9de6d5774b6b0bfa"],["f41cc8bd3b060241"],["817e304f8bb80d64"],["e0644847be635f58"]],"inputLabels":["battery_array"]},{"id":"e0644847be635f58","type":"debug","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Logdata","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":720,"y":1060,"wires":[]},{"id":"ad085261cf8a2cc1","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Error Signal","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_error_signal"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":730,"y":760,"wires":[["829124787188e159"]]},{"id":"2dc42d364f58d67f","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"PID Output","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_pid_output"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":730,"y":700,"wires":[["16582ddbc62d0305"]]},{"id":"7ede0bd5857ee7a0","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Proportional term","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_p_term"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":920,"y":820,"wires":[[]]},{"id":"5370c30d8cdab4b8","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Integral term","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_i_term"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":910,"y":880,"wires":[[]]},{"id":"ed568c00a0795773","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Differential term","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_d_term"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":920,"y":940,"wires":[[]]},{"id":"e05c810323db1ceb","type":"debug","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Charging","active":false,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":900,"y":1060,"wires":[]},{"id":"16582ddbc62d0305","type":"smooth","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"","property":"payload","action":"sd","count":"8","round":"","mult":"single","reduce":false,"x":900,"y":700,"wires":[["303bb8cea00177f9"]]},{"id":"303bb8cea00177f9","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"PID deviation","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_pid_output_deviation"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":1070,"y":700,"wires":[[]]},{"id":"829124787188e159","type":"smooth","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"","property":"payload","action":"sd","count":"8","round":"","mult":"single","reduce":false,"x":900,"y":760,"wires":[["d4422f8539cc8ecf"]]},{"id":"d4422f8539cc8ecf","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Error deviation","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"input_number.set_value","floorId":[],"areaId":[],"deviceId":[],"entityId":["input_number.house_battery_control_error_deviation"],"labelId":[],"data":"{\"value\": \"{{payload}}\"}","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"input_number","service":"set_value","x":1080,"y":760,"wires":[[]]},{"id":"71d8d3b4f6ef9310","type":"function","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Output dampening","func":"// INPUT\nlet PID_unfiltered = msg.payload; // W Watt\nlet PID_last = context.get(\"PID_last\")||0; // W watt\nlet PID_damp = Number(flow.get(\"house_battery_control_pid_output_dampening\"))||0; // 0% - 100%\nPID_damp = PID_damp/100; // to Number\n\n// simple averaging filter\nlet PID_filtered = (1-PID_damp)*PID_unfiltered + (PID_damp)*PID_last;\n\n// OUTFLOW\ncontext.set(\"PID_last\", PID_filtered);\n\n// OUTPUT\nmsg.payload = Number(PID_filtered)\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":750,"y":660,"wires":[["4563c0e2adab2fee"]]},{"id":"4563c0e2adab2fee","type":"function","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Output failsafe","func":"// INFLOW\nlet maxCharge = msg.batteries_max_charge_power||undefined;\nlet maxDischarge = msg.batteries_max_discharge_power||undefined;\nlet isCharging = msg.batteries_charging||undefined;\nlet PID_output = msg.payload; // Watt\n\n// No failsafe\nif (maxCharge === undefined || maxDischarge == undefined) {\n node.status({ fill: \"red\", shape: \"dot\", text: \"disabled\" });\n node.warn(`Output Failsafe is INACTIVE. Max charge or discharge limits are undefined`);\n // continue without safeguard\n return msg;\n} \n\n// limit\nlet limit = 0;\n\n// charging state unknown\nif(isCharging === undefined) {\n // limit on lowest of charge / discharge\n limit = Math.min(maxCharge,maxDischarge); \n}\n// charging state known\nlimit = isCharging ? maxCharge : maxDischarge;\n\n// Safe output\nif (PID_output \u003c= limit){\n node.status({ fill: \"green\", shape: \"dot\", text: \"safe\" });\n return msg;\n} \n\n// Unsafe, limit output\nnode.status({ fill: \"yellow\", shape: \"ring\", text: \"power limiting\" });\nmsg.payload = Number(limit);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1080,"y":660,"wires":[["3acf8bf116f857e1","2dc42d364f58d67f"]]},{"id":"9de6d5774b6b0bfa","type":"rbe","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":730,"y":880,"wires":[["5370c30d8cdab4b8"]]},{"id":"3acf8bf116f857e1","type":"function","z":"2dc255585c9021ad","g":"81378689693e940b","name":"Load distribution","func":"// Logger\nconst log = global.get(\"logger\");\nlog(this,\"**PID load distribution**\");\n\n// INPUT\nvar batteries = msg.batteries; // Battery configuration as provided by `Home Batteries IO` flow\nvar isCharging = msg.batteries_charging; // Are we in a battery charging scenario?\nvar hasChargingLimiter = flow.get(\"has_soc_charging_limiter\") || false; // Battery life improvement\nvar hasReverseDischargePriority = flow.get(\"has_reverse_priority_discharge\") || false; // Prioritize (dis)charging the same battery\nconst isDischargeDisabled = RED.util.getMessageProperty(msg,\"advanced_settings.discharge_disabled\"); // Disallow discharging?\n\n// how much power do the batteries need to compensate?\nvar unassigned_power = Math.round(Math.abs(msg.payload)); // Power in W to compensate using batteries\nlog(this,`How much power do the batteries need to compensate: ${unassigned_power} W`);\n\n// inits \nvar solution_array = []; // load distribution solutions\nvar batteries_total_assignable_power = 0; // Current max. available total (dis)charge power. Exceeding this max should trigger the anti-windup routine for the Integral terms\n\n// enums\nconst CMODE = {\n STOP: \"stop\", // Marstek batteries disconnect a relay when this is set or Power is 0W\n CHARGE: \"charge\",\n DISCHARGE: \"discharge\"\n}\n\n/**\n** Battery life improvement (slow charge near max SoC)\n* @param {number} soc\n* @param {number} max_power\n*/\nfunction chargingLimiter(soc, max_power) {\n // Must be greater than zero\n let limitedPower = Math.max(0, max_power); \n\n // Limit charge at high SoC\n if (hasChargingLimiter) {\n if (soc \u003e= 98) limitedPower = Math.min(300, limitedPower);\n else if (soc \u003e= 95) limitedPower = Math.min(500, limitedPower);\n else if (soc \u003e= 85) limitedPower = Math.min(1500, limitedPower);\n }\n return limitedPower;\n}\n\n/**\n* battery life improvement (disconnects battery only if the battery is not used for ... seconds)\n* @param {string | number} batteryID\n* @returns {{id: string|number, mode: string, power: number}} battery solution\n*/\nfunction getStopSolution(batteryID) {\n // idle time, before stop is given to inverter relay.\n let time_idle_minimum = flow.get(\"idle_time_minutes\")*60*1000||0; // Milliseconds\n\n // retrieve last registered active times for each battery based on their ids\n let lasttime_active = context.get(\"lasttime_active\");\n \n // battery exists in register \n if(lasttime_active[batteryID] !== undefined){\n // timing\n let time_last = lasttime_active[batteryID]; // Milliseconds \n let time_now = Date.now(); // Milliseconds \n let time_idle = (time_now - time_last); // Milliseconds\n \n // battery is ready for STOP\n if (time_idle \u003e= time_idle_minimum) {\n log(this,`Battery ${batteryID} [STOP]: 0 Watt and disconnect relay.`);\n return {id: batteryID, mode: CMODE.STOP, power:0}; // STOP or 0 Watt disconnects relay\n }\n \n // battery is kept IDLE, while awaiting minimum inactive time\n log(this,`Battery ${batteryID} [IDLE]: 1 Watt for ${Math.round((time_idle_minimum-time_idle)/1000)}s. Keep relay engaged.`);\n return { id: batteryID, mode: isCharging ? CMODE.CHARGE : CMODE.DISCHARGE, power: 1 }; // 1 Watt keeps battery IDLE, keeping relay engaged\n }\n\n // battery missing in register\n log(this,`Battery ${batteryID} [IDLE]: unknown last active time`);\n return getActiveSolution(batteryID, 1); // set a last active time and engage idle state\n}\n\n/**\n* Get battery solution and register a last active time.\n* @param {any} batteryID\n* @param {number} assigned_power\n* @returns {{id: string|number, mode: string, power: number}} battery solution\n*/\nfunction getActiveSolution(batteryID, assigned_power){\n // register battery as active\n let lasttime_active = context.get(\"lasttime_active\")||[]; // last registered active times for each battery based on their ids\n lasttime_active[batteryID] = Date.now();\n context.set(\"lasttime_active\", lasttime_active);\n\n // solution\n return {\n id: batteryID,\n mode: isCharging ? CMODE.CHARGE : CMODE.DISCHARGE,\n power: Math.round(assigned_power)\n };\n}\n\n// -- BATTERY DISCHARGE PRIORITY\nif (!isCharging \u0026\u0026 hasReverseDischargePriority) {\n batteries.reverse();\n}\n\n// -- LOAD BALANCER --\nbatteries.forEach((/** @type {{ id: any; soc: number; soc_min: number; soc_max: number; rs485: string; charging_max: number; discharging_max: any; }} */ battery) =\u003e {\n \n // Explain\n log(this, `Battery ${battery.id}: ${battery.soc}% (min: ${battery.soc_min}%, max: ${battery.soc_max}%) ; RS485: ${battery.rs485==\u0027enable\u0027?\u0027OK\u0027:\u0027Nok\u0027}`);\n\n // track if the battery should be skipped and why\n let skipReason = null;\n if (battery.rs485 !== \"enable\") {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because RS485 is not \u0027enable\u0027`;\n } else if (isCharging \u0026\u0026 battery.soc \u003e= battery.soc_max) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because SoC (${battery.soc}%) \u003e= Max (${battery.soc_max}%) while charging`;\n } else if (!isCharging \u0026\u0026 battery.soc \u003c= battery.soc_min) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because SoC (${battery.soc}%) \u003c= Min (${battery.soc_min}%) while discharging`;\n } else if (!isCharging \u0026\u0026 isDischargeDisabled) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped, discharging was disabled via msg property`;\n }\n\n // battery NOT AVAIABLE, skip via soft stop\n if (skipReason !== null) {\n log(this, skipReason); // communicate reason\n let solution = getStopSolution(battery.id); // get a soft stop solution for this battery\n solution_array.push(solution); // add solution, do not update last active time.\n // next battery\n return; \n }\n\n // battery is AVAILABLE, assign power\n let battery_assignable_power = isCharging ? chargingLimiter(battery.soc, battery.charging_max) : battery.discharging_max;\n let assign = Math.min(unassigned_power, battery_assignable_power);\n \n // select solution\n var solution;\n if (assign \u003c= 0 ) {\n // no power solution\n solution = getStopSolution(battery.id); // a soft stop solution\n assign = 0;\n } else {\n // assigned power solution\n solution = getActiveSolution(battery.id, Math.round(assign));\n }\n solution_array.push(solution);\n \n // Explain: charge limiting\n if(unassigned_power \u003e battery_assignable_power \u0026\u0026 battery_assignable_power \u003c battery.charging_max) {\n log(this,`Charging limited to protect battery (${battery.soc}%): ${battery.charging_max}W -\u003e ${battery_assignable_power}W`);\n }\n // Explain: solution result\n log(this, `\u003cfont color=\"#009ac7\"\u003eBattery ${solution.id}\u003c/font\u003e: assigned to ${solution.mode} with ${solution.power}W / ${isCharging ? battery.charging_max : battery.discharging_max }W max.`)\n\n // remaining power to assign\n unassigned_power -= assign;\n batteries_total_assignable_power += Number(battery_assignable_power);\n});\n\n// battery discharge priority - put solutions back in initial order\nif (!isCharging \u0026\u0026 hasReverseDischargePriority) {\n solution_array.reverse();\n}\n\n// store solutions in msg\nRED.util.setMessageProperty(msg, \"solutions\", solution_array,true);\n\n// OUTFLOW\nflow.set(\"batteries_total_assignable_power\", batteries_total_assignable_power); // this is set for the anti-windup calculation in the earlier \u0027calculate corrections\u0027 node, which will pick this up during next iteration.\n\n// OUTPUT\nreturn msg; // to Home Battery flow","outputs":1,"timeout":0,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n\ncontext.set(\"lasttime_active\",[]);","finalize":"","libs":[],"x":420,"y":1160,"wires":[["9197f431cd22f55c","188b6d74052d7b75"]],"inputLabels":["isCharging (boolean)"]},{"id":"9197f431cd22f55c","type":"debug","z":"2dc255585c9021ad","g":"81378689693e940b","name":"Load distribution","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"solutions","targetType":"msg","statusVal":"","statusType":"auto","x":640,"y":1160,"wires":[]},{"id":"deae32c2aaf84154","type":"switch","z":"2dc255585c9021ad","g":"085064404e672245","name":"Pass or Halt","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"halted by deadband","vt":"str"},{"t":"neq","v":"halted by deadband","vt":"str"}],"checkall":"false","repair":false,"outputs":2,"x":910,"y":340,"wires":[["2f97f335f207820a"],["72ac88c55f926b58"]]},{"id":"188b6d74052d7b75","type":"link out","z":"2dc255585c9021ad","g":"481e254058a8227d","name":"Return","mode":"return","links":[],"x":225,"y":1240,"wires":[]},{"id":"d6ba85caf21c6c7a","type":"comment","z":"2dc255585c9021ad","g":"481e254058a8227d","name":"End (readme)","info":"Should return a solution_array of battery objects\n\n## battery object format\n`{{id: string|number, mode: string, power: number}} battery solution`\n- id is an arbitrary battery ID\n- mode is \"stop\", \"charge\", \"discharge\" for Marstek\n- power in Watts\n\n### example array\nreturn this type of solution_array via msg.solutions\n` \nlet solution_array = [];\nsolution_array.push({id:\"M1\", mode: \"charge\", power: 100}); // per battery\nreturn {solutions: solution_array};\n` ","x":170,"y":1180,"wires":[]},{"id":"2f97f335f207820a","type":"link out","z":"2dc255585c9021ad","g":"085064404e672245","name":"go to End","mode":"link","links":["02843f3df46ffe5b"],"x":1090,"y":340,"wires":[],"inputLabels":["Halt"],"l":true},{"id":"02843f3df46ffe5b","type":"link in","z":"2dc255585c9021ad","g":"481e254058a8227d","name":"link to End","links":["2f97f335f207820a"],"x":105,"y":1240,"wires":[["188b6d74052d7b75"]]},{"id":"729b998cf3b87cc7","type":"api-call-service","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Is charging","server":"176d29a.6f648d6","version":7,"debugenabled":false,"action":"","floorId":[],"areaId":[],"deviceId":[],"entityId":[],"labelId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":false,"domain":"","service":"","x":1070,"y":1000,"wires":[[]]},{"id":"83a1a6be0e294782","type":"function","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"Config action","func":"const value = msg.payload;\nconst target = \"input_boolean.house_battery_control_is_charging\";\n\nmsg.payload = {\n \"action\": value?\"input_boolean.turn_on\":\"input_boolean.turn_off\",\n \"target\": {\n \"entity_id\": [target]\n },\n \"data\": {}\n}\n\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":910,"y":1000,"wires":[["729b998cf3b87cc7"]]},{"id":"c7386c2b9a7d1cc0","type":"switch","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"Target","property":"house_target_grid_consumption_in_w","propertyType":"msg","rules":[{"t":"nnull"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":390,"y":120,"wires":[["edbd6dfed6ba7652"],["89c318c6c8728833"]]},{"id":"8bc9658f77284e22","type":"debug","z":"2dc255585c9021ad","g":"d4bd6010b2a82002","name":"Target","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"house_target_grid_consumption_in_w","targetType":"msg","statusVal":"house_target_grid_consumption_in_w","statusType":"auto","x":790,"y":100,"wires":[]},{"id":"e2f0589bab3add2b","type":"server-state-changed","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Strategy changes","server":"176d29a.6f648d6","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["input_text.house_battery_strategy_active_sub_strategy","input_select.house_battery_strategy"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"string","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":1720,"y":340,"wires":[["85a1019efd05b4bd"]]},{"id":"85a1019efd05b4bd","type":"switch","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"Full stop","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1855,"y":340,"wires":[["2a5a379c33bdc594"]],"l":false},{"id":"bc18c2754c562a1a","type":"server-state-changed","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"Full stop trigger","server":"176d29a.6f648d6","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["sensor.ev_is_charging"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"string","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":1720,"y":420,"wires":[["332d68169bd129ff"]]},{"id":"332d68169bd129ff","type":"switch","z":"2dc255585c9021ad","g":"fa53931ece8d9e5c","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"on","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1855,"y":420,"wires":[["2a5a379c33bdc594"]],"l":false},{"id":"3db366538fbac912","type":"rbe","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":730,"y":820,"wires":[["7ede0bd5857ee7a0"]]},{"id":"f41cc8bd3b060241","type":"rbe","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":730,"y":940,"wires":[["ed568c00a0795773"]]},{"id":"817e304f8bb80d64","type":"rbe","z":"2dc255585c9021ad","g":"0538d5e767988695","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":730,"y":1000,"wires":[["83a1a6be0e294782","e05c810323db1ceb"]]},{"id":"6333a8d8470cd4f5","type":"change","z":"2dc255585c9021ad","g":"ba3cab11cfa58369","name":"Trace","rules":[{"t":"set","p":"strategy.trace","pt":"msg","to":"[\t strategy.trace,\t {\t \"flow\": target,\t \"flow_settings\": advanced_settings,\t \"timestamp\": $now()\t } \t]","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":190,"y":200,"wires":[["c7386c2b9a7d1cc0"]],"info":"Attaches a breadcrumb trace to the msg\r\nso the user can reconstruct which route has\r\nbeen traveled"},{"id":"916c17ae8b44a945","type":"catch","z":"2dc255585c9021ad","g":"ff7db384260f1b22","name":"","scope":null,"uncaught":false,"x":95,"y":320,"wires":[["768fa0dcf7e8e3b5"]],"l":false},{"id":"768fa0dcf7e8e3b5","type":"function","z":"2dc255585c9021ad","g":"ff7db384260f1b22","name":"Unhandled Exception","func":"const handle = global.get(\u0027unhandledException\u0027);\n\nhandle(this);\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":145,"y":320,"wires":[["30ea27bc433988a5"]],"l":false},{"id":"30ea27bc433988a5","type":"link out","z":"2dc255585c9021ad","g":"ff7db384260f1b22","name":"link out 7","mode":"return","links":[],"x":195,"y":320,"wires":[]},{"id":"176d29a.6f648d6","type":"server","name":"Home Assistant","addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"","connectionDelay":false,"cacheJson":false,"heartbeat":false,"heartbeatInterval":"","statusSeparator":"","enableGlobalContextStore":false},{"id":"4daf6e7bcce195d0","type":"global-config","env":[],"modules":{"node-red-contrib-home-assistant-websocket":"0.80.3","node-red-node-smooth":"0.1.2"}}]
diff --git a/node-red/all-flows-in-one-file.json b/node-red/all-flows-in-one-file.json
index 4bc6ea0..7b71f17 100644
--- a/node-red/all-flows-in-one-file.json
+++ b/node-red/all-flows-in-one-file.json
@@ -9870,7 +9870,7 @@
"z": "2ebbb6bcc8c1e75c",
"g": "31a6251d574684ca",
"name": "Load distribution",
- "func": "// Logger\nconst log = global.get(\"logger\");\nlog(this,\"**PID load distribution**\");\n\n// INPUT\nvar batteries = msg.batteries; // Battery configuration as provided by `Home Batteries IO` flow\nvar isCharging = msg.batteries_charging; // Are we in a battery charging scenario?\nvar hasChargingLimiter = flow.get(\"has_soc_charging_limiter\") || false; // Battery life improvement\nvar hasReverseDischargePriority = flow.get(\"has_reverse_priority_discharge\") || false; // Prioritize (dis)charging the same battery\nconst isDischargeDisabled = RED.util.getMessageProperty(msg,\"advanced_settings.discharge_disabled\"); // Disallow discharging?\n\n// how much power do the batteries need to compensate?\nvar unassigned_power = Math.round(Math.abs(msg.payload)); // Power in W to compensate using batteries\nlog(this,`How much power do the batteries need to compensate: ${unassigned_power} W`);\n\n// inits \nvar solution_array = []; // load distribution solutions\nvar batteries_total_assignable_power = 0; // Current max. available total (dis)charge power. Exceeding this max should trigger the anti-windup routine for the Integral terms\n\n// enums\nconst CMODE = {\n STOP: \"stop\", // Marstek batteries disconnect a relay when this is set or Power is 0W\n CHARGE: \"charge\",\n DISCHARGE: \"discharge\"\n}\n\n/**\n** Battery life improvement (slow charge near max SoC)\n* @param {number} soc\n* @param {number} max_power\n*/\nfunction chargingLimiter(soc, max_power) {\n // Must be greater than zero\n let limitedPower = Math.max(0, max_power); \n\n // Limit charge at high SoC\n if (hasChargingLimiter) {\n if (soc \u003e= 98) limitedPower = Math.min(300, limitedPower);\n else if (soc \u003e= 95) limitedPower = Math.min(500, limitedPower);\n else if (soc \u003e= 85) limitedPower = Math.min(1500, limitedPower);\n }\n return limitedPower;\n}\n\n/**\n* battery life improvement (disconnects battery only if the battery is not used for ... seconds)\n* @param {string | number} batteryID\n* @returns {{id: string|number, mode: string, power: number}} battery solution\n*/\nfunction getStopSolution(batteryID) {\n // idle time, before stop is given to inverter relay.\n let time_idle_minimum = flow.get(\"idle_time_minutes\")*60*1000||0; // Milliseconds\n\n // retrieve last registered active times for each battery based on their ids\n let lasttime_active = context.get(\"lasttime_active\");\n \n // battery exists in register \n if(lasttime_active[batteryID] !== undefined){\n // timing\n let time_last = lasttime_active[batteryID]; // Milliseconds \n let time_now = Date.now(); // Milliseconds \n let time_idle = (time_now - time_last); // Milliseconds\n \n // battery is ready for STOP\n if (time_idle \u003e= time_idle_minimum) {\n log(this,`Battery ${batteryID} [STOP]: 0 Watt and disconnect relay.`);\n return {id: batteryID, mode: CMODE.STOP, power:0}; // STOP or 0 Watt disconnects relay\n }\n \n // battery is kept IDLE, while awaiting minimum inactive time\n log(this,`Battery ${batteryID} [IDLE]: 1 Watt for ${Math.round((time_idle_minimum-time_idle)/1000)}s. Keep relay engaged.`);\n return { id: batteryID, mode: isCharging ? CMODE.CHARGE : CMODE.DISCHARGE, power: 1 }; // 1 Watt keeps battery IDLE, keeping relay engaged\n }\n\n // battery missing in register\n log(this,`Battery ${batteryID} [IDLE]: unknown last active time`);\n return getActiveSolution(batteryID, 1); // set a last active time and engage idle state\n}\n\n/**\n* Get battery solution and register a last active time.\n* @param {any} batteryID\n* @param {number} assigned_power\n* @returns {{id: string|number, mode: string, power: number}} battery solution\n*/\nfunction getActiveSolution(batteryID, assigned_power){\n // register battery as active\n let lasttime_active = context.get(\"lasttime_active\")||[]; // last registered active times for each battery based on their ids\n lasttime_active[batteryID] = Date.now();\n context.set(\"lasttime_active\", lasttime_active);\n\n // solution\n return {\n id: batteryID,\n mode: isCharging ? CMODE.CHARGE : CMODE.DISCHARGE,\n power: Math.round(assigned_power)\n };\n}\n\n// -- BATTERY DISCHARGE PRIORITY\nif (!isCharging \u0026\u0026 hasReverseDischargePriority) {\n batteries.reverse();\n}\n\n// -- LOAD BALANCER --\nbatteries.forEach((/** @type {{ id: any; soc: number; soc_min: number; soc_max: number; rs485: string; charging_max: number; discharging_max: any; }} */ battery) =\u003e {\n \n // Explain\n log(this, `Battery ${battery.id}: ${battery.soc}% (min: ${battery.soc_min}%, max: ${battery.soc_max}%) ; RS485: ${battery.rs485==\u0027enable\u0027?\u0027OK\u0027:\u0027Nok\u0027}`);\n\n // track if the battery should be skipped and why\n let skipReason = null;\n if (battery.rs485 !== \"enable\") {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because RS485 is not \u0027enable\u0027`;\n } else if (isCharging \u0026\u0026 battery.soc \u003e= battery.soc_max) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because SoC (${battery.soc}%) \u003e= Max (${battery.soc_max}%) while charging`;\n } else if (!isCharging \u0026\u0026 battery.soc \u003c= battery.soc_min) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because SoC (${battery.soc}%) \u003c= Min (${battery.soc_min}%) while discharging`;\n } else if (!isCharging \u0026\u0026 isDischargeDisabled) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped, discharging was disabled via msg property`;\n }\n\n // battery NOT AVAIABLE, skip via soft stop\n if (skipReason !== null) {\n log(this, skipReason); // communicate reason\n let solution = getStopSolution(battery.id); // get a soft stop solution for this battery\n solution_array.push(solution); // add solution, do not update last active time.\n // next battery\n return; \n }\n\n // battery is AVAILABLE, assign power\n let battery_assignable_power = isCharging ? chargingLimiter(battery.soc, battery.charging_max) : battery.discharging_max;\n let assign = Math.min(unassigned_power, battery_assignable_power);\n \n // select solution\n var solution;\n if (assign \u003c= 0 ) {\n // no power solution\n solution = getStopSolution(battery.id); // a soft stop solution\n assign = 0;\n } else {\n // assigned power solution\n solution = getActiveSolution(battery.id, Math.round(assign));\n }\n solution_array.push(solution);\n \n // Explain: charge limiting\n if(unassigned_power \u003e battery_assignable_power \u0026\u0026 battery_assignable_power \u003c battery.charging_max) {\n log(this,`Charging limited to protect battery (${battery.soc}%): ${battery.charging_max}W -\u003e ${battery_assignable_power}W`);\n }\n // Explain: solution result\n log(this, `\u003cfont color=\"#009ac7\"\u003eBattery ${solution.id}\u003c/font\u003e: assigned to ${solution.mode} with ${solution.power}W / ${isCharging ? battery.charging_max : battery.discharging_max }W max.`)\n\n // remaining power to assign\n unassigned_power -= assign;\n batteries_total_assignable_power += Number(battery_assignable_power);\n});\n\n// battery discharge priority - put solutions back in initial order\nif (!isCharging \u0026\u0026 hasReverseDischargePriority) {\n solution_array.reverse();\n}\n\n// store solutions in msg\nRED.util.setMessageProperty(msg, \"solutions\", solution_array,true);\n\n// OUTFLOW\nflow.set(\"batteries_total_assignable_power\", batteries_total_assignable_power); // this is set for the anti-windup calculation in the earlier \u0027calculate corrections\u0027 node, which will pick this up during next iteration.\n\n// OUTPUT\nreturn msg; // to Home Battery flow",
+ "func": "// Logger\nconst log = global.get(\"logger\");\nlog(this,\"**PID load distribution**\");\n\n// INPUT\nvar batteries = msg.batteries; // Battery configuration as provided by `Home Batteries IO` flow\nvar isCharging = msg.batteries_charging; // Are we in a battery charging scenario?\nvar hasChargingLimiter = flow.get(\"has_soc_charging_limiter\") || false; // Battery life improvement\nvar hasReverseDischargePriority = flow.get(\"has_reverse_priority_discharge\") || false; // Prioritize (dis)charging the same battery\nconst isDischargeDisabled = RED.util.getMessageProperty(msg,\"advanced_settings.discharge_disabled\"); // Disallow discharging?\n\n// how much power do the batteries need to compensate?\nvar unassigned_power = Math.round(Math.abs(msg.payload)); // Power in W to compensate using batteries\nlog(this,`How much power do the batteries need to compensate: ${unassigned_power} W`);\n\n// inits \nvar solution_array = []; // load distribution solutions\nvar batteries_total_assignable_power = 0; // Current max. available total (dis)charge power. Exceeding this max should trigger the anti-windup routine for the Integral terms\n\n// enums\nconst CMODE = {\n STOP: \"stop\", // Marstek batteries disconnect a relay when this is set or Power is 0W\n CHARGE: \"charge\",\n DISCHARGE: \"discharge\"\n}\n\n/**\n** Battery life improvement (slow charge near max SoC)\n* @param {number} soc\n* @param {number} max_power\n*/\nfunction chargingLimiter(soc, max_power) {\n // Must be greater than zero\n let limitedPower = Math.max(0, max_power); \n\n // Limit charge at high SoC\n if (hasChargingLimiter) {\n if (soc >= 98) limitedPower = Math.min(300, limitedPower);\n else if (soc >= 95) limitedPower = Math.min(500, limitedPower);\n else if (soc >= 85) limitedPower = Math.min(1500, limitedPower);\n }\n return limitedPower;\n}\n\n/**\n* battery life improvement (disconnects battery only if the battery is not used for ... seconds)\n* @param {string | number} batteryID\n* @returns {{id: string|number, mode: string, power: number}} battery solution\n*/\nfunction getStopSolution(batteryID) {\n // idle time, before stop is given to inverter relay.\n let time_idle_minimum = flow.get(\"idle_time_minutes\")*60*1000||0; // Milliseconds\n\n // retrieve last registered active times for each battery based on their ids\n let lasttime_active = context.get(\"lasttime_active\");\n \n // battery exists in register \n if(lasttime_active[batteryID] !== undefined){\n // timing\n let time_last = lasttime_active[batteryID]; // Milliseconds \n let time_now = Date.now(); // Milliseconds \n let time_idle = (time_now - time_last); // Milliseconds\n \n // battery is ready for STOP\n if (time_idle >= time_idle_minimum) {\n log(this,`Battery ${batteryID} [STOP]: 0 Watt and disconnect relay.`);\n return {id: batteryID, mode: CMODE.STOP, power:0}; // STOP or 0 Watt disconnects relay\n }\n \n // battery is kept IDLE, while awaiting minimum inactive time\n log(this,`Battery ${batteryID} [IDLE]: 1 Watt for ${Math.round((time_idle_minimum-time_idle)/1000)}s. Keep relay engaged.`);\n return { id: batteryID, mode: isCharging ? CMODE.CHARGE : CMODE.DISCHARGE, power: 1 }; // 1 Watt keeps battery IDLE, keeping relay engaged\n }\n\n // battery missing in register\n log(this,`Battery ${batteryID} [IDLE]: unknown last active time`);\n return getActiveSolution(batteryID, 1); // set a last active time and engage idle state\n}\n\n/**\n* Get battery solution and register a last active time.\n* @param {any} batteryID\n* @param {number} assigned_power\n* @returns {{id: string|number, mode: string, power: number}} battery solution\n*/\nfunction getActiveSolution(batteryID, assigned_power){\n // register battery as active\n let lasttime_active = context.get(\"lasttime_active\")||[]; // last registered active times for each battery based on their ids\n lasttime_active[batteryID] = Date.now();\n context.set(\"lasttime_active\", lasttime_active);\n\n // solution\n return {\n id: batteryID,\n mode: isCharging ? CMODE.CHARGE : CMODE.DISCHARGE,\n power: Math.round(assigned_power)\n };\n}\n\n// -- BATTERY DISCHARGE PRIORITY\nif (!isCharging && hasReverseDischargePriority) {\n batteries.reverse();\n}\n\n// -- LOAD BALANCER --\nbatteries.forEach((/** @type {{ id: any; soc: number; soc_min: number; soc_max: number; rs485: string; charging_max: number; discharging_max: any; }} */ battery) => {\n \n // Explain\n log(this, `Battery ${battery.id}: ${battery.soc}% (min: ${battery.soc_min}%, max: ${battery.soc_max}%) ; RS485: ${battery.rs485=='enable'?'OK':'Nok'}`);\n\n // track if the battery should be skipped and why\n let skipReason = null;\n if (battery.rs485 !== \"enable\") {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because RS485 is not 'enable'`;\n } else if (isCharging && battery.soc >= battery.soc_max) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because SoC (${battery.soc}%) >= Max (${battery.soc_max}%) while charging`;\n } else if (!isCharging && battery.soc <= (battery.soc_min - 1)) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped because SoC (${battery.soc}%) <= Min (${battery.soc_min}%) while discharging`;\n } else if (!isCharging && isDischargeDisabled) {\n skipReason = `Battery ${battery.id} [SKIP]: skipped, discharging was disabled via msg property`;\n }\n\n // battery NOT AVAIABLE, skip via soft stop\n if (skipReason !== null) {\n log(this, skipReason); // communicate reason\n let solution = getStopSolution(battery.id); // get a soft stop solution for this battery\n solution_array.push(solution); // add solution, do not update last active time.\n // next battery\n return; \n }\n\n // battery is AVAILABLE, assign power\n let battery_assignable_power = isCharging ? chargingLimiter(battery.soc, battery.charging_max) : battery.discharging_max;\n let assign = Math.min(unassigned_power, battery_assignable_power);\n \n // select solution\n var solution;\n if (assign <= 0 ) {\n // no power solution\n solution = getStopSolution(battery.id); // a soft stop solution\n assign = 0;\n } else {\n // assigned power solution\n solution = getActiveSolution(battery.id, Math.round(assign));\n }\n solution_array.push(solution);\n \n // Explain: charge limiting\n if(unassigned_power > battery_assignable_power && battery_assignable_power < battery.charging_max) {\n log(this,`Charging limited to protect battery (${battery.soc}%): ${battery.charging_max}W -> ${battery_assignable_power}W`);\n }\n // Explain: solution result\n log(this, `Battery ${solution.id}: assigned to ${solution.mode} with ${solution.power}W / ${isCharging ? battery.charging_max : battery.discharging_max }W max.`)\n\n // remaining power to assign\n unassigned_power -= assign;\n batteries_total_assignable_power += Number(battery_assignable_power);\n});\n\n// battery discharge priority - put solutions back in initial order\nif (!isCharging && hasReverseDischargePriority) {\n solution_array.reverse();\n}\n\n// store solutions in msg\nRED.util.setMessageProperty(msg, \"solutions\", solution_array,true);\n\n// OUTFLOW\nflow.set(\"batteries_total_assignable_power\", batteries_total_assignable_power); // this is set for the anti-windup calculation in the earlier 'calculate corrections' node, which will pick this up during next iteration.\n\n// OUTPUT\nreturn msg; // to Home Battery flow",
"outputs": 1,
"timeout": 0,
"noerr": 0,
@@ -12730,4 +12730,4 @@
"node-red-contrib-time-range-switch": "1.2.0"
}
}
-]
\ No newline at end of file
+]