Skip to content

Commit e201937

Browse files
Merge pull request #4110 from springfall2008/fix/gateway_startup2
fix startup condition problem with plan
2 parents 417d89c + 94ed386 commit e201937

3 files changed

Lines changed: 60 additions & 2 deletions

File tree

apps/predbat/component_base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,12 @@ async def start(self):
208208
if not self.api_started:
209209
self.api_started = True
210210
self.log(f"{self.__class__.__name__}: Started")
211-
first = False # Clear first flag once started
211+
# Clear first flag once started. This must happen even when a
212+
# component sets api_started itself from a background task (e.g.
213+
# the gateway's MQTT loop): otherwise first stays True forever and
214+
# start() keeps re-running the first=True startup path on backoff,
215+
# never reaching the steady-state housekeeping run().
216+
first = False
212217
else:
213218
self.count_errors += 1
214219
self.non_fatal_error_occurred()

apps/predbat/predbat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import pytz
3636
import asyncio
3737

38-
THIS_VERSION = "v8.41.2"
38+
THIS_VERSION = "v8.41.3"
3939

4040
from download import predbat_update_move, predbat_update_download, check_install, DEFAULT_PREDBAT_REPOSITORY
4141
from const import MINUTE_WATT

apps/predbat/tests/test_component_base.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,58 @@ async def run_test():
301301
return asyncio.run(run_test())
302302

303303

304+
def test_component_base_first_cleared_when_run_presets_api_started(my_predbat):
305+
"""Regression: a component that sets api_started itself must still leave the startup path.
306+
307+
The gateway's MQTT background loop sets self.api_started = True before run(first=True)
308+
returns. If start() only clears the `first` flag inside `if not self.api_started`, the
309+
flag stays True forever and start() keeps re-running the first=True startup path on
310+
backoff, never reaching the steady-state (first=False) housekeeping that publishes the
311+
plan. This verifies start() transitions to first=False regardless of who set api_started.
312+
"""
313+
print("\n*** Test: ComponentBase clears first when run() pre-sets api_started ***")
314+
315+
class PresetComponent(ComponentBase):
316+
def __init__(self, base):
317+
self.first_flags = []
318+
super().__init__(base)
319+
320+
def initialize(self, **kwargs):
321+
pass
322+
323+
async def run(self, seconds, first):
324+
self.first_flags.append(first)
325+
# Mimic a background task marking the component started before run() returns.
326+
self.api_started = True
327+
return True
328+
329+
async def run_test():
330+
with patch("asyncio.sleep", side_effect=fast_sleep):
331+
base = MockBase()
332+
component = PresetComponent(base)
333+
334+
task = asyncio.create_task(component.start())
335+
336+
# Wait long enough (sped up 100x by fast_sleep → ~2s real) for the component
337+
# loop to advance past simulated seconds=60 so a steady-state run can occur.
338+
await asyncio.sleep(200)
339+
340+
assert component.api_started, "Component should be started"
341+
342+
await component.stop()
343+
await task
344+
345+
assert component.first_flags, "run() should have been called"
346+
assert component.first_flags[0] is True, "First run should be first=True"
347+
assert any(f is False for f in component.first_flags), "Component must reach steady-state housekeeping (first=False); got first flags: {}".format(component.first_flags)
348+
assert component.first_flags.count(True) == 1, "Startup run() should happen exactly once, got {}".format(component.first_flags)
349+
350+
print(f"PASS: first cleared despite self-set api_started (flags={component.first_flags})")
351+
return False # False = test passed
352+
353+
return asyncio.run(run_test())
354+
355+
304356
def test_component_base_all(my_predbat):
305357
"""Run all component_base tests"""
306358
tests = [
@@ -310,6 +362,7 @@ def test_component_base_all(my_predbat):
310362
("normal_operation", test_component_base_normal_operation_after_start, "Component runs every 60s after start"),
311363
("exception_handling", test_component_base_exception_handling, "Component handles exceptions with backoff"),
312364
("run_timeout", test_component_base_run_timeout, "Hung run() triggers timeout, stack trace, and error count"),
365+
("first_cleared_preset", test_component_base_first_cleared_when_run_presets_api_started, "first flag clears even when run() pre-sets api_started"),
313366
]
314367

315368
failed = []

0 commit comments

Comments
 (0)