Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions docs/05-setup-dynamic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion docs/06-advanced-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:**
Expand Down
Binary file removed home assistant/.DS_Store
Binary file not shown.
14 changes: 12 additions & 2 deletions home assistant/dashboard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >-
<ha-alert alert-type="warning">The <b>Charge</b> trigger strategy
requires sufficient grid capacity and/or EV charger load balancing.
Otherwise EV and battery charging can overload the grid connection.</ha-alert>
- type: markdown
content: >-
<font color=grey>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.</font>
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.</font>
text_only: true
- type: tile
grid_options:
Expand Down
3 changes: 3 additions & 0 deletions home assistant/packages/house_battery_control.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions node-red/01 start-flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -3620,4 +3620,4 @@
"node-red-contrib-home-assistant-websocket": "0.80.3"
}
}
]
]
5 changes: 4 additions & 1 deletion node-red/02 strategy-dynamic-2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down Expand Up @@ -2070,4 +2073,4 @@
"node-red-contrib-home-assistant-websocket": "0.80.3"
}
}
]
]
2 changes: 1 addition & 1 deletion node-red/02 strategy-partials.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading