@@ -209,6 +209,75 @@ def test_display_outage_preserves_cloud_run_and_ping(self):
209209 assert not comps .is_all_alive ()
210210 assert asyncio .run (web .html_api_ping (None )).status == 500
211211
212+ def test_offline_display_startup_does_not_wait_or_fail_cloud_health (self ):
213+ """An optional display starts before its first successful MQTT connection."""
214+ from component_base import ComponentBase
215+ from components import Components
216+ from predbat import PredBat
217+ from web import WebInterface
218+
219+ gw = self ._make_gateway ()
220+ gw ._mqtt_connected = False
221+ gw ._load_discovery = AsyncMock ()
222+ gw ._mqtt_loop = AsyncMock ()
223+ gw ._startup_wait = AsyncMock (return_value = 0 )
224+ gw .update_success_timestamp = lambda : ComponentBase .update_success_timestamp (gw )
225+ comps = Components .__new__ (Components )
226+ comps .components = {"gateway" : gw }
227+ comps .component_tasks = {"gateway" : SimpleNamespace (is_alive = lambda : True )}
228+ comps .component_errors = {}
229+ base = gw .base
230+ base .components = comps
231+ base .prefix = "predbat"
232+ base .stop_thread = False
233+ base .fatal_error = False
234+ base .had_errors = False
235+ base .get_state_wrapper .side_effect = lambda entity , attribute = None , default = None : False if attribute == "error" else datetime .datetime .now (datetime .timezone .utc ).isoformat ()
236+ base .is_running .side_effect = lambda : PredBat .is_running (base )
237+ web = WebInterface .__new__ (WebInterface )
238+ web .base = base
239+ web .log = base .log
240+
241+ async def exercise ():
242+ try :
243+ assert await gw .run (0 , True )
244+ assert gw .api_started
245+ gw ._startup_wait .assert_not_awaited ()
246+ assert comps .is_all_alive ()
247+ assert (await web .html_api_ping (None )).status == 200
248+ PredBat .record_final_run_status (base , "Charging" , "" )
249+ assert base .record_status .call_args .args [0 ] == "Charging"
250+ assert not gw ._auto_configured
251+ assert gw ._last_status is None
252+ display_call = next (call for call in reversed (base .dashboard_item .call_args_list ) if call .args [0 ] == "binary_sensor.predbat_gateway_display_online" )
253+ assert display_call .args [1 ] == "off"
254+ finally :
255+ gw ._mqtt_task .cancel ()
256+ await asyncio .gather (gw ._mqtt_task , return_exceptions = True )
257+
258+ with patch ("gateway.HAS_AIOMQTT" , True ):
259+ asyncio .run (exercise ())
260+
261+ def test_control_startup_still_waits_for_connection_and_configuration (self ):
262+ """The optional-display fast path cannot relax real inverter startup."""
263+ gw = self ._make_gateway (False )
264+ gw ._load_discovery = AsyncMock ()
265+ gw ._mqtt_loop = AsyncMock ()
266+ gw ._startup_wait = AsyncMock (return_value = 0 )
267+
268+ async def exercise ():
269+ try :
270+ assert await gw .run (0 , True )
271+ assert gw ._startup_wait .await_count == 2
272+ assert not gw .api_started
273+ assert not gw ._auto_configured
274+ finally :
275+ gw ._mqtt_task .cancel ()
276+ await asyncio .gather (gw ._mqtt_task , return_exceptions = True )
277+
278+ with patch ("gateway.HAS_AIOMQTT" , True ):
279+ asyncio .run (exercise ())
280+
212281 def test_control_transport_outage_still_fails_health (self ):
213282 """Optional display health cannot relax the active controller's checks."""
214283 gw = self ._make_gateway (False )
0 commit comments