Skip to content

Commit fc4bfab

Browse files
zweckjCopilot
authored andcommitted
Lamarzocco fix websocket reconnect issue (home-assistant#156786)
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
1 parent 769a12f commit fc4bfab

File tree

4 files changed

+60
-22
lines changed

4 files changed

+60
-22
lines changed

homeassistant/components/lamarzocco/coordinator.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from abc import abstractmethod
6+
from asyncio import Task
67
from dataclasses import dataclass
78
from datetime import timedelta
89
import logging
@@ -44,7 +45,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
4445

4546
_default_update_interval = SCAN_INTERVAL
4647
config_entry: LaMarzoccoConfigEntry
47-
websocket_terminated = True
48+
_websocket_task: Task | None = None
4849

4950
def __init__(
5051
self,
@@ -64,6 +65,13 @@ def __init__(
6465
self.device = device
6566
self.cloud_client = cloud_client
6667

68+
@property
69+
def websocket_terminated(self) -> bool:
70+
"""Return True if the websocket task is terminated or not running."""
71+
if self._websocket_task is None:
72+
return True
73+
return self._websocket_task.done()
74+
6775
async def _async_update_data(self) -> None:
6876
"""Do the data update."""
6977
try:
@@ -95,13 +103,14 @@ async def _internal_async_update_data(self) -> None:
95103
# ensure token stays valid; does nothing if token is still valid
96104
await self.cloud_client.async_get_access_token()
97105

98-
if self.device.websocket.connected:
106+
# Only skip websocket reconnection if it's currently connected and the task is still running
107+
if self.device.websocket.connected and not self.websocket_terminated:
99108
return
100109

101110
await self.device.get_dashboard()
102111
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
103112

104-
self.config_entry.async_create_background_task(
113+
self._websocket_task = self.config_entry.async_create_background_task(
105114
hass=self.hass,
106115
target=self.connect_websocket(),
107116
name="lm_websocket_task",
@@ -120,7 +129,6 @@ async def connect_websocket(self) -> None:
120129

121130
_LOGGER.debug("Init WebSocket in background task")
122131

123-
self.websocket_terminated = False
124132
self.async_update_listeners()
125133

126134
await self.device.connect_dashboard_websocket(
@@ -129,7 +137,6 @@ async def connect_websocket(self) -> None:
129137
disconnect_callback=self.async_update_listeners,
130138
)
131139

132-
self.websocket_terminated = True
133140
self.async_update_listeners()
134141

135142

tests/components/lamarzocco/conftest.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Lamarzocco session fixtures."""
22

33
from collections.abc import Generator
4-
from unittest.mock import MagicMock, patch
4+
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
55

66
from bleak.backends.device import BLEDevice
77
from pylamarzocco.const import ModelName
@@ -132,6 +132,10 @@ def mock_lamarzocco(device_fixture: ModelName) -> Generator[MagicMock]:
132132
"schedule": machine_mock.schedule.to_dict(),
133133
"settings": machine_mock.settings.to_dict(),
134134
}
135+
machine_mock.connect_dashboard_websocket = AsyncMock()
136+
machine_mock.websocket = MagicMock()
137+
machine_mock.websocket.connected = True
138+
machine_mock.websocket.disconnect = AsyncMock()
135139
yield machine_mock
136140

137141

@@ -149,10 +153,11 @@ def mock_ble_device() -> BLEDevice:
149153

150154

151155
@pytest.fixture
152-
def mock_websocket_terminated() -> Generator[bool]:
156+
def mock_websocket_terminated() -> Generator[PropertyMock]:
153157
"""Mock websocket terminated."""
154158
with patch(
155159
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoUpdateCoordinator.websocket_terminated",
156-
new=False,
160+
new_callable=PropertyMock,
157161
) as mock_websocket_terminated:
162+
mock_websocket_terminated.return_value = False
158163
yield mock_websocket_terminated

tests/components/lamarzocco/test_binary_sensor.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""Tests for La Marzocco binary sensors."""
22

3-
from collections.abc import Generator
43
from datetime import timedelta
5-
from unittest.mock import MagicMock, patch
4+
from unittest.mock import MagicMock, PropertyMock, patch
65

76
from freezegun.api import FrozenDateTimeFactory
87
from pylamarzocco.exceptions import RequestNotSuccessful
@@ -36,24 +35,15 @@ async def test_binary_sensors(
3635
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
3736

3837

39-
@pytest.fixture(autouse=True)
40-
def mock_websocket_terminated() -> Generator[bool]:
41-
"""Mock websocket terminated."""
42-
with patch(
43-
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoUpdateCoordinator.websocket_terminated",
44-
new=False,
45-
) as mock_websocket_terminated:
46-
yield mock_websocket_terminated
47-
48-
4938
async def test_brew_active_unavailable(
5039
hass: HomeAssistant,
5140
mock_lamarzocco: MagicMock,
5241
mock_config_entry: MockConfigEntry,
42+
mock_websocket_terminated: PropertyMock,
5343
) -> None:
5444
"""Test the La Marzocco brew active becomes unavailable."""
5545

56-
mock_lamarzocco.websocket.connected = False
46+
mock_websocket_terminated.return_value = True
5747
await async_init_integration(hass, mock_config_entry)
5848
state = hass.states.get(
5949
f"binary_sensor.{mock_lamarzocco.serial_number}_brewing_active"

tests/components/lamarzocco/test_init.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Test initialization of lamarzocco."""
22

3+
from datetime import timedelta
34
from unittest.mock import AsyncMock, MagicMock, patch
45

6+
from freezegun.api import FrozenDateTimeFactory
57
from pylamarzocco.const import FirmwareType, ModelName
68
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
79
from pylamarzocco.models import WebSocketDetails
@@ -30,7 +32,7 @@
3032
get_bluetooth_service_info,
3133
)
3234

33-
from tests.common import MockConfigEntry
35+
from tests.common import MockConfigEntry, async_fire_time_changed
3436

3537

3638
async def test_load_unload_config_entry(
@@ -310,3 +312,37 @@ async def test_device(
310312
device = device_registry.async_get(entry.device_id)
311313
assert device
312314
assert device == snapshot
315+
316+
317+
async def test_websocket_reconnects_after_termination(
318+
hass: HomeAssistant,
319+
mock_config_entry: MockConfigEntry,
320+
mock_lamarzocco: MagicMock,
321+
freezer: FrozenDateTimeFactory,
322+
) -> None:
323+
"""Test the websocket reconnects after background task terminates."""
324+
# Setup: websocket connected initially
325+
mock_websocket = MagicMock()
326+
mock_websocket.closed = False
327+
mock_lamarzocco.websocket = WebSocketDetails(mock_websocket, None)
328+
329+
await async_init_integration(hass, mock_config_entry)
330+
331+
# Verify initial websocket connection was attempted
332+
assert mock_lamarzocco.connect_dashboard_websocket.call_count == 1
333+
334+
# Simulate websocket disconnection (e.g., after internet outage)
335+
mock_websocket.closed = True
336+
337+
# Simulate the background task terminating by patching websocket_terminated
338+
with patch(
339+
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoConfigUpdateCoordinator.websocket_terminated",
340+
new=True,
341+
):
342+
# Trigger the coordinator's update (which runs every 60 seconds)
343+
freezer.tick(timedelta(seconds=61))
344+
async_fire_time_changed(hass)
345+
await hass.async_block_till_done()
346+
347+
# Verify websocket reconnection was attempted
348+
assert mock_lamarzocco.connect_dashboard_websocket.call_count == 2

0 commit comments

Comments
 (0)