Skip to content

Commit b172549

Browse files
authored
Add guards to prevent shutdown from failing (#542)
* Add guards to prevent shutdown from failing * Add unit tests
1 parent a7bae4b commit b172549

File tree

5 files changed

+202
-6
lines changed

5 files changed

+202
-6
lines changed

tests/test_device.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,3 +1212,70 @@ async def test_symfonisk_events(
12121212
)
12131213
)
12141214
]
1215+
1216+
1217+
async def test_device_on_remove_callback_failure(
1218+
zha_gateway: Gateway,
1219+
caplog: pytest.LogCaptureFixture,
1220+
) -> None:
1221+
"""Test that device.on_remove continues when callback fails."""
1222+
zigpy_dev = await zigpy_device_from_json(
1223+
zha_gateway.application_controller,
1224+
"tests/data/devices/philips-sml001.json",
1225+
)
1226+
zha_device = await join_zigpy_device(zha_gateway, zigpy_dev)
1227+
1228+
failing_callback = mock.Mock(side_effect=Exception("Callback failed"))
1229+
zha_device._on_remove_callbacks.append(failing_callback)
1230+
1231+
await zha_device.on_remove()
1232+
1233+
assert failing_callback.call_count == 1
1234+
assert "Failed to execute on_remove callback" in caplog.text
1235+
assert "Callback failed" in caplog.text
1236+
1237+
1238+
async def test_device_on_remove_platform_entity_failure(
1239+
zha_gateway: Gateway,
1240+
caplog: pytest.LogCaptureFixture,
1241+
) -> None:
1242+
"""Test that device.on_remove continues when platform entity removal fails."""
1243+
zigpy_dev = await zigpy_device_from_json(
1244+
zha_gateway.application_controller,
1245+
"tests/data/devices/philips-sml001.json",
1246+
)
1247+
zha_device = await join_zigpy_device(zha_gateway, zigpy_dev)
1248+
1249+
switch_entity = get_entity(zha_device, platform=Platform.SWITCH)
1250+
with patch.object(
1251+
switch_entity, "on_remove", side_effect=Exception("Entity removal failed")
1252+
):
1253+
await zha_device.on_remove()
1254+
1255+
assert "Failed to remove platform entity" in caplog.text
1256+
assert "Entity removal failed" in caplog.text
1257+
1258+
1259+
async def test_device_on_remove_pending_entity_failure(
1260+
zha_gateway: Gateway,
1261+
caplog: pytest.LogCaptureFixture,
1262+
) -> None:
1263+
"""Test that device.on_remove continues when pending entity removal fails."""
1264+
zigpy_dev = await zigpy_device_from_json(
1265+
zha_gateway.application_controller,
1266+
"tests/data/devices/philips-sml001.json",
1267+
)
1268+
zha_device = await join_zigpy_device(zha_gateway, zigpy_dev)
1269+
1270+
update_entity = get_entity(zha_device, platform=Platform.UPDATE)
1271+
zha_device._pending_entities.append(update_entity)
1272+
1273+
with patch.object(
1274+
update_entity,
1275+
"on_remove",
1276+
side_effect=Exception("Pending entity removal failed"),
1277+
):
1278+
await zha_device.on_remove()
1279+
1280+
assert "Failed to remove pending entity" in caplog.text
1281+
assert "Pending entity removal failed" in caplog.text

