Skip to content

Commit 012f673

Browse files
CIclaude
andcommitted
refactor(wallbox): read the saved control state once when nothing is stored
An empty answer from Storage is taken as nothing saved, as myenergi, GivEnergy EVC and Sunsynk do. A read that raises is still retried. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 15a3ad5 commit 012f673

2 files changed

Lines changed: 15 additions & 40 deletions

File tree

‎apps/predbat/tests/test_wallbox.py‎

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,43 +1649,23 @@ async def flaky_save(module, filename, data, **kwargs):
16491649
print(" ✓ A failed control state save is retried")
16501650

16511651

1652-
def test_control_state_load_returning_nothing_is_retried_then_given_up():
1653-
"""Storage returns None for a failed read as well as a missing file, so a None is retried a few times."""
1654-
storage = _storage_with_paused("101")
1655-
real_load = storage.load
1656-
answers = [None]
1652+
def test_control_state_with_nothing_saved_is_read_once():
1653+
"""An empty answer from Storage means nothing is saved: it is read once, as the other components do."""
1654+
storage = _Storage()
16571655
calls = []
16581656

1659-
async def flaky_load(module, filename):
1660-
"""Return None once, as a failed read does, then the real data."""
1661-
calls.append(filename)
1662-
if answers:
1663-
return answers.pop()
1664-
return await real_load(module, filename)
1665-
1666-
storage.load = flaky_load
1667-
component = _make_component({101: _status(status_id=178, power=0)}, wallbox_control=False)
1668-
component.base.components = _Components(storage)
1669-
run_async(component.run(0, True))
1670-
assert component.transport.count("resume") == 0
1671-
run_async(component.run(120, False))
1672-
assert component.transport.count("resume") == 1, "The saved charger was released once the read worked"
1673-
1674-
empty = _Storage()
1675-
empty_calls = []
1676-
16771657
async def counting_load(module, filename):
16781658
"""Always empty, counting the reads."""
1679-
empty_calls.append(filename)
1659+
calls.append(filename)
16801660
return None
16811661

1682-
empty.load = counting_load
1662+
storage.load = counting_load
16831663
component = _make_component()
1684-
component.base.components = _Components(empty)
1685-
for cycle in range(12):
1664+
component.base.components = _Components(storage)
1665+
for cycle in range(6):
16861666
run_async(component.run(cycle * 60, cycle == 0))
1687-
assert 1 < len(empty_calls) <= 5, "A file that is simply absent is not read forever: {}".format(len(empty_calls))
1688-
print(" ✓ An empty control state read is retried a few times, then left")
1667+
assert len(calls) == 1, "Read once, not on every cycle: {}".format(len(calls))
1668+
print(" ✓ With nothing saved, the control state is read once")
16891669

16901670

16911671
def test_schedule_restoration_is_retried_after_a_failure():
@@ -1798,7 +1778,7 @@ def test_wallbox(my_predbat=None):
17981778
test_account_wide_errors_still_stop_the_control_loop()
17991779
test_pause_is_persisted_before_it_is_sent()
18001780
test_failed_control_state_save_is_retried()
1801-
test_control_state_load_returning_nothing_is_retried_then_given_up()
1781+
test_control_state_with_nothing_saved_is_read_once()
18021782
test_schedule_restoration_is_retried_after_a_failure()
18031783
print("=" * 70)
18041784
return False

‎apps/predbat/wallbox.py‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@
7474

7575
WALLBOX_STORAGE_MODULE = "wallbox"
7676
WALLBOX_CONTROL_STATE = "control_state"
77-
# How many cycles an empty answer from Storage is retried before the saved state is taken to be absent
78-
CONTROL_STATE_LOAD_ATTEMPTS = 5
7977

8078
# Status id to (text, car connected). The text is the Home Assistant wallbox integration's
8179
# wording, so states match what existing users already see. A None status id means the
@@ -493,7 +491,6 @@ def initialize(self, username, password, automatic=True, wallbox_control=False,
493491
self.lock_warned = set()
494492
self.ocpp_warned = set()
495493
self.control_state_loaded = False
496-
self.control_state_load_attempts = 0
497494
self.control_state_dirty = False
498495
# Chargers Predbat has resumed on release but not yet handed back to their own schedule
499496
self.schedule_pending = set()
@@ -840,23 +837,21 @@ async def load_control_state(self):
840837
"""Restore the control switch and the chargers Predbat still owes a release.
841838
842839
The saved lists are what let a restart with control turned off still release a
843-
charger an earlier session left paused. Storage answers None for a failed read as
844-
well as for a file that is not there, so a None is tried again on the next few
845-
cycles rather than taken as "nothing saved"; after that the file is taken to be
846-
absent. What is read is merged into what this session already knows, never
840+
charger an earlier session left paused. A read that raises is tried again on the
841+
next cycle, as Sunsynk's cache restore does; an empty answer is taken as nothing
842+
saved. What is read is merged into what this session already knows, never
847843
replacing it. Fails soft with no Storage component.
848844
"""
849-
if self.control_state_loaded or self.storage is None or self.control_state_load_attempts >= CONTROL_STATE_LOAD_ATTEMPTS:
845+
if self.control_state_loaded or self.storage is None:
850846
return
851-
self.control_state_load_attempts += 1
852847
try:
853848
saved = await self.storage.load(WALLBOX_STORAGE_MODULE, WALLBOX_CONTROL_STATE)
854849
except Exception as exc:
855850
self.log("Warn: wallbox: could not read the saved charge control state: {}".format(exc))
856851
return
852+
self.control_state_loaded = True
857853
if not isinstance(saved, dict):
858854
return
859-
self.control_state_loaded = True
860855
if "control_enabled" in saved:
861856
self.control_enabled = bool(saved["control_enabled"])
862857
for key, target in (("paused", self.paused_by_predbat), ("schedule", self.schedule_pending)):

0 commit comments

Comments
 (0)