Skip to content

Commit 9a969ce

Browse files
elupusfrenck
authored andcommitted
Ensure togrill detects disconnected devices (home-assistant#153067)
1 parent ef16327 commit 9a969ce

File tree

3 files changed

+94
-8
lines changed

3 files changed

+94
-8
lines changed

homeassistant/components/togrill/coordinator.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ async def _connect_and_update_registry(self) -> Client:
139139
raise DeviceNotFound("Unable to find device")
140140

141141
try:
142-
client = await Client.connect(device, self._notify_callback)
142+
client = await Client.connect(
143+
device,
144+
self._notify_callback,
145+
disconnected_callback=self._disconnected_callback,
146+
)
143147
except BleakError as exc:
144148
self.logger.debug("Connection failed", exc_info=True)
145149
raise DeviceNotFound("Unable to connect to device") from exc
@@ -169,9 +173,6 @@ async def async_shutdown(self) -> None:
169173
self.client = None
170174

171175
async def _get_connected_client(self) -> Client:
172-
if self.client and not self.client.is_connected:
173-
await self.client.disconnect()
174-
self.client = None
175176
if self.client:
176177
return self.client
177178

@@ -196,6 +197,12 @@ def _notify_callback(self, packet: Packet):
196197

197198
async def _async_update_data(self) -> dict[tuple[int, int | None], Packet]:
198199
"""Poll the device."""
200+
if self.client and not self.client.is_connected:
201+
await self.client.disconnect()
202+
self.client = None
203+
self._async_request_refresh_soon()
204+
raise DeviceFailed("Device was disconnected")
205+
199206
client = await self._get_connected_client()
200207
try:
201208
await client.request(PacketA0Notify)
@@ -206,12 +213,23 @@ async def _async_update_data(self) -> dict[tuple[int, int | None], Packet]:
206213
raise DeviceFailed(f"Device failed {exc}") from exc
207214
return self.data
208215

216+
@callback
217+
def _async_request_refresh_soon(self) -> None:
218+
self.config_entry.async_create_task(
219+
self.hass, self.async_request_refresh(), eager_start=False
220+
)
221+
222+
@callback
223+
def _disconnected_callback(self) -> None:
224+
"""Handle Bluetooth device being disconnected."""
225+
self._async_request_refresh_soon()
226+
209227
@callback
210228
def _async_handle_bluetooth_event(
211229
self,
212230
service_info: BluetoothServiceInfoBleak,
213231
change: BluetoothChange,
214232
) -> None:
215233
"""Handle a Bluetooth event."""
216-
if not self.client and isinstance(self.last_exception, DeviceNotFound):
217-
self.hass.async_create_task(self.async_refresh())
234+
if isinstance(self.last_exception, DeviceNotFound):
235+
self._async_request_refresh_soon()

tests/components/togrill/conftest.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,18 @@ def mock_client(enable_bluetooth: None, mock_client_class: Mock) -> Generator[Mo
5757
client_object.mocked_notify = None
5858

5959
async def _connect(
60-
address: str, callback: Callable[[Packet], None] | None = None
60+
address: str,
61+
callback: Callable[[Packet], None] | None = None,
62+
disconnected_callback: Callable[[], None] | None = None,
6163
) -> Mock:
6264
client_object.mocked_notify = callback
65+
if disconnected_callback:
66+
67+
def _disconnected_callback():
68+
client_object.is_connected = False
69+
disconnected_callback()
70+
71+
client_object.mocked_disconnected_callback = _disconnected_callback
6372
return client_object
6473

6574
async def _disconnect() -> None:

tests/components/togrill/test_sensor.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Test sensors for ToGrill integration."""
22

3-
from unittest.mock import Mock
3+
from unittest.mock import Mock, patch
44

5+
from habluetooth import BluetoothServiceInfoBleak
56
import pytest
67
from syrupy.assertion import SnapshotAssertion
78
from togrill_bluetooth.packets import PacketA0Notify, PacketA1Notify
@@ -16,6 +17,16 @@
1617
from tests.components.bluetooth import inject_bluetooth_service_info
1718

1819

20+
def patch_async_ble_device_from_address(
21+
return_value: BluetoothServiceInfoBleak | None = None,
22+
):
23+
"""Patch async_ble_device_from_address to return a mocked BluetoothServiceInfoBleak."""
24+
return patch(
25+
"homeassistant.components.bluetooth.async_ble_device_from_address",
26+
return_value=return_value,
27+
)
28+
29+
1930
@pytest.mark.parametrize(
2031
"packets",
2132
[
@@ -57,3 +68,51 @@ async def test_setup(
5768
mock_client.mocked_notify(packet)
5869

5970
await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id)
71+
72+
73+
async def test_device_disconnected(
74+
hass: HomeAssistant,
75+
mock_entry: MockConfigEntry,
76+
mock_client: Mock,
77+
) -> None:
78+
"""Test the switch set."""
79+
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
80+
81+
await setup_entry(hass, mock_entry, [Platform.SENSOR])
82+
83+
entity_id = "sensor.pro_05_battery"
84+
85+
state = hass.states.get(entity_id)
86+
assert state
87+
assert state.state == "0"
88+
89+
with patch_async_ble_device_from_address():
90+
mock_client.mocked_disconnected_callback()
91+
await hass.async_block_till_done()
92+
93+
state = hass.states.get(entity_id)
94+
assert state
95+
assert state.state == "unavailable"
96+
97+
98+
async def test_device_discovered(
99+
hass: HomeAssistant,
100+
mock_entry: MockConfigEntry,
101+
mock_client: Mock,
102+
) -> None:
103+
"""Test the switch set."""
104+
105+
await setup_entry(hass, mock_entry, [Platform.SENSOR])
106+
107+
entity_id = "sensor.pro_05_battery"
108+
109+
state = hass.states.get(entity_id)
110+
assert state
111+
assert state.state == "unavailable"
112+
113+
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
114+
await hass.async_block_till_done()
115+
116+
state = hass.states.get(entity_id)
117+
assert state
118+
assert state.state == "0"

0 commit comments

Comments
 (0)