tests/test_gateway.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
get_entity,
2222
get_group_entity,
2323
join_zigpy_device,
24+
zigpy_device_from_json,
2425
)
2526
from zha.application import Platform
2627
from zha.application.const import (
@@ -882,3 +883,86 @@ async def test_gateway_energy_scan(zha_gateway: Gateway) -> None:
882883
assert mock_scan.mock_calls == [
883884
call(channels=channels, duration_exp=4, count=1)
884885
]
886+
887+
888+
async def test_gateway_shutdown_device_on_remove_failure(
889+
zha_gateway: Gateway,
890+
caplog: pytest.LogCaptureFixture,
891+
) -> None:
892+
"""Test that gateway shutdown continues when device.on_remove fails."""
893+
zigpy_dev = await zigpy_device_from_json(
894+
zha_gateway.application_controller,
895+
"tests/data/devices/centralite-3320-l.json",
896+
)
897+
zha_device = await join_zigpy_device(zha_gateway, zigpy_dev)
898+
899+
with patch.object(
900+
zha_device, "on_remove", side_effect=Exception("Device removal failed")
901+
):
902+
await zha_gateway.shutdown()
903+
await zha_gateway.async_block_till_done()
904+
905+
assert "Failed to remove device" in caplog.text
906+
assert "Device removal failed" in caplog.text
907+
908+
909+
async def test_gateway_shutdown_group_on_remove_failure(
910+
zha_gateway: Gateway,
911+
caplog: pytest.LogCaptureFixture,
912+
) -> None:
913+
"""Test that gateway shutdown continues when group.on_remove fails."""
914+
zigpy_dev_1 = await zigpy_device_from_json(
915+
zha_gateway.application_controller,
916+
"tests/data/devices/ikea-of-sweden-tradfri-bulb-gu10-ws-400lm.json",
917+
)
918+
zha_device_1 = await join_zigpy_device(zha_gateway, zigpy_dev_1)
919+
920+
zha_group = await zha_gateway.async_create_zigpy_group(
921+
"Test Group",
922+
[GroupMemberReference(ieee=zha_device_1.ieee, endpoint_id=1)],
923+
)
924+
await zha_gateway.async_block_till_done()
925+
926+
with patch.object(
927+
zha_group, "on_remove", side_effect=Exception("Group removal failed")
928+
):
929+
await zha_gateway.shutdown()
930+
await zha_gateway.async_block_till_done()
931+
932+
assert "Failed to remove group" in caplog.text
933+
assert "Group removal failed" in caplog.text
934+
935+
936+
async def test_group_on_remove_entity_failure(
937+
zha_gateway: Gateway,
938+
caplog: pytest.LogCaptureFixture,
939+
) -> None:
940+
"""Test that group.on_remove continues when group entity removal fails."""
941+
zigpy_dev_1 = await zigpy_device_from_json(
942+
zha_gateway.application_controller,
943+
"tests/data/devices/ikea-of-sweden-tradfri-bulb-gu10-ws-400lm.json",
944+
)
945+
zigpy_dev_2 = await zigpy_device_from_json(
946+
zha_gateway.application_controller,
947+
"tests/data/devices/ikea-of-sweden-tradfri-bulb-e26-opal-1000lm.json",
948+
)
949+
zha_device_1 = await join_zigpy_device(zha_gateway, zigpy_dev_1)
950+
zha_device_2 = await join_zigpy_device(zha_gateway, zigpy_dev_2)
951+
952+
members = [
953+
GroupMemberReference(ieee=zha_device_1.ieee, endpoint_id=1),
954+
GroupMemberReference(ieee=zha_device_2.ieee, endpoint_id=1),
955+
]
956+
957+
zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members)
958+
await zha_gateway.async_block_till_done()
959+
960+
group_entity = get_group_entity(zha_group, platform=Platform.LIGHT)
961+
962+
with patch.object(
963+
group_entity, "on_remove", side_effect=Exception("Group entity removal failed")
964+
):
965+
await zha_group.on_remove()
966+
967+
assert "Failed to remove group entity" in caplog.text
968+
assert "Group entity removal failed" in caplog.text

zha/application/gateway.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -741,10 +741,24 @@ async def shutdown(self) -> None:
741741
self._device_availability_checker.stop()
742742

743743
for device in self._devices.values():
744-
await device.on_remove()
744+
try:
745+
await device.on_remove()
746+
except Exception:
747+
_LOGGER.warning(
748+
"Failed to remove device %s during shutdown",
749+
device,
750+
exc_info=True,
751+
)
745752

746753
for group in self._groups.values():
747-
await group.on_remove()
754+
try:
755+
await group.on_remove()
756+
except Exception:
757+
_LOGGER.warning(
758+
"Failed to remove group %s during shutdown",
759+
group,
760+
exc_info=True,
761+
)
748762

749763
_LOGGER.debug("Shutting down ZHA ControllerApplication")
750764
if self.application_controller is not None:

zha/zigbee/device.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,13 +1024,37 @@ def entity_update_listener(event: EntityStateChangedEvent) -> None:
10241024
async def on_remove(self) -> None:
10251025
"""Cancel tasks this device owns."""
10261026
for callback in self._on_remove_callbacks:
1027-
callback()
1027+
try:
1028+
callback()
1029+
except Exception:
1030+
_LOGGER.warning(
1031+
"Failed to execute on_remove callback %s for device %s",
1032+
callback,
1033+
self,
1034+
exc_info=True,
1035+
)
10281036

10291037
for platform_entity in self._platform_entities.values():
1030-
await platform_entity.on_remove()
1038+
try:
1039+
await platform_entity.on_remove()
1040+
except Exception:
1041+
_LOGGER.warning(
1042+
"Failed to remove platform entity %s for device %s",
1043+
platform_entity,
1044+
self,
1045+
exc_info=True,
1046+
)
10311047

10321048
for entity in self._pending_entities:
1033-
await entity.on_remove()
1049+
try:
1050+
await entity.on_remove()
1051+
except Exception:
1052+
_LOGGER.warning(
1053+
"Failed to remove pending entity %s for device %s",
1054+
entity,
1055+
self,
1056+
exc_info=True,
1057+
)
10341058

10351059
def async_get_clusters(self) -> dict[int, dict[str, dict[int, Cluster]]]:
10361060
"""Get all clusters for this device."""

zha/zigbee/group.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,11 @@ def log(self, level: int, msg: str, *args: Any, **kwargs) -> None:
354354
async def on_remove(self) -> None:
355355
"""Cancel tasks this group owns."""
356356
for group_entity in tuple(self._group_entities.values()):
357-
await group_entity.on_remove()
357+
try:
358+
await group_entity.on_remove()
359+
except Exception:
360+
_LOGGER.warning(
361+
"Failed to remove group entity %s",
362+
group_entity,
363+
exc_info=True,
364+
)

0 commit comments

Comments
 (0)