Skip to content

Commit fb81519

Browse files
mgazzaclaude
andcommitted
fix(gecloud): no default options on stale settings after a failed refresh; test the hourly backoff cap
If the background settings refresh raised within the wait, the cycle still treated the settings as fresh, and ran the default options pass and the AC charge gate re-check on the settings left from before. They now wait for a cycle with fresh settings. Added a test that a register which keeps failing on an hourly-refreshed EMS member inverter is still read on every hourly refresh and is never left unread for more than an hour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BaJjLpuMEtQYfvmTRcFJf
1 parent 51eaca8 commit fb81519

2 files changed

Lines changed: 35 additions & 1 deletion

File tree

‎apps/predbat/gecloud.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,6 +2497,9 @@ async def run(self, seconds, first):
24972497
self.settings_refresh_detached = True
24982498
settings_fresh = False
24992499
self.log("GECloud: Settings refresh of {} is taking longer than {} seconds, carrying on with live data while it completes".format(refresh_devices, SETTINGS_REFRESH_WAIT_SECONDS))
2500+
elif self.settings_refresh_error:
2501+
# It failed, so the settings are the old ones: leave the defaults to a cycle with fresh settings
2502+
settings_fresh = False
25002503

25012504
# Run at startup, and again whenever the periodic device check finds the hardware has changed
25022505
if first or devices_changed:

‎apps/predbat/tests/test_ge_cloud.py‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8904,6 +8904,28 @@ async def test():
89048904
if sorted(call[1] for call in ge.read_calls[before:]) != [17, 120] or ge.settings["inv001"][120]["value"] != "04:00":
89058905
print("ERROR: Registers not read on the first refresh after an outage: {}".format(ge.read_calls[before:]))
89068906
failed += 1
8907+
8908+
# An EMS member inverter is only re-read once an hour: a register that keeps failing there is still read on
8909+
# every one of those refreshes, as the backoff never keeps a register unread for more than an hour
8910+
ge = _make_isolation_ge_cloud()
8911+
ge.device_list = ["inv001"]
8912+
ge.pending_writes = {"inv001": []}
8913+
with patch("gecloud.RETRIES", 1), patch("gecloud.RETRY_FACTOR", 0):
8914+
ge.settings["inv001"] = await ge.async_get_inverter_settings("inv001", previous={})
8915+
ge.read_values[120] = None
8916+
last_read = ge.now_utc_exact
8917+
longest_gap = timedelta(0)
8918+
for _ in range(8):
8919+
ge._now_utc_exact += timedelta(minutes=SETTING_READ_BACKOFF_MAX_MINUTES)
8920+
before = len(ge.read_calls)
8921+
ge.settings["inv001"] = await ge.async_get_inverter_settings("inv001", previous=ge.settings["inv001"], backoff=True)
8922+
if any(call[1] == 120 for call in ge.read_calls[before:]):
8923+
longest_gap = max(longest_gap, ge.now_utc_exact - last_read)
8924+
last_read = ge.now_utc_exact
8925+
longest_gap = max(longest_gap, ge.now_utc_exact - last_read)
8926+
if longest_gap > timedelta(minutes=SETTING_READ_BACKOFF_MAX_MINUTES) or ge.setting_read_backoff["inv001"][120]["failures"] != 8:
8927+
print("ERROR: Hourly refreshed register went {} unread: {}".format(longest_gap, ge.setting_read_backoff))
8928+
failed += 1
89078929
return failed
89088930

89098931
return run_async(test())
@@ -8962,9 +8984,15 @@ async def record_options(device, registers):
89628984
print("ERROR: Default options did not run on the fresh settings: {}".format(option_calls))
89638985
failed += 1
89648986

8965-
# A refresh that raises fails the cycle, with the traceback logged, and the next cycle is clean
8987+
# A refresh that raises fails the cycle, with the traceback logged, and the next cycle is clean.
8988+
# The default options pass is due, but must not run on the settings the failed refresh left behind
89668989
ge = _make_isolation_ge_cloud()
8990+
option_calls = []
8991+
ge.enable_default_options = record_options
89678992
await ge.run(seconds=0, first=True)
8993+
option_calls.clear()
8994+
ge._read_only = False
8995+
ge.default_options_stamp = ge.now_utc_exact - timedelta(hours=25)
89688996

89698997
async def broken(*args, **kwargs):
89708998
raise ValueError("bad register list")
@@ -8974,6 +9002,9 @@ async def broken(*args, **kwargs):
89749002
if result is not False or not any("Traceback" in message for message in ge.log_messages):
89759003
print("ERROR: A failed background refresh was not reported: result {}".format(result))
89769004
failed += 1
9005+
if option_calls:
9006+
print("ERROR: Default options ran on stale settings after the refresh failed: {}".format(option_calls))
9007+
failed += 1
89779008
if await ge.run(seconds=660, first=False) is not True:
89789009
print("ERROR: The cycle after a reported refresh failure should succeed")
89799010
failed += 1

0 commit comments

Comments
 (0)