From b8b7d2462447df543522272837e6395f3d7123f0 Mon Sep 17 00:00:00 2001 From: Rik Allen Date: Thu, 30 Jul 2026 18:12:50 +0100 Subject: [PATCH 1/4] docs(inverter-setup): fix Sigenergy Freeze Charging forced-import bug discharge_cut_off_state_of_charge was hardcoded to 100 for Freeze Charging, which is always above current SoC by construction - Sigenergy has confirmed this is a firmware bug: setting this above current SoC makes the inverter actively import from grid to reach it, even with grid_import_limitation at 0. Not a trickle in practice - observed several kW, continuing unattended until the target is reached. Root-caused and fix confirmed via live testing (springfall2008/batpred#4375), and independently corroborated in the community setup thread (#2077, Sigenergy's own acknowledgement quoted there from Feb 2026). - Pin discharge_cut_off_state_of_charge to current SoC instead of a hardcoded value in the main mode-bridge automation. - Add a second automation that re-pins continuously while Freeze Charging is active, not just on mode change - a long session with house load exceeding solar (the situation Freeze Charging exists for) would otherwise let SoC drift below a stale pin and reopen the same gap. grid_import_limitation is not a reliable backstop against this. - Add an explicit note explaining the firmware bug and why grid_import_limitation alone doesn't fix it. - Fix a pre-existing YAML indentation bug in the docs snippet (automation_sigen_ess_max_charging_limit_input_number_action was nested two spaces too deep, making it a child of the first automation's actions instead of a sibling automation - caught by parsing the snippet directly). Live-tested on real Sigenergy hardware (springfall2008/batpred#4375). --- docs/inverter-setup.md | 74 +++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/docs/inverter-setup.md b/docs/inverter-setup.md index 174d2bc0b..2743ddf0b 100644 --- a/docs/inverter-setup.md +++ b/docs/inverter-setup.md @@ -1654,6 +1654,13 @@ Add the following automations to `automations.yaml` (or configure via the UI): # Solar power to meet house load, the excess house load is met from grid import, but if there is excess Solar # power above the house load, the excess solar will be used to charge the battery # In Sigenergy, this is effectively "self consumption" mode with discharging prohibited + # + # discharge_cut_off_state_of_charge is pinned to the *current* battery SoC here, not a + # hardcoded value. Sigenergy has confirmed this is needed due to a firmware bug: if this is + # set above current SoC, the inverter actively imports from grid to reach it - even with + # grid_import_limitation at 0 below. See the note after this automation, and the + # "Freeze Charging discharge cut-off tracking" automation further down, which keeps this + # pinned continuously (not just on mode change) for long Freeze Charging sessions. - conditions: - condition: state entity_id: input_select.predbat_requested_mode @@ -1667,8 +1674,8 @@ Add the following automations to `automations.yaml` (or configure via the UI): - action: number.set_value target: entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge - data: - value: 100 + data_template: + value: "{{ states('sensor.sigen_plant_battery_state_of_charge') | float(100) }}" - action: number.set_value target: entity_id: number.sigen_plant_grid_import_limitation @@ -1681,6 +1688,10 @@ Add the following automations to `automations.yaml` (or configure via the UI): # excess solar generated, the current SoC level will be held and the excess solar will be exported. If there is # a shortfall of generated solar power to meet the house load, the battery will discharge to meet the extra load. # In Sigenergy, this is effectively "self consumption" mode with charging prohibited + # + # charge_cut_off_state_of_charge is left hardcoded at 0 here, unlike the discharge cut-off + # above - 0 is always at or below current SoC (SoC can't be negative), so it can't trigger + # the same "target above current value" firmware bug regardless of how SoC drifts. - conditions: - condition: state entity_id: input_select.predbat_requested_mode @@ -1728,20 +1739,20 @@ Add the following automations to `automations.yaml` (or configure via the UI): data: value: 100 - - id: automation_sigen_ess_max_charging_limit_input_number_action - alias: Predbat max charging limit action - description: Mapper from input_number.charge_rate to number sigen_plant_ess_max_charging_limit - triggers: - - trigger: state - entity_id: input_number.charge_rate - actions: - - action: number.set_value - target: - entity_id: number.sigen_plant_ess_max_charging_limit - data: - value: '{{ [(states(''input_number.charge_rate'') | float / 1000) | round(2), - states(''sensor.sigen_inverter_ess_rated_charging_power'') | float] | min}}' - mode: single +- id: automation_sigen_ess_max_charging_limit_input_number_action + alias: Predbat max charging limit action + description: Mapper from input_number.charge_rate to number sigen_plant_ess_max_charging_limit + triggers: + - trigger: state + entity_id: input_number.charge_rate + actions: + - action: number.set_value + target: + entity_id: number.sigen_plant_ess_max_charging_limit + data: + value: '{{ [(states(''input_number.charge_rate'') | float / 1000) | round(2), + states(''sensor.sigen_inverter_ess_rated_charging_power'') | float] | min}}' + mode: single - id: automation_sigen_ess_max_discharging_limit_input_number_action alias: Predbat max discharging limit action @@ -1757,6 +1768,35 @@ Add the following automations to `automations.yaml` (or configure via the UI): value: '{{ [(states(''input_number.discharge_rate'') | float / 1000) | round(2), states(''sensor.sigen_inverter_ess_rated_discharging_power'') | float] | min}}' mode: single + +- id: predbat_freeze_charging_discharge_cutoff_tracking + alias: Predbat Freeze Charging discharge cut-off tracking + description: > + The main automation above only re-pins discharge_cut_off_state_of_charge to current SoC when + predbat_requested_mode *changes*. If SoC drifts down during a long Freeze Charging session + (house load exceeding solar - the situation Freeze Charging exists for), the pin goes stale + and reopens the gap that causes the inverter to import from grid to close it (see the firmware + bug note below). grid_import_limitation is not a reliable backstop against this either. This + automation re-pins continuously as SoC drifts, so the gap can't reopen. + triggers: + - trigger: state + entity_id: sensor.sigen_plant_battery_state_of_charge + conditions: + - condition: state + entity_id: input_select.predbat_requested_mode + state: "Freeze Charging" + - condition: template + value_template: > + {{ trigger.to_state.state not in ['unknown', 'unavailable'] and + (states('number.sigen_plant_ess_discharge_cut_off_state_of_charge') | float(-1)) + != (trigger.to_state.state | float(-2)) }} + actions: + - action: number.set_value + target: + entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge + data: + value: "{{ trigger.to_state.state | float(100) }}" + mode: single ``` *Note:* Some Sigenergy Predbat users have reported that their Sigenergy modbus integration has created some of the entities that Predbat requires with different names @@ -1769,6 +1809,8 @@ so you may need to adapt the above automations and `apps.yaml` (or rename your e *Important:* Depending upon your electricity supply, you may need to change where **number.sigen_plant_grid_import_limitation** is set to 100 in the first integration to any lower import limit that your electricity supplier may have imposed, e.g. 18kW roughly corresponds to an 80A supply. +*Important:* Sigenergy have confirmed this is a known firmware bug on their side (not a Predbat or integration issue): even with **grid_import_limitation** set to 0kW, the inverter will still import from the grid to charge the battery if the current SoC is below **discharge_cut_off_state_of_charge**. In practice this has been observed importing several kW, not just a trickle, when the gap between SoC and the cut-off is large - continuing unattended until the target is reached. **grid_import_limitation** is therefore not a reliable backstop against this: the fix is keeping **discharge_cut_off_state_of_charge** pinned to current SoC at all times during Freeze Charging, as the automations above do. See [batpred#4375](https://github.com/springfall2008/batpred/issues/4375) and the wider [Sigenergy setup discussion](https://github.com/springfall2008/batpred/issues/2077) for the full investigation. + ## Sigenergy Cloud **Experimental** From a78c07fd39f64bc33b099df780c9e815f8f77579 Mon Sep 17 00:00:00 2001 From: Rik Allen Date: Thu, 30 Jul 2026 20:28:50 +0100 Subject: [PATCH 2/4] docs(inverter-setup): simplify Sigenergy freeze pin to one-time + dead zone Revises the previous commit in this PR after further review: - Drop the continuous re-pinning automation. It was based on a flawed premise (that SoC drifting down during a long Freeze Charging session needs tracking) - but Freeze Charging's own contract already covers this: any load shortfall is met from grid, not the battery, so SoC can only stay flat or rise while discharge is genuinely prohibited. A one-time pin at freeze-start stays valid for the whole session by construction. Continuous re-pinning actively undermines "frozen" - it relocates the anchor to wherever SoC has already drifted to instead of correcting back to it, so real drift (e.g. inverter standby losses) would never get corrected. - Add a small (1 percentage point) margin to the pinned value. The confirmed firmware mechanism only ever corrects one way (import if SoC reads below target), so ordinary sensor-reading noise around an exact-SoC pin is not self-cancelling - a low reading costs a real (if tiny) import, a high reading costs nothing. The margin absorbs normal jitter without needing to track it. - Apply the same one-time-pin treatment to Freeze Discharging's charge_cut_off_state_of_charge, mirroring the discharge side instead of leaving it hardcoded to 0. This restores what jamiekets independently proposed in #2077 back in November 2025 (both sides pinned together) - the version that shipped only kept half of it. Not itself vendor-confirmed the way the discharge side is, but 0 is always below current SoC by the same structural shape as the confirmed bug, and pinning costs nothing (still fully blocks charging either way). A more advanced variant - ratcheting the target up only in response to confirmed solar surplus over each period (using PV/load totals as the trigger, not the SoC reading itself, to avoid noise entirely) - is being tested live but is not included here; docs should stay simple and proven. Noted as a future direction in the text and on #4375. --- docs/inverter-setup.md | 69 +++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 42 deletions(-) diff --git a/docs/inverter-setup.md b/docs/inverter-setup.md index 2743ddf0b..f7f93acbc 100644 --- a/docs/inverter-setup.md +++ b/docs/inverter-setup.md @@ -1655,12 +1655,17 @@ Add the following automations to `automations.yaml` (or configure via the UI): # power above the house load, the excess solar will be used to charge the battery # In Sigenergy, this is effectively "self consumption" mode with discharging prohibited # - # discharge_cut_off_state_of_charge is pinned to the *current* battery SoC here, not a - # hardcoded value. Sigenergy has confirmed this is needed due to a firmware bug: if this is - # set above current SoC, the inverter actively imports from grid to reach it - even with - # grid_import_limitation at 0 below. See the note after this automation, and the - # "Freeze Charging discharge cut-off tracking" automation further down, which keeps this - # pinned continuously (not just on mode change) for long Freeze Charging sessions. + # discharge_cut_off_state_of_charge is pinned once here, to current SoC minus a small + # margin, not a hardcoded value and not continuously re-pinned. Sigenergy has confirmed a + # firmware bug: if this is set above current SoC, the inverter actively imports from grid + # to reach it - even with grid_import_limitation at 0 below - so the target must never sit + # above SoC. Setting it once, fixed, is what actually implements "frozen": any real deficit + # against that fixed point (house load, or even the inverter's own standby losses) gets + # corrected by grid import back up to the target, rather than the target chasing SoC + # downward and never enforcing anything. The small margin exists only to stop ordinary + # sensor-reading noise around the target from triggering a real (if tiny) grid import to + # "correct" a fluctuation that was never a real deficit - see the note after this + # automation for the full reasoning. - conditions: - condition: state entity_id: input_select.predbat_requested_mode @@ -1675,7 +1680,7 @@ Add the following automations to `automations.yaml` (or configure via the UI): target: entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge data_template: - value: "{{ states('sensor.sigen_plant_battery_state_of_charge') | float(100) }}" + value: "{{ [(states('sensor.sigen_plant_battery_state_of_charge') | float(100)) - 1, 0] | max }}" - action: number.set_value target: entity_id: number.sigen_plant_grid_import_limitation @@ -1689,9 +1694,14 @@ Add the following automations to `automations.yaml` (or configure via the UI): # a shortfall of generated solar power to meet the house load, the battery will discharge to meet the extra load. # In Sigenergy, this is effectively "self consumption" mode with charging prohibited # - # charge_cut_off_state_of_charge is left hardcoded at 0 here, unlike the discharge cut-off - # above - 0 is always at or below current SoC (SoC can't be negative), so it can't trigger - # the same "target above current value" firmware bug regardless of how SoC drifts. + # charge_cut_off_state_of_charge is pinned once here too, mirroring the Freeze Charging + # side above - to current SoC plus a small margin, not hardcoded to 0. This isn't fixing a + # vendor-confirmed bug the way the discharge side is (Sigenergy's acknowledgement was + # specifically about the discharge cut-off), but 0 is always below current SoC by exactly + # the same structural shape as the confirmed bug, so if the underlying mechanism turns out + # to be symmetric, hardcoding 0 would silently force extra discharge/export on every single + # Freeze Discharging session. Pinning to current SoC costs nothing - it still fully blocks + # charging either way - so there's no reason to take the risk. - conditions: - condition: state entity_id: input_select.predbat_requested_mode @@ -1700,8 +1710,8 @@ Add the following automations to `automations.yaml` (or configure via the UI): - action: number.set_value target: entity_id: number.sigen_plant_ess_charge_cut_off_state_of_charge - data: - value: 0 + data_template: + value: "{{ [(states('sensor.sigen_plant_battery_state_of_charge') | float(0)) + 1, 100] | min }}" - action: number.set_value target: entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge @@ -1768,35 +1778,6 @@ Add the following automations to `automations.yaml` (or configure via the UI): value: '{{ [(states(''input_number.discharge_rate'') | float / 1000) | round(2), states(''sensor.sigen_inverter_ess_rated_discharging_power'') | float] | min}}' mode: single - -- id: predbat_freeze_charging_discharge_cutoff_tracking - alias: Predbat Freeze Charging discharge cut-off tracking - description: > - The main automation above only re-pins discharge_cut_off_state_of_charge to current SoC when - predbat_requested_mode *changes*. If SoC drifts down during a long Freeze Charging session - (house load exceeding solar - the situation Freeze Charging exists for), the pin goes stale - and reopens the gap that causes the inverter to import from grid to close it (see the firmware - bug note below). grid_import_limitation is not a reliable backstop against this either. This - automation re-pins continuously as SoC drifts, so the gap can't reopen. - triggers: - - trigger: state - entity_id: sensor.sigen_plant_battery_state_of_charge - conditions: - - condition: state - entity_id: input_select.predbat_requested_mode - state: "Freeze Charging" - - condition: template - value_template: > - {{ trigger.to_state.state not in ['unknown', 'unavailable'] and - (states('number.sigen_plant_ess_discharge_cut_off_state_of_charge') | float(-1)) - != (trigger.to_state.state | float(-2)) }} - actions: - - action: number.set_value - target: - entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge - data: - value: "{{ trigger.to_state.state | float(100) }}" - mode: single ``` *Note:* Some Sigenergy Predbat users have reported that their Sigenergy modbus integration has created some of the entities that Predbat requires with different names @@ -1809,7 +1790,11 @@ so you may need to adapt the above automations and `apps.yaml` (or rename your e *Important:* Depending upon your electricity supply, you may need to change where **number.sigen_plant_grid_import_limitation** is set to 100 in the first integration to any lower import limit that your electricity supplier may have imposed, e.g. 18kW roughly corresponds to an 80A supply. -*Important:* Sigenergy have confirmed this is a known firmware bug on their side (not a Predbat or integration issue): even with **grid_import_limitation** set to 0kW, the inverter will still import from the grid to charge the battery if the current SoC is below **discharge_cut_off_state_of_charge**. In practice this has been observed importing several kW, not just a trickle, when the gap between SoC and the cut-off is large - continuing unattended until the target is reached. **grid_import_limitation** is therefore not a reliable backstop against this: the fix is keeping **discharge_cut_off_state_of_charge** pinned to current SoC at all times during Freeze Charging, as the automations above do. See [batpred#4375](https://github.com/springfall2008/batpred/issues/4375) and the wider [Sigenergy setup discussion](https://github.com/springfall2008/batpred/issues/2077) for the full investigation. +*Important:* Sigenergy have confirmed this is a known firmware bug on their side (not a Predbat or integration issue): even with **grid_import_limitation** set to 0kW, the inverter will still import from the grid to charge the battery if the current SoC is below **discharge_cut_off_state_of_charge**. In practice this has been observed importing several kW, not just a trickle, when the gap between SoC and the cut-off is large - continuing unattended until the target is reached. **grid_import_limitation** is therefore not a reliable backstop against this: the fix is keeping **discharge_cut_off_state_of_charge** pinned so it's never above current SoC, as the automation above does. + +The pin is set once, when Freeze Charging starts, rather than continuously updated as SoC changes - and this matters, not just as a simplification. "Frozen" means holding a fixed point; if the target itself kept moving to track live SoC, any downward drift (from real losses or otherwise) would just relocate the target to wherever the battery ended up, with nothing ever correcting it back. A fixed target is what makes the correction mechanism (the same import behaviour that caused the original bug) actually useful: it holds the line against any real deficit, including the inverter's own standby losses, not just customer load. The small margin (1 percentage point) below the pinned value exists purely to stop ordinary sensor-reading noise from being mistaken for a real deficit and triggering an unnecessary (if small) import to "correct" it - the underlying mechanism only ever corrects upward, so noise is not self-cancelling: a reading that dips low costs a real, if tiny, import; a reading that reads high costs nothing. Without the margin, that asymmetry nets out as pure unnecessary cost over a long session. + +See [batpred#4375](https://github.com/springfall2008/batpred/issues/4375) and the wider [Sigenergy setup discussion](https://github.com/springfall2008/batpred/issues/2077) for the full investigation, including a more advanced (currently experimental, untested) variant that ratchets the target up in response to confirmed solar surplus over each period rather than using a fixed one-off value. ## Sigenergy Cloud From 0db9335ba60d7e0ae39ab236b3a858b2d72a70b8 Mon Sep 17 00:00:00 2001 From: Rik Allen Date: Thu, 30 Jul 2026 20:46:47 +0100 Subject: [PATCH 3/4] docs(inverter-setup): revert Freeze Discharging mirror fix, note wraparound safety - Revert charge_cut_off_state_of_charge for Freeze Discharging back to a plain hardcoded 0. The mirrored-bug concern was speculative (never vendor-confirmed the way the discharge-side bug was), and 0 has been in real use across the wider community template for months without anyone reporting the kind of dramatic, easily-noticed symptom a real mirrored bug would produce - not worth the added complexity without evidence. - Add a note explaining why discharge_cut_off's margin is clamped at 0 rather than left to go negative: it's an unsigned 16-bit Modbus register on the wire, and the integration's own write encoding has no guard against a negative value - it would silently wrap around into a huge, nonsensical raw value rather than being rejected. --- docs/inverter-setup.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/inverter-setup.md b/docs/inverter-setup.md index f7f93acbc..d9a0cc24f 100644 --- a/docs/inverter-setup.md +++ b/docs/inverter-setup.md @@ -1694,14 +1694,13 @@ Add the following automations to `automations.yaml` (or configure via the UI): # a shortfall of generated solar power to meet the house load, the battery will discharge to meet the extra load. # In Sigenergy, this is effectively "self consumption" mode with charging prohibited # - # charge_cut_off_state_of_charge is pinned once here too, mirroring the Freeze Charging - # side above - to current SoC plus a small margin, not hardcoded to 0. This isn't fixing a - # vendor-confirmed bug the way the discharge side is (Sigenergy's acknowledgement was - # specifically about the discharge cut-off), but 0 is always below current SoC by exactly - # the same structural shape as the confirmed bug, so if the underlying mechanism turns out - # to be symmetric, hardcoding 0 would silently force extra discharge/export on every single - # Freeze Discharging session. Pinning to current SoC costs nothing - it still fully blocks - # charging either way - so there's no reason to take the risk. + # charge_cut_off_state_of_charge is left as a simple hardcoded 0 here, unlike the + # discharge cut-off above. A mirrored bug (SoC above charge_cut_off forcing extra + # discharge/export) was considered - 0 is always below current SoC by the same + # structural shape as the confirmed discharge-side bug - but it was never + # vendor-confirmed, and 0 has been in real use across the wider community template + # for months without anyone reporting the kind of dramatic, easily-noticed symptom + # a real mirrored bug would produce. Kept simple rather than adding unproven complexity. - conditions: - condition: state entity_id: input_select.predbat_requested_mode @@ -1710,8 +1709,8 @@ Add the following automations to `automations.yaml` (or configure via the UI): - action: number.set_value target: entity_id: number.sigen_plant_ess_charge_cut_off_state_of_charge - data_template: - value: "{{ [(states('sensor.sigen_plant_battery_state_of_charge') | float(0)) + 1, 100] | min }}" + data: + value: 0 - action: number.set_value target: entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge @@ -1794,6 +1793,8 @@ e.g. 18kW roughly corresponds to an 80A supply. The pin is set once, when Freeze Charging starts, rather than continuously updated as SoC changes - and this matters, not just as a simplification. "Frozen" means holding a fixed point; if the target itself kept moving to track live SoC, any downward drift (from real losses or otherwise) would just relocate the target to wherever the battery ended up, with nothing ever correcting it back. A fixed target is what makes the correction mechanism (the same import behaviour that caused the original bug) actually useful: it holds the line against any real deficit, including the inverter's own standby losses, not just customer load. The small margin (1 percentage point) below the pinned value exists purely to stop ordinary sensor-reading noise from being mistaken for a real deficit and triggering an unnecessary (if small) import to "correct" it - the underlying mechanism only ever corrects upward, so noise is not self-cancelling: a reading that dips low costs a real, if tiny, import; a reading that reads high costs nothing. Without the margin, that asymmetry nets out as pure unnecessary cost over a long session. +The margin is clamped at 0 (`[value, 0] | max`) rather than allowed to go negative. This isn't just tidiness: `discharge_cut_off_state_of_charge` is an unsigned 16-bit Modbus register on the wire, and the integration's own write encoding has no guard against a negative value - it would silently wrap around into a huge, nonsensical raw value rather than being rejected. At very low SoC (below the margin) an unclamped template could produce exactly that. + See [batpred#4375](https://github.com/springfall2008/batpred/issues/4375) and the wider [Sigenergy setup discussion](https://github.com/springfall2008/batpred/issues/2077) for the full investigation, including a more advanced (currently experimental, untested) variant that ratchets the target up in response to confirmed solar surplus over each period rather than using a fixed one-off value. ## Sigenergy Cloud From 19019c7e703c766043590027fab88debfe8b4227 Mon Sep 17 00:00:00 2001 From: Rik Allen Date: Thu, 30 Jul 2026 21:31:26 +0100 Subject: [PATCH 4/4] docs(inverter-setup): tighten Sigenergy freeze margin from 1pp to 0.25pp Live-observed (2026-07-30) that 1pp was more generous than needed. The pin is one-time (set once when Freeze Charging starts, not continuously re-sampled), so the margin only ever has to cover possible imprecision in that single initial reading - not ongoing noise across a session. 0.25pp is enough for that. --- docs/inverter-setup.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/inverter-setup.md b/docs/inverter-setup.md index d9a0cc24f..15ccdf530 100644 --- a/docs/inverter-setup.md +++ b/docs/inverter-setup.md @@ -1680,7 +1680,7 @@ Add the following automations to `automations.yaml` (or configure via the UI): target: entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge data_template: - value: "{{ [(states('sensor.sigen_plant_battery_state_of_charge') | float(100)) - 1, 0] | max }}" + value: "{{ [(states('sensor.sigen_plant_battery_state_of_charge') | float(100)) - 0.25, 0] | max }}" - action: number.set_value target: entity_id: number.sigen_plant_grid_import_limitation @@ -1791,7 +1791,7 @@ e.g. 18kW roughly corresponds to an 80A supply. *Important:* Sigenergy have confirmed this is a known firmware bug on their side (not a Predbat or integration issue): even with **grid_import_limitation** set to 0kW, the inverter will still import from the grid to charge the battery if the current SoC is below **discharge_cut_off_state_of_charge**. In practice this has been observed importing several kW, not just a trickle, when the gap between SoC and the cut-off is large - continuing unattended until the target is reached. **grid_import_limitation** is therefore not a reliable backstop against this: the fix is keeping **discharge_cut_off_state_of_charge** pinned so it's never above current SoC, as the automation above does. -The pin is set once, when Freeze Charging starts, rather than continuously updated as SoC changes - and this matters, not just as a simplification. "Frozen" means holding a fixed point; if the target itself kept moving to track live SoC, any downward drift (from real losses or otherwise) would just relocate the target to wherever the battery ended up, with nothing ever correcting it back. A fixed target is what makes the correction mechanism (the same import behaviour that caused the original bug) actually useful: it holds the line against any real deficit, including the inverter's own standby losses, not just customer load. The small margin (1 percentage point) below the pinned value exists purely to stop ordinary sensor-reading noise from being mistaken for a real deficit and triggering an unnecessary (if small) import to "correct" it - the underlying mechanism only ever corrects upward, so noise is not self-cancelling: a reading that dips low costs a real, if tiny, import; a reading that reads high costs nothing. Without the margin, that asymmetry nets out as pure unnecessary cost over a long session. +The pin is set once, when Freeze Charging starts, rather than continuously updated as SoC changes - and this matters, not just as a simplification. "Frozen" means holding a fixed point; if the target itself kept moving to track live SoC, any downward drift (from real losses or otherwise) would just relocate the target to wherever the battery ended up, with nothing ever correcting it back. A fixed target is what makes the correction mechanism (the same import behaviour that caused the original bug) actually useful: it holds the line against any real deficit, including the inverter's own standby losses, not just customer load. The small margin (0.25 percentage points) below the pinned value exists to cover possible imprecision in that one reading - not ongoing noise tolerance, since the pin is fixed rather than re-sampled, so only the single initial reading matters. It still matters because the underlying mechanism only ever corrects one way: a reading that's a hair low at the moment of pinning would cost a real, if tiny, import to "correct" a gap that was never really there, while a reading that's a hair high costs nothing - so even a one-off imprecise read isn't self-cancelling without some margin. The margin is clamped at 0 (`[value, 0] | max`) rather than allowed to go negative. This isn't just tidiness: `discharge_cut_off_state_of_charge` is an unsigned 16-bit Modbus register on the wire, and the integration's own write encoding has no guard against a negative value - it would silently wrap around into a huge, nonsensical raw value rather than being rejected. At very low SoC (below the margin) an unclamped template could produce exactly that.