Skip to content

Commit 4bb1534

Browse files
Add exception handling in _do_callbacks (#554)
A single bad callback would crash _do_callbacks and prevent remaining callbacks from executing. Now wraps each callback in try/except to ensure all registered callbacks run even if one raises. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b176a8d commit 4bb1534

File tree

2 files changed

+47
-2
lines changed

2 files changed

+47
-2
lines changed

custom_components/dreo/pydreo/pydreobasedevice.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,16 @@ def add_attr_callback(self, cb):
190190
self._attr_cbs.append(cb)
191191

192192
def _do_callbacks(self):
193-
"""Run all registered callback"""
193+
"""Run all registered callbacks."""
194194
cbs = []
195195
with self._lock:
196196
for cb in self._attr_cbs:
197197
cbs.append(cb)
198198
for cb in cbs:
199-
cb()
199+
try:
200+
cb()
201+
except Exception as ex: # pylint: disable=broad-except
202+
_LOGGER.error("_do_callbacks: Callback %s raised: %s", cb, ex)
200203

201204
@property
202205
def device_definition(self) -> DreoDeviceDetails:
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Tests for PyDreoBaseDevice callback safety."""
2+
import logging
3+
from unittest.mock import MagicMock, patch
4+
from .imports import * # pylint: disable=W0401,W0614
5+
from .testbase import TestBase
6+
7+
logger = logging.getLogger(__name__)
8+
logger.setLevel(logging.DEBUG)
9+
10+
11+
class TestPyDreoBaseDevice(TestBase):
12+
"""Test PyDreoBaseDevice class."""
13+
14+
def test_do_callbacks_continues_after_exception(self):
15+
"""Test that _do_callbacks continues executing remaining callbacks when one raises."""
16+
self.get_devices_file_name = "get_devices_HSH009S.json"
17+
self.pydreo_manager.load_devices()
18+
device = self.pydreo_manager.devices[0]
19+
20+
cb1 = MagicMock()
21+
cb2 = MagicMock(side_effect=RuntimeError("callback error"))
22+
cb3 = MagicMock()
23+
24+
device.add_attr_callback(cb1)
25+
device.add_attr_callback(cb2)
26+
device.add_attr_callback(cb3)
27+
28+
device._do_callbacks()
29+
30+
# All three should have been called, even though cb2 raised
31+
cb1.assert_called_once()
32+
cb2.assert_called_once()
33+
cb3.assert_called_once()
34+
35+
def test_do_callbacks_no_callbacks(self):
36+
"""Test _do_callbacks with no registered callbacks."""
37+
self.get_devices_file_name = "get_devices_HSH009S.json"
38+
self.pydreo_manager.load_devices()
39+
device = self.pydreo_manager.devices[0]
40+
41+
# Should not raise
42+
device._do_callbacks()

0 commit comments

Comments
 (0)