diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 689fa65..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/docs/05-setup-dynamic.md b/docs/05-setup-dynamic.md
index 2cb1181..4cd7772 100644
--- a/docs/05-setup-dynamic.md
+++ b/docs/05-setup-dynamic.md
@@ -75,6 +75,8 @@ The Dynamic controls live in the `Timed/Dynamic` tab of the Home Battery Control
| **Regular period strategy** | Action during neutral (unmarked) intervals. Choices: `Charge PV`, `Self-consumption`, `Full stop`. |
| **High period strategy** | Action during `high`-marked intervals. Choices: `Self-consumption`, `Sell`, `Charge PV`, `Full stop`. |
+Changes to any of these period strategies take effect immediately. If the current price interval is low, neutral, or high, Dynamic re-evaluates that interval and switches to the newly selected sub-strategy without needing to stop and restart Dynamic.
+
> **Tip:** the underlying `Charge` / `Sell` strategies can be configured with solar forecast, peak-shaving reserves, etc. Dynamic respects those settings.
The price-data table (today and tomorrow with marks) and an ApexChart for review are visible in the same tab when debug/insights mode is enabled.
diff --git a/docs/06-advanced-features.md b/docs/06-advanced-features.md
index a0eb0c4..b38b2b8 100644
--- a/docs/06-advanced-features.md
+++ b/docs/06-advanced-features.md
@@ -12,9 +12,13 @@ nav_order: 6
- Configure by entering the `entity_id` of an `input_boolean` or `on/off` template sensor
- The trigger sensor should indicate when your EV or heavy appliance is actively charging
- Choose the trigger strategy:
+ - **Disabled:** retain the configured sensor but do not override the active battery strategy
- **Full stop:** stop battery operation until the trigger sensor returns to off
- **Standby / peak shave:** keep normal battery operation idle, but allow peak shaving when grid limits are exceeded
- - Manual **Full stop** in the main strategy selector always takes precedence, even when the EV trigger strategy is set to `Standby / peak shave`
+ - **Charge PV:** charge only from surplus solar while the trigger is active
+ - **Charge:** charge using the configured Charge strategy while the trigger is active
+ - Requires sufficient grid capacity and/or EV charger load balancing. Otherwise simultaneous EV and battery charging can overload the grid connection.
+ - Manual **Full stop** in the main strategy selector always takes precedence over the EV trigger strategy
- Useful for preventing home battery discharge during high-power EV charging sessions
- Configurable through the Advanced Settings dashboard
@@ -126,6 +130,7 @@ Peak Shaving helps reduce import and export peaks on your grid connection by int
- Set your **export limit** on the "Settings" tab (maximum power you want to feed back to the grid)
- Note: Most capacity tariff contracts only require import limiting
- Peak Shaving integrates seamlessly with Charge, Self-consumption, Sell, Dynamic, and Timed strategies
+- During **Charge**, the battery's measured charging power is excluded from import-peak detection. Regulated Charge uses the configured import limit as its PID target, filling only the remaining grid headroom; peak shaving still activates when the non-battery load itself exceeds the limit.
- Full Stop strategy takes precedence and will not be overridden by peak shaving
**Limitations to understand:**
diff --git a/home assistant/.DS_Store b/home assistant/.DS_Store
deleted file mode 100644
index 8c1d6d6..0000000
Binary files a/home assistant/.DS_Store and /dev/null differ
diff --git a/home assistant/dashboard.yaml b/home assistant/dashboard.yaml
index 2cdd311..f1d6a2a 100644
--- a/home assistant/dashboard.yaml
+++ b/home assistant/dashboard.yaml
@@ -1870,12 +1870,22 @@ views:
secondary_info: none
- entity: input_select.house_battery_strategy_ev_stop_trigger_strategy
name: Trigger strategy
+ - type: markdown
+ visibility:
+ - condition: state
+ entity: input_select.house_battery_strategy_ev_stop_trigger_strategy
+ state: Charge
+ content: >-
+ The Charge trigger strategy
+ requires sufficient grid capacity and/or EV charger load balancing.
+ Otherwise EV and battery charging can overload the grid connection.
- type: markdown
content: >-
Enter the `entity_id` of an `input_boolean` or
`on/off template_sensor` in the field above. Choose whether an
- active trigger should fully stop the batteries or keep them in
- standby for peak shaving.
+ active trigger should fully stop the batteries, keep them in
+ standby for peak shaving, or charge. Select `Disabled` to retain
+ the configured sensor without changing the active battery strategy.
text_only: true
- type: tile
grid_options:
diff --git a/home assistant/packages/house_battery_control.yaml b/home assistant/packages/house_battery_control.yaml
index 479faec..080a232 100644
--- a/home assistant/packages/house_battery_control.yaml
+++ b/home assistant/packages/house_battery_control.yaml
@@ -671,8 +671,11 @@ input_select:
house_battery_strategy_ev_stop_trigger_strategy:
name: EV Stop Trigger Strategy
options:
+ - Disabled # Keep the configured trigger sensor without overriding the active battery strategy
- Full stop # Stop (dis)charging and disengage grid relay
- Standby / peak shave # Standby, peak shave only. Requires power limits to be set!
+ - Charge PV # Charge only when surplus PV is available
+ - Charge # Charge from the grid using the configured Charge strategy
icon: mdi:power-plug-off
# Available sub-strategies for timed / smart
diff --git a/node-red/01 start-flow.json b/node-red/01 start-flow.json
index d810a1d..9ee51ee 100644
--- a/node-red/01 start-flow.json
+++ b/node-red/01 start-flow.json
@@ -556,7 +556,7 @@
"z": "d606df74d35deb5a",
"g": "2f55254a75de7eac",
"name": "Strategy selection",
- "func": "// Logger\nconst logger = global.get(\"logger\");\n\n// INFLOW\nlet strategy = flow.get(\"house_battery_strategy\");\n\n// Configure msg.strategy object\nRED.util.setMessageProperty(msg,\"strategy.selected\",strategy,true); // by user\nRED.util.setMessageProperty(msg,\"strategy.trace\",[],true); // init trace: which flows have been executed\n\n// stop on error\nif(msg.batteries === undefined) {\n logger(this, \"Battery configuration undefined\", \"error\");\n return null;\n}\nif(strategy === undefined) {\n logger(this, \"Battery strategy undefined\", \"error\");\n return null;\n}\n\n// Default | select target flow based on the input_select `house_battery_strategy`\nmsg.target = `${strategy}`;\n\n// EV stop trigger overrules selected strategies, except a manual Full stop.\nconst stopTrigger = RED.util.getMessageProperty(msg,\"full_stop_trigger\");\nconst isStopTriggerActive = stopTrigger == \"on\" || stopTrigger === true || stopTrigger === 1;\nif(isStopTriggerActive && strategy !== \"Full stop\") {\n const stopTriggerStrategy = RED.util.getMessageProperty(msg,\"ev_stop_trigger_strategy\") || \"Full stop\";\n msg.target = stopTriggerStrategy;\n // Explain\n logger(this, `**${stopTriggerStrategy}** strategy selected, due to EV stop trigger: '${stopTrigger}'`);\n}\n\n// add execution marker\nRED.util.setMessageProperty(msg,\"strategy.execution_start\",Date.now()); // when do we start executing the selected strategy\n\n// OUTPUT\nnode.status({fill:\"grey\",shape:\"dot\",text:msg.target});\nreturn msg;",
+ "func": "// Logger\nconst logger = global.get(\"logger\");\n\n// INFLOW\nlet strategy = flow.get(\"house_battery_strategy\");\n\n// Configure msg.strategy object\nRED.util.setMessageProperty(msg,\"strategy.selected\",strategy,true); // by user\nRED.util.setMessageProperty(msg,\"strategy.trace\",[],true); // init trace: which flows have been executed\n\n// stop on error\nif(msg.batteries === undefined) {\n logger(this, \"Battery configuration undefined\", \"error\");\n return null;\n}\nif(strategy === undefined) {\n logger(this, \"Battery strategy undefined\", \"error\");\n return null;\n}\n\n// Default | select target flow based on the input_select `house_battery_strategy`\nmsg.target = `${strategy}`;\n\n// EV stop trigger overrules selected strategies, except a manual Full stop.\nconst stopTrigger = RED.util.getMessageProperty(msg,\"full_stop_trigger\");\nconst isStopTriggerActive = stopTrigger == \"on\" || stopTrigger === true || stopTrigger === 1;\nconst stopTriggerStrategy = RED.util.getMessageProperty(msg,\"ev_stop_trigger_strategy\") || \"Full stop\";\nif(isStopTriggerActive && strategy !== \"Full stop\" && stopTriggerStrategy !== \"Disabled\") {\n msg.target = stopTriggerStrategy;\n // Explain\n logger(this, `**${stopTriggerStrategy}** strategy selected, due to EV stop trigger: '${stopTrigger}'`);\n}\n\n// add execution marker\nRED.util.setMessageProperty(msg,\"strategy.execution_start\",Date.now()); // when do we start executing the selected strategy\n\n// OUTPUT\nnode.status({fill:\"grey\",shape:\"dot\",text:msg.target});\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
@@ -3620,4 +3620,4 @@
"node-red-contrib-home-assistant-websocket": "0.80.3"
}
}
-]
\ No newline at end of file
+]
diff --git a/node-red/02 strategy-dynamic-2.json b/node-red/02 strategy-dynamic-2.json
index 2ed2161..9e9ce67 100644
--- a/node-red/02 strategy-dynamic-2.json
+++ b/node-red/02 strategy-dynamic-2.json
@@ -932,6 +932,9 @@
"input_number.house_battery_strategy_dynamic_expensive_hrs",
"input_select.house_battery_strategy_dynamic_data_source",
"input_number.house_battery_strategy_dynamic_threshold_delta",
+ "input_select.house_battery_strategy_dynamic_default",
+ "input_select.house_battery_strategy_dynamic_cheapest",
+ "input_select.house_battery_strategy_dynamic_expensive",
"input_select.house_battery_strategy"
],
"substring": [],
@@ -2070,4 +2073,4 @@
"node-red-contrib-home-assistant-websocket": "0.80.3"
}
}
-]
\ No newline at end of file
+]
diff --git a/node-red/02 strategy-partials.json b/node-red/02 strategy-partials.json
index 5ac136c..cf512e4 100644
--- a/node-red/02 strategy-partials.json
+++ b/node-red/02 strategy-partials.json
@@ -341,7 +341,7 @@
"type": "function",
"z": "1ae53b821b8673a2",
"name": "Mode selection",
- "func": "/* Mode selection \n * \n * Peak shaving can be triggered by:\n * - Grid power exceeds the import/export limit set by the user\n * \n * Peak shaving will persist while:\n * - Grid power exceeds the import/export limit set by the user\n * - Batteries are applied to keep system below the limit set by the user\n * \n * The first is checked in this node.\n * The latter is checked in the last function node of the Peak Shaving flow.\n * Both set the last_power_limit_violation flow variable to Date.now() to persist peak shaving.\n * \n * PEAK_SHAVING_TIMEOUT_SEC is advised to be kept above atleast 3-5 seconds, to account for rate limiters and other power saving options.\n*/\n\n// Timestamp\nconst now = Date.now();\n\n// Logger\nconst log = global.get(\"logger\");\n\n// Configuration & State recovery\nconst PEAK_SHAVING_TIMEOUT_SEC = 10;\nlet isPeakShaving = flow.get(\"is_peak_shaving\") ?? false;\nlet lastPowerLimitViolation = flow.get(\"last_power_limit_violation\") ?? now;\nlet isPowerLimitViolation = false;\nlet peakDirection = flow.get(\"peak_shave_direction\") || null;\nlet powerLimit = Number(flow.get(\"peak_shave_power_limit\")) || 0;\n\n// Input data | grid\nconst P1_power = parseInt(RED.util.getMessageProperty(msg, \"grid_power\") || 0);\n// Input data | Charge PV\nconst hasImportLimit = RED.util.getMessageProperty(msg, \"grid_power_has_limit_import\") || false;\nconst importLimit = parseInt(RED.util.getMessageProperty(msg, \"grid_power_limit_import\") || 0);\n// Input data | Sell PV\nconst hasExportLimit = RED.util.getMessageProperty(msg, \"grid_power_has_limit_export\") || false;\nconst exportLimit = parseInt(RED.util.getMessageProperty(msg, \"grid_power_limit_export\") || 0);\n\n// ACTIVATE shaving\n// Latch the direction and limit at the moment of the real violation. During the\n// release timeout the battery response can flip P1 import/export, but that\n// should not turn an import shave into an export shave or vice versa.\nif (hasImportLimit && (P1_power > importLimit)) {\n isPeakShaving = true;\n isPowerLimitViolation = true;\n peakDirection = \"import\";\n powerLimit = importLimit;\n}\nif (hasExportLimit && (P1_power < exportLimit)) {\n isPeakShaving = true;\n isPowerLimitViolation = true;\n peakDirection = \"export\";\n powerLimit = exportLimit;\n}\n\n// UPDATE timer or continue\nif (isPowerLimitViolation) lastPowerLimitViolation = now;\n\n// CALCULATE release logic\n// Seconds since the last time the power limit was exceeded\nlet secondsSinceLastViolation = Math.floor((now - lastPowerLimitViolation) / 1000);\n\n// RELEASE shaving logic\n// Only release if we are currently shaving AND the timeout has passed\nif (isPeakShaving && (secondsSinceLastViolation >= PEAK_SHAVING_TIMEOUT_SEC)) {\n isPeakShaving = false;\n peakDirection = null;\n powerLimit = 0;\n log(this,\"Peak Shaving released, resume normal operation\");\n}\n\n// UI & OUTPUT Logic\nif (isPeakShaving) {\n // Timeout progress\n const secondsRemaining = PEAK_SHAVING_TIMEOUT_SEC - secondsSinceLastViolation;\n // Set latched grid power limit\n msg.payload = powerLimit;\n // UI\n node.status({\n fill: \"yellow\",\n shape: \"dot\",\n text: isPowerLimitViolation ? \"Peak Shaving\" : \"Peak Shaving: release in \" + secondsRemaining + \"s\"\n });\n} else {\n // UI\n node.status({\n fill: \"green\",\n shape: \"dot\",\n text: \"Grid OK - \" + msg.target\n });\n}\n\n// Persist state for next run\nflow.set(\"is_peak_shaving\", isPeakShaving); \nflow.set(\"peak_shave_direction\", peakDirection);\nflow.set(\"peak_shave_power_limit\", powerLimit);\nif (isPowerLimitViolation) flow.set(\"last_power_limit_violation\", now); // Power limit exceeded. Update the lastPowerLimitViolation timestamp to current time\n\n// Finalize msg\nRED.util.setMessageProperty(msg, \"strategy.is_peak_shaving\", isPeakShaving, true);\nRED.util.setMessageProperty(msg, \"strategy.peak_direction\", peakDirection, true);\n\n// OUTPUT\nreturn msg;",
+ "func": "/* Mode selection \n * \n * Peak shaving can be triggered by:\n * - Grid power exceeds the import/export limit set by the user\n * \n * Peak shaving will persist while:\n * - Grid power exceeds the import/export limit set by the user\n * - Batteries are applied to keep system below the limit set by the user\n * \n * The first is checked in this node.\n * The latter is checked in the last function node of the Peak Shaving flow.\n * Both set the last_power_limit_violation flow variable to Date.now() to persist peak shaving.\n * \n * PEAK_SHAVING_TIMEOUT_SEC is advised to be kept above atleast 3-5 seconds, to account for rate limiters and other power saving options.\n*/\n\n// Timestamp\nconst now = Date.now();\n\n// Logger\nconst log = global.get(\"logger\");\n\n// Configuration & State recovery\nconst PEAK_SHAVING_TIMEOUT_SEC = 10;\nlet isPeakShaving = flow.get(\"is_peak_shaving\") ?? false;\nlet lastPowerLimitViolation = flow.get(\"last_power_limit_violation\") ?? now;\nlet isPowerLimitViolation = false;\nlet peakDirection = flow.get(\"peak_shave_direction\") || null;\nlet powerLimit = Number(flow.get(\"peak_shave_power_limit\")) || 0;\n\n// Input data | grid\n// During Charge, exclude the current battery charging power from import-peak detection.\n// Regulated Charge already uses the import limit as its PID target; counting its own\n// load here would make it trigger peak shaving unnecessarily.\nconst measuredGridPower = parseInt(RED.util.getMessageProperty(msg, \"grid_power\") || 0);\nconst batteryChargePower = msg.target === \"Charge\"\n ? Math.max(0, Number(RED.util.getMessageProperty(msg, \"batteries_total_power\")) || 0)\n : 0;\nconst P1_power = measuredGridPower - batteryChargePower;\n// Input data | Charge PV\nconst hasImportLimit = RED.util.getMessageProperty(msg, \"grid_power_has_limit_import\") || false;\nconst importLimit = parseInt(RED.util.getMessageProperty(msg, \"grid_power_limit_import\") || 0);\n// Input data | Sell PV\nconst hasExportLimit = RED.util.getMessageProperty(msg, \"grid_power_has_limit_export\") || false;\nconst exportLimit = parseInt(RED.util.getMessageProperty(msg, \"grid_power_limit_export\") || 0);\n\n// ACTIVATE shaving\n// Latch the direction and limit at the moment of the real violation. During the\n// release timeout the battery response can flip P1 import/export, but that\n// should not turn an import shave into an export shave or vice versa.\nif (hasImportLimit && (P1_power > importLimit)) {\n isPeakShaving = true;\n isPowerLimitViolation = true;\n peakDirection = \"import\";\n powerLimit = importLimit;\n}\nif (hasExportLimit && (P1_power < exportLimit)) {\n isPeakShaving = true;\n isPowerLimitViolation = true;\n peakDirection = \"export\";\n powerLimit = exportLimit;\n}\n\n// UPDATE timer or continue\nif (isPowerLimitViolation) lastPowerLimitViolation = now;\n\n// CALCULATE release logic\n// Seconds since the last time the power limit was exceeded\nlet secondsSinceLastViolation = Math.floor((now - lastPowerLimitViolation) / 1000);\n\n// RELEASE shaving logic\n// Only release if we are currently shaving AND the timeout has passed\nif (isPeakShaving && (secondsSinceLastViolation >= PEAK_SHAVING_TIMEOUT_SEC)) {\n isPeakShaving = false;\n peakDirection = null;\n powerLimit = 0;\n log(this,\"Peak Shaving released, resume normal operation\");\n}\n\n// UI & OUTPUT Logic\nif (isPeakShaving) {\n // Timeout progress\n const secondsRemaining = PEAK_SHAVING_TIMEOUT_SEC - secondsSinceLastViolation;\n // Set latched grid power limit\n msg.payload = powerLimit;\n // UI\n node.status({\n fill: \"yellow\",\n shape: \"dot\",\n text: isPowerLimitViolation ? \"Peak Shaving\" : \"Peak Shaving: release in \" + secondsRemaining + \"s\"\n });\n} else {\n // UI\n node.status({\n fill: \"green\",\n shape: \"dot\",\n text: \"Grid OK - \" + msg.target\n });\n}\n\n// Persist state for next run\nflow.set(\"is_peak_shaving\", isPeakShaving); \nflow.set(\"peak_shave_direction\", peakDirection);\nflow.set(\"peak_shave_power_limit\", powerLimit);\nif (isPowerLimitViolation) flow.set(\"last_power_limit_violation\", now); // Power limit exceeded. Update the lastPowerLimitViolation timestamp to current time\n\n// Finalize msg\nRED.util.setMessageProperty(msg, \"strategy.is_peak_shaving\", isPeakShaving, true);\nRED.util.setMessageProperty(msg, \"strategy.peak_direction\", peakDirection, true);\n\n// OUTPUT\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
diff --git a/node-red/all-flows-in-one-file.json b/node-red/all-flows-in-one-file.json
index 1f90040..5f729b7 100644
--- a/node-red/all-flows-in-one-file.json
+++ b/node-red/all-flows-in-one-file.json
@@ -2916,7 +2916,7 @@
"z": "b3306a83536ac1ba",
"g": "6cdf174b958506ac",
"name": "Strategy selection",
- "func": "// Logger\nconst logger = global.get(\"logger\");\n\n// INFLOW\nlet strategy = flow.get(\"house_battery_strategy\");\n\n// Configure msg.strategy object\nRED.util.setMessageProperty(msg,\"strategy.selected\",strategy,true); // by user\nRED.util.setMessageProperty(msg,\"strategy.trace\",[],true); // init trace: which flows have been executed\n\n// stop on error\nif(msg.batteries === undefined) {\n logger(this, \"Battery configuration undefined\", \"error\");\n return null;\n}\nif(strategy === undefined) {\n logger(this, \"Battery strategy undefined\", \"error\");\n return null;\n}\n\n// Default | select target flow based on the input_select `house_battery_strategy`\nmsg.target = `${strategy}`;\n\n// EV stop trigger overrules selected strategies, except a manual Full stop.\nconst stopTrigger = RED.util.getMessageProperty(msg,\"full_stop_trigger\");\nconst isStopTriggerActive = stopTrigger == \"on\" || stopTrigger === true || stopTrigger === 1;\nif(isStopTriggerActive && strategy !== \"Full stop\") {\n const stopTriggerStrategy = RED.util.getMessageProperty(msg,\"ev_stop_trigger_strategy\") || \"Full stop\";\n msg.target = stopTriggerStrategy;\n // Explain\n logger(this, `**${stopTriggerStrategy}** strategy selected, due to EV stop trigger: '${stopTrigger}'`);\n}\n\n// add execution marker\nRED.util.setMessageProperty(msg,\"strategy.execution_start\",Date.now()); // when do we start executing the selected strategy\n\n// OUTPUT\nnode.status({fill:\"grey\",shape:\"dot\",text:msg.target});\nreturn msg;",
+ "func": "// Logger\nconst logger = global.get(\"logger\");\n\n// INFLOW\nlet strategy = flow.get(\"house_battery_strategy\");\n\n// Configure msg.strategy object\nRED.util.setMessageProperty(msg,\"strategy.selected\",strategy,true); // by user\nRED.util.setMessageProperty(msg,\"strategy.trace\",[],true); // init trace: which flows have been executed\n\n// stop on error\nif(msg.batteries === undefined) {\n logger(this, \"Battery configuration undefined\", \"error\");\n return null;\n}\nif(strategy === undefined) {\n logger(this, \"Battery strategy undefined\", \"error\");\n return null;\n}\n\n// Default | select target flow based on the input_select `house_battery_strategy`\nmsg.target = `${strategy}`;\n\n// EV stop trigger overrules selected strategies, except a manual Full stop.\nconst stopTrigger = RED.util.getMessageProperty(msg,\"full_stop_trigger\");\nconst isStopTriggerActive = stopTrigger == \"on\" || stopTrigger === true || stopTrigger === 1;\nconst stopTriggerStrategy = RED.util.getMessageProperty(msg,\"ev_stop_trigger_strategy\") || \"Full stop\";\nif(isStopTriggerActive && strategy !== \"Full stop\" && stopTriggerStrategy !== \"Disabled\") {\n msg.target = stopTriggerStrategy;\n // Explain\n logger(this, `**${stopTriggerStrategy}** strategy selected, due to EV stop trigger: '${stopTrigger}'`);\n}\n\n// add execution marker\nRED.util.setMessageProperty(msg,\"strategy.execution_start\",Date.now()); // when do we start executing the selected strategy\n\n// OUTPUT\nnode.status({fill:\"grey\",shape:\"dot\",text:msg.target});\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
@@ -6142,7 +6142,7 @@
"type": "function",
"z": "9c5e9dbe3c6522d6",
"name": "Mode selection",
- "func": "/* Mode selection \n * \n * Peak shaving can be triggered by:\n * - Grid power exceeds the import/export limit set by the user\n * \n * Peak shaving will persist while:\n * - Grid power exceeds the import/export limit set by the user\n * - Batteries are applied to keep system below the limit set by the user\n * \n * The first is checked in this node.\n * The latter is checked in the last function node of the Peak Shaving flow.\n * Both set the last_power_limit_violation flow variable to Date.now() to persist peak shaving.\n * \n * PEAK_SHAVING_TIMEOUT_SEC is advised to be kept above atleast 3-5 seconds, to account for rate limiters and other power saving options.\n*/\n\n// Timestamp\nconst now = Date.now();\n\n// Logger\nconst log = global.get(\"logger\");\n\n// Configuration & State recovery\nconst PEAK_SHAVING_TIMEOUT_SEC = 10;\nlet isPeakShaving = flow.get(\"is_peak_shaving\") ?? false;\nlet lastPowerLimitViolation = flow.get(\"last_power_limit_violation\") ?? now;\nlet isPowerLimitViolation = false;\nlet peakDirection = flow.get(\"peak_shave_direction\") || null;\nlet powerLimit = Number(flow.get(\"peak_shave_power_limit\")) || 0;\n\n// Input data | grid\nconst P1_power = parseInt(RED.util.getMessageProperty(msg, \"grid_power\") || 0);\n// Input data | Charge PV\nconst hasImportLimit = RED.util.getMessageProperty(msg, \"grid_power_has_limit_import\") || false;\nconst importLimit = parseInt(RED.util.getMessageProperty(msg, \"grid_power_limit_import\") || 0);\n// Input data | Sell PV\nconst hasExportLimit = RED.util.getMessageProperty(msg, \"grid_power_has_limit_export\") || false;\nconst exportLimit = parseInt(RED.util.getMessageProperty(msg, \"grid_power_limit_export\") || 0);\n\n// ACTIVATE shaving\n// Latch the direction and limit at the moment of the real violation. During the\n// release timeout the battery response can flip P1 import/export, but that\n// should not turn an import shave into an export shave or vice versa.\nif (hasImportLimit && (P1_power > importLimit)) {\n isPeakShaving = true;\n isPowerLimitViolation = true;\n peakDirection = \"import\";\n powerLimit = importLimit;\n}\nif (hasExportLimit && (P1_power < exportLimit)) {\n isPeakShaving = true;\n isPowerLimitViolation = true;\n peakDirection = \"export\";\n powerLimit = exportLimit;\n}\n\n// UPDATE timer or continue\nif (isPowerLimitViolation) lastPowerLimitViolation = now;\n\n// CALCULATE release logic\n// Seconds since the last time the power limit was exceeded\nlet secondsSinceLastViolation = Math.floor((now - lastPowerLimitViolation) / 1000);\n\n// RELEASE shaving logic\n// Only release if we are currently shaving AND the timeout has passed\nif (isPeakShaving && (secondsSinceLastViolation >= PEAK_SHAVING_TIMEOUT_SEC)) {\n isPeakShaving = false;\n peakDirection = null;\n powerLimit = 0;\n log(this,\"Peak Shaving released, resume normal operation\");\n}\n\n// UI & OUTPUT Logic\nif (isPeakShaving) {\n // Timeout progress\n const secondsRemaining = PEAK_SHAVING_TIMEOUT_SEC - secondsSinceLastViolation;\n // Set latched grid power limit\n msg.payload = powerLimit;\n // UI\n node.status({\n fill: \"yellow\",\n shape: \"dot\",\n text: isPowerLimitViolation ? \"Peak Shaving\" : \"Peak Shaving: release in \" + secondsRemaining + \"s\"\n });\n} else {\n // UI\n node.status({\n fill: \"green\",\n shape: \"dot\",\n text: \"Grid OK - \" + msg.target\n });\n}\n\n// Persist state for next run\nflow.set(\"is_peak_shaving\", isPeakShaving); \nflow.set(\"peak_shave_direction\", peakDirection);\nflow.set(\"peak_shave_power_limit\", powerLimit);\nif (isPowerLimitViolation) flow.set(\"last_power_limit_violation\", now); // Power limit exceeded. Update the lastPowerLimitViolation timestamp to current time\n\n// Finalize msg\nRED.util.setMessageProperty(msg, \"strategy.is_peak_shaving\", isPeakShaving, true);\nRED.util.setMessageProperty(msg, \"strategy.peak_direction\", peakDirection, true);\n\n// OUTPUT\nreturn msg;",
+ "func": "/* Mode selection \n * \n * Peak shaving can be triggered by:\n * - Grid power exceeds the import/export limit set by the user\n * \n * Peak shaving will persist while:\n * - Grid power exceeds the import/export limit set by the user\n * - Batteries are applied to keep system below the limit set by the user\n * \n * The first is checked in this node.\n * The latter is checked in the last function node of the Peak Shaving flow.\n * Both set the last_power_limit_violation flow variable to Date.now() to persist peak shaving.\n * \n * PEAK_SHAVING_TIMEOUT_SEC is advised to be kept above atleast 3-5 seconds, to account for rate limiters and other power saving options.\n*/\n\n// Timestamp\nconst now = Date.now();\n\n// Logger\nconst log = global.get(\"logger\");\n\n// Configuration & State recovery\nconst PEAK_SHAVING_TIMEOUT_SEC = 10;\nlet isPeakShaving = flow.get(\"is_peak_shaving\") ?? false;\nlet lastPowerLimitViolation = flow.get(\"last_power_limit_violation\") ?? now;\nlet isPowerLimitViolation = false;\nlet peakDirection = flow.get(\"peak_shave_direction\") || null;\nlet powerLimit = Number(flow.get(\"peak_shave_power_limit\")) || 0;\n\n// Input data | grid\n// During Charge, exclude the current battery charging power from import-peak detection.\n// Regulated Charge already uses the import limit as its PID target; counting its own\n// load here would make it trigger peak shaving unnecessarily.\nconst measuredGridPower = parseInt(RED.util.getMessageProperty(msg, \"grid_power\") || 0);\nconst batteryChargePower = msg.target === \"Charge\"\n ? Math.max(0, Number(RED.util.getMessageProperty(msg, \"batteries_total_power\")) || 0)\n : 0;\nconst P1_power = measuredGridPower - batteryChargePower;\n// Input data | Charge PV\nconst hasImportLimit = RED.util.getMessageProperty(msg, \"grid_power_has_limit_import\") || false;\nconst importLimit = parseInt(RED.util.getMessageProperty(msg, \"grid_power_limit_import\") || 0);\n// Input data | Sell PV\nconst hasExportLimit = RED.util.getMessageProperty(msg, \"grid_power_has_limit_export\") || false;\nconst exportLimit = parseInt(RED.util.getMessageProperty(msg, \"grid_power_limit_export\") || 0);\n\n// ACTIVATE shaving\n// Latch the direction and limit at the moment of the real violation. During the\n// release timeout the battery response can flip P1 import/export, but that\n// should not turn an import shave into an export shave or vice versa.\nif (hasImportLimit && (P1_power > importLimit)) {\n isPeakShaving = true;\n isPowerLimitViolation = true;\n peakDirection = \"import\";\n powerLimit = importLimit;\n}\nif (hasExportLimit && (P1_power < exportLimit)) {\n isPeakShaving = true;\n isPowerLimitViolation = true;\n peakDirection = \"export\";\n powerLimit = exportLimit;\n}\n\n// UPDATE timer or continue\nif (isPowerLimitViolation) lastPowerLimitViolation = now;\n\n// CALCULATE release logic\n// Seconds since the last time the power limit was exceeded\nlet secondsSinceLastViolation = Math.floor((now - lastPowerLimitViolation) / 1000);\n\n// RELEASE shaving logic\n// Only release if we are currently shaving AND the timeout has passed\nif (isPeakShaving && (secondsSinceLastViolation >= PEAK_SHAVING_TIMEOUT_SEC)) {\n isPeakShaving = false;\n peakDirection = null;\n powerLimit = 0;\n log(this,\"Peak Shaving released, resume normal operation\");\n}\n\n// UI & OUTPUT Logic\nif (isPeakShaving) {\n // Timeout progress\n const secondsRemaining = PEAK_SHAVING_TIMEOUT_SEC - secondsSinceLastViolation;\n // Set latched grid power limit\n msg.payload = powerLimit;\n // UI\n node.status({\n fill: \"yellow\",\n shape: \"dot\",\n text: isPowerLimitViolation ? \"Peak Shaving\" : \"Peak Shaving: release in \" + secondsRemaining + \"s\"\n });\n} else {\n // UI\n node.status({\n fill: \"green\",\n shape: \"dot\",\n text: \"Grid OK - \" + msg.target\n });\n}\n\n// Persist state for next run\nflow.set(\"is_peak_shaving\", isPeakShaving); \nflow.set(\"peak_shave_direction\", peakDirection);\nflow.set(\"peak_shave_power_limit\", powerLimit);\nif (isPowerLimitViolation) flow.set(\"last_power_limit_violation\", now); // Power limit exceeded. Update the lastPowerLimitViolation timestamp to current time\n\n// Finalize msg\nRED.util.setMessageProperty(msg, \"strategy.is_peak_shaving\", isPeakShaving, true);\nRED.util.setMessageProperty(msg, \"strategy.peak_direction\", peakDirection, true);\n\n// OUTPUT\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
@@ -14975,4 +14975,4 @@
]
]
}
-]
\ No newline at end of file
+]