diff --git a/src/helper/device_handler_base.py b/src/helper/device_handler_base.py index 97a3018..7c674ed 100644 --- a/src/helper/device_handler_base.py +++ b/src/helper/device_handler_base.py @@ -1,5 +1,6 @@ """Shared base class for all Hive device handlers.""" +import asyncio import logging from typing import Any @@ -39,16 +40,24 @@ async def _execute_state_change(self, device: Device, **state_kwargs) -> bool: return False await self.session.hive_refresh_tokens() data = self.session.data.products[device.hive_id] - resp = await self.session.api.set_state( - data["type"], device.hive_id, **state_kwargs - ) - if resp["original"] == HTTP_OK: + try: + resp = await self.session.api.set_state( + data["type"], device.hive_id, **state_kwargs + ) + except asyncio.TimeoutError: + # Honour the documented False-on-failure contract so one slow + # device doesn't abort a multi-entity service call. + _LOGGER.warning( + "_execute_state_change - set_state timed out for %s", device.ha_name + ) + return False + if resp.get("original") == HTTP_OK: await self.session.get_devices(device.hive_id) return True _LOGGER.error( "_execute_state_change - set_state failed for %s: HTTP %s", device.ha_name, - resp["original"], + resp.get("original"), ) return False diff --git a/tests/unit/test_base_handler.py b/tests/unit/test_base_handler.py index 5720035..fcb6de9 100644 --- a/tests/unit/test_base_handler.py +++ b/tests/unit/test_base_handler.py @@ -2,9 +2,9 @@ # pylint: disable=protected-access,too-few-public-methods,attribute-defined-outside-init +import asyncio from unittest.mock import AsyncMock, MagicMock -import pytest from apyhiveapi.helper.device_handler_base import BaseDeviceHandler from apyhiveapi.helper.hivedataclasses import Device from apyhiveapi.helper.map import Map @@ -167,11 +167,20 @@ async def test_non_200_returns_false(self): assert result is False session.get_devices.assert_not_called() - async def test_malformed_set_state_response_raises_key_error(self): - """KeyError propagates when set_state response is missing 'original' key.""" + async def test_malformed_set_state_response_returns_false(self): + """Returns False when set_state response is missing 'original' key.""" session = _make_session({"prod-1": {"type": "heating"}}) session.api.set_state = AsyncMock(return_value={"parsed": {}}) h = _make_handler(session) d = _make_device() - with pytest.raises(KeyError): - await h._execute_state_change(d, mode="MANUAL") + assert await h._execute_state_change(d, mode="MANUAL") is False + session.get_devices.assert_not_called() + + async def test_set_state_timeout_returns_false(self): + """Returns False instead of raising when set_state times out.""" + session = _make_session({"prod-1": {"type": "heating"}}) + session.api.set_state = AsyncMock(side_effect=asyncio.TimeoutError) + h = _make_handler(session) + d = _make_device() + assert await h._execute_state_change(d, mode="MANUAL") is False + session.get_devices.assert_not_called()