From 12f8a3507b6e63a8703c8c8fe29ff026007c352c Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sat, 23 May 2026 16:37:57 +0100 Subject: [PATCH 01/10] refactor: improve exception handling and code organization - Replace __class__.__name__ string checks with direct error.response parsing for boto3 ClientError - Remove redundant if checks when re-raising EndpointConnectionError as HiveApiError - Defer asyncio.get_event_loop() call to async_init() using get_running_loop() - Remove deprecated pool_region parameter from HiveAuthAsync.__init__ - Add HiveError base class and reorganize exception hierarchy (HiveConfigurationError, HiveAuthCredentialError) --- .coverage_full | Bin 0 -> 335872 bytes .secrets.baseline | 32 +++---- src/__init__.py | 3 + src/api/device_registration.py | 18 ++-- src/api/hive_async_api.py | 26 +++--- src/api/hive_auth_async.py | 76 +++++++--------- src/devices/sensor.py | 26 +++++- src/helper/const.py | 23 ----- src/helper/hive_exceptions.py | 72 ++++++++++----- src/helper/hive_helper.py | 14 --- src/helper/map.py | 7 +- src/session/polling.py | 19 ++-- tests/unit/test_device_registration.py | 52 +---------- tests/unit/test_hive_async_api.py | 89 ++++++++++++------- tests/unit/test_hive_auth_async.py | 27 +++++- tests/unit/test_hive_auth_async_extended.py | 61 +++++++------ tests/unit/test_hive_exceptions.py | 93 ++++++++++++++++++++ tests/unit/test_hive_helper_extended.py | 32 ------- tests/unit/test_map.py | 15 +++- tests/unit/test_polling.py | 74 ++++++++++++++++ tests/unit/test_remaining_branches.py | 6 +- 21 files changed, 458 insertions(+), 307 deletions(-) create mode 100644 .coverage_full create mode 100644 tests/unit/test_hive_exceptions.py diff --git a/.coverage_full b/.coverage_full new file mode 100644 index 0000000000000000000000000000000000000000..6aa88301b4fa613ea857311cec0dd12aa4b5fd1c GIT binary patch literal 335872 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

G34?~)kJku) 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 N`| dict[str, Any]: """Return a copy of payload with sensitive values masked for logs.""" diff --git a/src/helper/map.py b/src/helper/map.py index b4bd8ea..3b38c9d 100644 --- a/src/helper/map.py +++ b/src/helper/map.py @@ -10,6 +10,11 @@ class Map(dict): dict (dict): dictionary to map. """ - __getattr__ = dict.get + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(f"Map has no key {key!r}") from None + __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ diff --git a/src/session/polling.py b/src/session/polling.py index 27341fc..abe5859 100644 --- a/src/session/polling.py +++ b/src/session/polling.py @@ -144,7 +144,17 @@ async def get_devices(self, _n_id: str): # pylint: disable=too-many-locals,too- api_call_start = time.monotonic() try: api_resp_d = await self.api.get_all() + api_call_duration = time.monotonic() - api_call_start + if api_call_duration > self._slow_poll_threshold: + _LOGGER.debug( + "get_devices - Hive API response took %.1fs — marking poll as slow.", + api_call_duration, + ) + self._last_poll_slow = True + else: + self._last_poll_slow = False except HiveAuthError: + self._last_poll_slow = False _LOGGER.warning( "Auth error (401/403) after token refresh, " "falling back to full device re-login." @@ -154,15 +164,6 @@ async def get_devices(self, _n_id: str): # pylint: disable=too-many-locals,too- self.api.get_all, reraise_as=HiveReauthRequired, ) - api_call_duration = time.monotonic() - api_call_start - if api_call_duration > self._slow_poll_threshold: - _LOGGER.debug( - "get_devices - Hive API response took %.1fs — marking poll as slow.", - api_call_duration, - ) - self._last_poll_slow = True - else: - self._last_poll_slow = False if not str(api_resp_d["original"]).startswith("2"): raise HTTPException if api_resp_d["parsed"] is None: diff --git a/tests/unit/test_device_registration.py b/tests/unit/test_device_registration.py index 372eada..43ad3bc 100644 --- a/tests/unit/test_device_registration.py +++ b/tests/unit/test_device_registration.py @@ -482,32 +482,9 @@ async def test_other_client_error_does_not_raise(self): result = await stub.forget_device("acc-token", "dev-key") assert result is None - async def test_endpoint_error_does_not_raise_api_error(self): - """EndpointConnectionError only raises HiveApiError if class name is - 'ResourceNotFoundException', which can never be true for an - EndpointConnectionError. The exception is therefore silently swallowed.""" + async def test_endpoint_error_raises_api_error(self): stub = await _make_stub() stub.loop.run_in_executor.side_effect = _endpoint_error() - # The guard condition is always False for a real EndpointConnectionError, - # so no exception propagates. - result = await stub.forget_device("acc-token", "dev-key") - assert result is None - - async def test_endpoint_error_named_resource_not_found_raises_api_error(self): - """A subclass of EndpointConnectionError named 'ResourceNotFoundException' - satisfies the guard at line 339 and raises HiveApiError (line 340).""" - stub = await _make_stub() - # Craft a class whose __class__.__name__ == "ResourceNotFoundException" - # but which IS an EndpointConnectionError (so it's caught by the except clause) - resource_cls = type( - "ResourceNotFoundException", - (botocore.exceptions.EndpointConnectionError,), - {}, - ) - resource_err = resource_cls( - endpoint_url="https://cognito.eu-west-1.amazonaws.com" - ) - stub.loop.run_in_executor.side_effect = resource_err with pytest.raises(HiveApiError): await stub.forget_device("acc-token", "dev-key") @@ -636,33 +613,6 @@ async def test_other_client_error_is_swallowed(self): result = await stub.confirm_device("name") assert result is None # no HiveInvalid2FACode raised - async def test_endpoint_error_wrong_name_is_swallowed(self): - """EndpointConnectionError subclass with wrong __name__ is swallowed (190->193).""" - stub = await _make_stub() - stub.generate_hash_device = AsyncMock( - return_value={"PasswordVerifier": "pv", "Salt": "s"} - ) - wrong_cls = type( - "WrongEndpoint", (botocore.exceptions.EndpointConnectionError,), {} - ) - wrong_err = wrong_cls(endpoint_url="https://cognito.eu-west-1.amazonaws.com") - stub.loop.run_in_executor.side_effect = wrong_err - result = await stub.confirm_device("name") - assert result is None # no HiveApiError raised - - -class TestUpdateDeviceStatusSwallowedEndpointError: - async def test_endpoint_error_wrong_name_is_swallowed(self): - """EndpointConnectionError with wrong name is caught but not re-raised (211->214).""" - stub = await _make_stub() - wrong_cls = type( - "WrongEndpoint", (botocore.exceptions.EndpointConnectionError,), {} - ) - wrong_err = wrong_cls(endpoint_url="https://cognito.eu-west-1.amazonaws.com") - stub.loop.run_in_executor.side_effect = wrong_err - result = await stub.update_device_status() - assert result is None # no HiveApiError raised - class TestDeviceRegistration: async def test_calls_confirm_and_update(self): diff --git a/tests/unit/test_hive_async_api.py b/tests/unit/test_hive_async_api.py index 86c1cd7..ade420c 100644 --- a/tests/unit/test_hive_async_api.py +++ b/tests/unit/test_hive_async_api.py @@ -65,50 +65,42 @@ def _make_api_no_token(_url_contains_sso=False): class TestHiveApiAsyncRequest: - @pytest.mark.asyncio async def test_successful_200_returns_response(self): api = _make_api(status=200, json_data={"ok": True}) resp = await api.request("get", "https://beekeeper.hivehome.com/1.0/nodes/all") assert resp.status == 200 - @pytest.mark.asyncio async def test_201_also_succeeds(self): api = _make_api(status=201) resp = await api.request("post", "https://beekeeper.hivehome.com/1.0/nodes/x/y") assert resp.status == 201 - @pytest.mark.asyncio async def test_sso_url_without_token_does_not_raise(self): api = _make_api_no_token() # Should not raise NoApiToken because "sso" is in the URL resp = await api.request("get", "https://sso.hivehome.com/") assert resp.status == 200 - @pytest.mark.asyncio async def test_non_sso_without_token_raises_no_api_token(self): api = _make_api_no_token() with pytest.raises(NoApiToken): await api.request("get", "https://beekeeper.hivehome.com/1.0/nodes/all") - @pytest.mark.asyncio async def test_401_raises_hive_auth_error(self): api = _make_api(status=401) with pytest.raises(HiveAuthError): await api.request("get", "https://beekeeper.hivehome.com/1.0/nodes/all") - @pytest.mark.asyncio async def test_403_raises_hive_auth_error(self): api = _make_api(status=403) with pytest.raises(HiveAuthError): await api.request("get", "https://beekeeper.hivehome.com/1.0/nodes/all") - @pytest.mark.asyncio async def test_500_raises_hive_api_error(self): api = _make_api(status=500) with pytest.raises(HiveApiError): await api.request("get", "https://beekeeper.hivehome.com/1.0/nodes/all") - @pytest.mark.asyncio async def test_404_raises_hive_api_error(self): api = _make_api(status=404) with pytest.raises(HiveApiError): @@ -121,7 +113,6 @@ async def test_404_raises_hive_api_error(self): class TestGetAll: - @pytest.mark.asyncio async def test_successful_get_all_returns_parsed_json(self): payload = {"products": [], "devices": []} api = _make_api(status=200, json_data=payload) @@ -129,21 +120,18 @@ async def test_successful_get_all_returns_parsed_json(self): assert result["original"] == 200 assert result["parsed"] == payload - @pytest.mark.asyncio async def test_timeout_error_propagates(self): api = _make_api(status=200) api.websession.request.side_effect = asyncio.TimeoutError with pytest.raises(asyncio.TimeoutError): await api.get_all() - @pytest.mark.asyncio async def test_os_error_calls_error_method(self): api = _make_api(status=200) api.websession.request.side_effect = OSError("network down") with pytest.raises(web_exceptions.HTTPError): await api.get_all() - @pytest.mark.asyncio async def test_runtime_error_calls_error_method(self): api = _make_api(status=200) api.websession.request.side_effect = RuntimeError("boom") @@ -157,7 +145,6 @@ async def test_runtime_error_calls_error_method(self): class TestGetEndpoints: - @pytest.mark.asyncio async def test_get_devices_returns_parsed_json(self): payload = [{"id": "dev1"}] api = _make_api(status=200, json_data=payload) @@ -165,7 +152,6 @@ async def test_get_devices_returns_parsed_json(self): assert result["original"] == 200 assert result["parsed"] == payload - @pytest.mark.asyncio async def test_get_products_returns_parsed_json(self): payload = [{"id": "prod1"}] api = _make_api(status=200, json_data=payload) @@ -173,7 +159,6 @@ async def test_get_products_returns_parsed_json(self): assert result["original"] == 200 assert result["parsed"] == payload - @pytest.mark.asyncio async def test_get_actions_returns_parsed_json(self): payload = [{"id": "act1"}] api = _make_api(status=200, json_data=payload) @@ -181,21 +166,18 @@ async def test_get_actions_returns_parsed_json(self): assert result["original"] == 200 assert result["parsed"] == payload - @pytest.mark.asyncio async def test_get_devices_os_error_raises_http_error(self): api = _make_api(status=200) api.websession.request.side_effect = OSError with pytest.raises(web_exceptions.HTTPError): await api.get_devices() - @pytest.mark.asyncio async def test_get_products_os_error_raises_http_error(self): api = _make_api(status=200) api.websession.request.side_effect = OSError with pytest.raises(web_exceptions.HTTPError): await api.get_products() - @pytest.mark.asyncio async def test_get_actions_os_error_raises_http_error(self): api = _make_api(status=200) api.websession.request.side_effect = OSError @@ -209,13 +191,11 @@ async def test_get_actions_os_error_raises_http_error(self): class TestSetState: - @pytest.mark.asyncio async def test_file_in_use_returns_file_response(self): api = _make_api(status=200, file_mode=True) result = await api.set_state("heating", "node-1", mode="MANUAL") assert result == {"original": "file"} - @pytest.mark.asyncio async def test_successful_set_state(self): payload = {"id": "node-1", "mode": "MANUAL"} api = _make_api(status=200, json_data=payload) @@ -223,14 +203,12 @@ async def test_successful_set_state(self): assert result["original"] == 200 assert result["parsed"] == payload - @pytest.mark.asyncio async def test_os_error_calls_error_method(self): api = _make_api(status=200) api.websession.request.side_effect = OSError("fail") with pytest.raises(web_exceptions.HTTPError): await api.set_state("heating", "node-1", mode="MANUAL") - @pytest.mark.asyncio async def test_runtime_error_calls_error_method(self): api = _make_api(status=200) api.websession.request.side_effect = RuntimeError("fail") @@ -244,19 +222,24 @@ async def test_runtime_error_calls_error_method(self): class TestSetAction: - @pytest.mark.asyncio async def test_file_in_use_returns_file_response(self): api = _make_api(status=200, file_mode=True) result = await api.set_action("action-1", '{"status": "on"}') assert result == {"original": "file"} - @pytest.mark.asyncio - async def test_successful_set_action_returns_json_return(self): - api = _make_api(status=200) + async def test_successful_set_action_returns_status_200(self): + payload = {"id": "action-1", "status": "on"} + api = _make_api(status=200, json_data=payload) result = await api.set_action("action-1", '{"status": "on"}') - assert result == api.json_return + assert result["original"] == 200 + assert result["parsed"] == payload + + async def test_runtime_error_calls_error_method(self): + api = _make_api(status=200) + api.websession.request.side_effect = RuntimeError("fail") + with pytest.raises(web_exceptions.HTTPError): + await api.set_action("action-1", "{}") - @pytest.mark.asyncio async def test_os_error_calls_error_method(self): api = _make_api(status=200) api.websession.request.side_effect = OSError @@ -264,13 +247,59 @@ async def test_os_error_calls_error_method(self): await api.set_action("action-1", "{}") +# --------------------------------------------------------------------------- +# Tests: HiveApiAsync.motion_sensor +# --------------------------------------------------------------------------- + + +class TestMotionSensor: + async def test_url_does_not_double_base_url(self): + payload = [{"timestamp": 12345}] + api = _make_api(status=200, json_data=payload) + captured = {} + original_request = api.request + + async def capture_request(method, url, **kwargs): + captured["url"] = url + return await original_request(method, url, **kwargs) + + api.request = capture_request + sensor = {"type": "motionsensor", "id": "ms-001"} + await api.motion_sensor(sensor, 1000000, 2000000) + url = captured["url"] + assert url.startswith(api.base_url + "/products/") + assert "motionsensor/ms-001" in url + assert url.count("https://beekeeper") == 1 + + async def test_motion_sensor_returns_parsed_json(self): + payload = [{"timestamp": 12345}] + api = _make_api(status=200, json_data=payload) + sensor = {"type": "motionsensor", "id": "ms-001"} + result = await api.motion_sensor(sensor, 1000000, 2000000) + assert result["original"] == 200 + assert result["parsed"] == payload + + +# --------------------------------------------------------------------------- +# Tests: HiveApiAsync.refresh_tokens +# --------------------------------------------------------------------------- + + +class TestRefreshTokens: + async def test_no_name_error_when_session_is_none(self): + websession = _make_mock_websession(status=200) + api = HiveApiAsync(hive_session=None, websession=websession) + api.request = AsyncMock() + result = await api.refresh_tokens() + assert result == api.json_return + + # --------------------------------------------------------------------------- # Tests: HiveApiAsync.error # --------------------------------------------------------------------------- class TestError: - @pytest.mark.asyncio async def test_error_raises_http_error(self): api = _make_api() with pytest.raises(web_exceptions.HTTPError): @@ -283,13 +312,11 @@ async def test_error_raises_http_error(self): class TestIsFileBeingUsed: - @pytest.mark.asyncio async def test_file_mode_raises_file_in_use(self): api = _make_api(file_mode=True) with pytest.raises(FileInUse): await api.is_file_being_used() - @pytest.mark.asyncio async def test_not_file_mode_does_not_raise(self): api = _make_api(file_mode=False) await api.is_file_being_used() # Should not raise diff --git a/tests/unit/test_hive_auth_async.py b/tests/unit/test_hive_auth_async.py index fb541f8..fa6939b 100644 --- a/tests/unit/test_hive_auth_async.py +++ b/tests/unit/test_hive_auth_async.py @@ -79,12 +79,27 @@ async def _make_auth( class TestHiveAuthAsyncInit: - def test_pool_region_raises_value_error(self): + def test_pool_region_no_longer_accepted(self): from apyhiveapi.api.hive_auth_async import HiveAuthAsync - with pytest.raises(ValueError, match="pool_region"): + with pytest.raises(TypeError): HiveAuthAsync(username="u", password="p", pool_region="eu-west-1") + async def test_async_init_sets_running_loop(self): + from apyhiveapi.api.hive_auth_async import HiveAuthAsync + + auth = HiveAuthAsync(username="u@test.com", password="pass") + assert auth.loop is None # not set until async_init + mock_data = { + "UPID": "eu-west-1_Test", + "CLIID": "client-id", + "REGION": "eu-west-1_Test", + } + with patch.object(auth.api, "get_login_info", return_value=mock_data): + with patch("boto3.client", return_value=MagicMock()): + await auth.async_init() + assert auth.loop is not None + async def test_file_flag_set_for_magic_username(self): from apyhiveapi.api.hive_auth_async import HiveAuthAsync @@ -444,6 +459,14 @@ async def test_new_device_metadata_in_sms_stores_keys(self): assert auth.device_group_key == "sms-grp" assert auth.device_key == "sms-dev" + @pytest.mark.asyncio + async def test_no_authentication_result_key_does_not_raise(self): + auth = await _make_auth() + auth.loop.run_in_executor.return_value = {"ChallengeName": "SMS_MFA"} + result = await auth.sms_2fa("123456", {"Session": "sess-1"}) + assert auth.access_token is None + assert result == {"ChallengeName": "SMS_MFA"} + # --------------------------------------------------------------------------- # Tests: refresh_token diff --git a/tests/unit/test_hive_auth_async_extended.py b/tests/unit/test_hive_auth_async_extended.py index 54d408d..b74a02b 100644 --- a/tests/unit/test_hive_auth_async_extended.py +++ b/tests/unit/test_hive_auth_async_extended.py @@ -90,13 +90,13 @@ async def test_async_init_sets_pool_id_and_client_id(self): auth.client = None # trigger async_init flow mock_boto_client = MagicMock() - - auth.loop = MagicMock() - auth.loop.run_in_executor = AsyncMock( + mock_loop = MagicMock() + mock_loop.run_in_executor = AsyncMock( side_effect=[_LOGIN_INFO, mock_boto_client] ) - await auth.async_init() + with patch("asyncio.get_running_loop", return_value=mock_loop): + await auth.async_init() assert auth._pool_id == "eu-west-1_TestPool" assert auth._client_id == "test-client-id" @@ -116,12 +116,13 @@ async def test_async_init_splits_region_correctly(self): "REGION": "ap-southeast-2_XyzPool", } mock_boto_client = MagicMock() - auth.loop = MagicMock() - auth.loop.run_in_executor = AsyncMock( + mock_loop = MagicMock() + mock_loop.run_in_executor = AsyncMock( side_effect=[login_info, mock_boto_client] ) - await auth.async_init() + with patch("asyncio.get_running_loop", return_value=mock_loop): + await auth.async_init() assert auth._region == "ap-southeast-2" @@ -712,10 +713,10 @@ async def test_other_client_error_in_initiate_auth_falls_through(self): class TestLoginInitiateAuthSwallowedEndpointError: - """Arc 284->288: EndpointConnectionError caught but class name is wrong.""" + """EndpointConnectionError in initiate_auth always raises HiveApiError.""" - async def test_wrong_name_endpoint_error_in_initiate_auth_falls_through(self): - """EndpointConnectionError with wrong name is swallowed; response stays None.""" + async def test_wrong_name_endpoint_error_in_initiate_auth_raises_api_error(self): + """Any EndpointConnectionError subclass in initiate_auth raises HiveApiError.""" auth = await _make_auth() wrong_cls = type( @@ -724,7 +725,7 @@ async def test_wrong_name_endpoint_error_in_initiate_auth_falls_through(self): wrong_err = wrong_cls(endpoint_url="https://cognito.eu-west-1.amazonaws.com") auth.loop.run_in_executor = AsyncMock(side_effect=wrong_err) - with pytest.raises((TypeError, KeyError)): + with pytest.raises(HiveApiError): await auth.login() @@ -771,10 +772,10 @@ async def test_other_client_error_in_challenge_falls_through(self): class TestLoginChallengeSwallowedEndpointError: - """Arc 313->319: EndpointConnectionError caught with wrong class name in challenge.""" + """EndpointConnectionError in respond_to_auth_challenge always raises HiveApiError.""" - async def test_wrong_name_endpoint_error_in_challenge_falls_through(self): - """EndpointConnectionError with wrong name is swallowed; result stays None.""" + async def test_wrong_name_endpoint_error_in_challenge_raises_api_error(self): + """Any EndpointConnectionError subclass in SRP challenge raises HiveApiError.""" auth = await _make_auth() challenge_response = { @@ -797,7 +798,7 @@ async def test_wrong_name_endpoint_error_in_challenge_falls_through(self): auth.loop.run_in_executor = AsyncMock( side_effect=[challenge_response, wrong_err] ) - with pytest.raises((TypeError, AttributeError)): + with pytest.raises(HiveApiError): await auth.login() @@ -884,11 +885,10 @@ async def test_device_login_calls_second_respond_to_auth_challenge(self): class TestDeviceLoginEndpointWrongName: - """Line 389: EndpointConnectionError with wrong __class__.__name__ raises - HiveInvalidDeviceAuthentication instead of HiveApiError.""" + """Any EndpointConnectionError in device_login always raises HiveApiError.""" - async def test_wrong_name_endpoint_error_raises_invalid_device_auth(self): - """A subclass of EndpointConnectionError with a different name hits line 389.""" + async def test_wrong_name_endpoint_error_raises_api_error(self): + """Any EndpointConnectionError subclass in device_login raises HiveApiError.""" auth = await _make_auth(device_key="dk-err", device_group_key="grp-err") auth.device_password = "dev-pass-err" @@ -898,7 +898,7 @@ async def test_wrong_name_endpoint_error_raises_invalid_device_auth(self): wrong_err = wrong_cls(endpoint_url="https://cognito.eu-west-1.amazonaws.com") auth.loop.run_in_executor = AsyncMock(side_effect=wrong_err) - with pytest.raises(HiveInvalidDeviceAuthentication): + with pytest.raises(HiveApiError): await auth.device_login() @@ -930,10 +930,10 @@ async def test_other_client_error_is_swallowed_returns_none(self): class TestSms2faSwallowedEndpointError: - """Arc 431->435: EndpointConnectionError caught with wrong class name in sms_2fa.""" + """Any EndpointConnectionError in sms_2fa raises HiveApiError.""" - async def test_wrong_name_endpoint_error_is_swallowed(self): - """EndpointConnectionError subclass with wrong name is swallowed; returns None.""" + async def test_wrong_name_endpoint_error_raises_api_error(self): + """Any EndpointConnectionError subclass in sms_2fa raises HiveApiError.""" auth = await _make_auth() wrong_cls = type( @@ -942,8 +942,8 @@ async def test_wrong_name_endpoint_error_is_swallowed(self): wrong_err = wrong_cls(endpoint_url="https://cognito.eu-west-1.amazonaws.com") auth.loop.run_in_executor = AsyncMock(side_effect=wrong_err) - result = await auth.sms_2fa("654321", {"Session": "sess-abc"}) - assert result is None + with pytest.raises(HiveApiError): + await auth.sms_2fa("654321", {"Session": "sess-abc"}) # --------------------------------------------------------------------------- @@ -952,10 +952,10 @@ async def test_wrong_name_endpoint_error_is_swallowed(self): class TestRefreshTokenSwallowedEndpointError: - """Arc 479->485: EndpointConnectionError caught with wrong class name in refresh_token.""" + """Any EndpointConnectionError in refresh_token raises HiveApiError.""" - async def test_wrong_name_endpoint_error_is_swallowed_returns_none(self): - """EndpointConnectionError subclass with wrong name is swallowed; result=None returned.""" + async def test_wrong_name_endpoint_error_raises_api_error(self): + """Any EndpointConnectionError subclass in refresh_token raises HiveApiError.""" auth = await _make_auth() wrong_cls = type( @@ -964,6 +964,5 @@ async def test_wrong_name_endpoint_error_is_swallowed_returns_none(self): wrong_err = wrong_cls(endpoint_url="https://cognito.eu-west-1.amazonaws.com") auth.loop.run_in_executor = AsyncMock(side_effect=wrong_err) - # result initialised to None; exception swallowed; line 485 reached; returns None - result = await auth.refresh_token("some-refresh-token") - assert result is None + with pytest.raises(HiveApiError): + await auth.refresh_token("some-refresh-token") diff --git a/tests/unit/test_hive_exceptions.py b/tests/unit/test_hive_exceptions.py new file mode 100644 index 0000000..33ffb5c --- /dev/null +++ b/tests/unit/test_hive_exceptions.py @@ -0,0 +1,93 @@ +"""Unit tests for the hive_exceptions hierarchy.""" + +import pytest +from apyhiveapi.helper.hive_exceptions import ( + FileInUse, + HiveApiError, + HiveAuthCredentialError, + HiveAuthError, + HiveConfigurationError, + HiveError, + HiveFailedToRefreshTokens, + HiveInvalid2FACode, + HiveInvalidDeviceAuthentication, + HiveInvalidPassword, + HiveInvalidUsername, + HiveReauthRequired, + HiveRefreshTokenExpired, + HiveUnknownConfiguration, + NoApiToken, +) + + +class TestHiveErrorBase: + def test_hive_api_error_is_hive_error(self): + assert issubclass(HiveApiError, HiveError) + + def test_hive_auth_error_is_hive_api_error(self): + assert issubclass(HiveAuthError, HiveApiError) + + def test_hive_auth_error_is_hive_error(self): + assert issubclass(HiveAuthError, HiveError) + + def test_hive_refresh_token_expired_is_hive_api_error(self): + assert issubclass(HiveRefreshTokenExpired, HiveApiError) + + def test_hive_failed_to_refresh_is_hive_api_error(self): + assert issubclass(HiveFailedToRefreshTokens, HiveApiError) + + def test_hive_reauth_required_is_hive_error(self): + assert issubclass(HiveReauthRequired, HiveError) + + +class TestCredentialErrors: + def test_invalid_username_is_hive_auth_credential_error(self): + assert issubclass(HiveInvalidUsername, HiveAuthCredentialError) + + def test_invalid_password_is_hive_auth_credential_error(self): + assert issubclass(HiveInvalidPassword, HiveAuthCredentialError) + + def test_invalid_2fa_is_hive_auth_credential_error(self): + assert issubclass(HiveInvalid2FACode, HiveAuthCredentialError) + + def test_auth_credential_error_is_hive_error(self): + assert issubclass(HiveAuthCredentialError, HiveError) + + +class TestConfigurationErrors: + def test_unknown_config_is_hive_configuration_error(self): + assert issubclass(HiveUnknownConfiguration, HiveConfigurationError) + + def test_invalid_device_auth_is_hive_configuration_error(self): + assert issubclass(HiveInvalidDeviceAuthentication, HiveConfigurationError) + + def test_configuration_error_is_hive_error(self): + assert issubclass(HiveConfigurationError, HiveError) + + +class TestStandaloneExceptions: + def test_file_in_use_is_not_hive_error(self): + assert not issubclass(FileInUse, HiveError) + + def test_no_api_token_is_not_hive_error(self): + assert not issubclass(NoApiToken, HiveError) + + def test_file_in_use_is_exception(self): + assert issubclass(FileInUse, Exception) + + def test_no_api_token_is_exception(self): + assert issubclass(NoApiToken, Exception) + + +class TestInstantiable: + def test_hive_error_is_raiseable(self): + with pytest.raises(HiveError): + raise HiveError("test") + + def test_hive_api_error_caught_as_hive_error(self): + with pytest.raises(HiveError): + raise HiveApiError("test") + + def test_invalid_username_caught_as_hive_error(self): + with pytest.raises(HiveError): + raise HiveInvalidUsername("test") diff --git a/tests/unit/test_hive_helper_extended.py b/tests/unit/test_hive_helper_extended.py index 8c13ec3..671318e 100644 --- a/tests/unit/test_hive_helper_extended.py +++ b/tests/unit/test_hive_helper_extended.py @@ -59,38 +59,6 @@ def test_returns_false_when_cache_is_empty(self): assert helper.get_device_from_id("any-id") is False -# --------------------------------------------------------------------------- -# get_heat_on_demand_device — lines 315-317 -# --------------------------------------------------------------------------- - - -class TestGetHeatOnDemandDevice: - """Covers HiveHelper.get_heat_on_demand_device (lines 315-317).""" - - def test_returns_linked_thermostat(self): - """Looks up TRV by HiveID, then fetches linked thermostat by zone.""" - trv_id = "trv-001" - thermostat_id = "zone-001" - - trv_data = {"state": {"zone": thermostat_id}, "type": "trvcontrol"} - thermostat_data = {"id": thermostat_id, "type": "heating"} - - products = { - trv_id: trv_data, - thermostat_id: thermostat_data, - } - helper = _make_helper(products=products) - - # Device accessed with dict-style key "HiveID" as used inside the method - device = MagicMock() - device.__getitem__ = MagicMock( - side_effect=lambda k: trv_id if k == "HiveID" else None - ) - - result = helper.get_heat_on_demand_device(device) - assert result == thermostat_data - - # --------------------------------------------------------------------------- # sanitize_payload — list masking (line 329) and non-str/dict/list fallthrough # --------------------------------------------------------------------------- diff --git a/tests/unit/test_map.py b/tests/unit/test_map.py index f6209f7..0cd5350 100644 --- a/tests/unit/test_map.py +++ b/tests/unit/test_map.py @@ -1,5 +1,6 @@ """Unit tests for Map — dot-notation dict wrapper.""" +import pytest from apyhiveapi.helper.map import Map @@ -15,10 +16,18 @@ def test_dict_read(): assert m["key"] == "value" -def test_missing_key_returns_none_not_keyerror(): - """Test that missing keys return None instead of raising KeyError.""" +def test_missing_key_raises_attribute_error(): + """Missing attribute access raises AttributeError.""" m = Map({}) - assert m.missing is None + with pytest.raises(AttributeError): + _ = m.missing + + +def test_missing_bracket_key_raises_key_error(): + """Missing bracket access raises KeyError (standard dict behaviour).""" + m = Map({}) + with pytest.raises(KeyError): + _ = m["missing"] def test_nested_access(): diff --git a/tests/unit/test_polling.py b/tests/unit/test_polling.py index e518941..2a18501 100644 --- a/tests/unit/test_polling.py +++ b/tests/unit/test_polling.py @@ -207,3 +207,77 @@ async def test_poll_devices_propagates_false(self): p.get_devices = AsyncMock(return_value=False) result = await p._poll_devices() assert result is False + + +# --------------------------------------------------------------------------- +# TestGetDevicesSlowPoll +# --------------------------------------------------------------------------- + + +class TestGetDevicesSlowPoll: + async def test_auth_error_sets_last_poll_slow_false(self): + from unittest.mock import AsyncMock, MagicMock + + from apyhiveapi.helper.hive_exceptions import HiveAuthError + + p = _make_polling() + p.api = MagicMock() + p.api.get_all = AsyncMock(side_effect=HiveAuthError()) + p.config = MagicMock() + p.config.file = False + p.tokens = MagicMock() + p._last_poll_slow = True # pre-set to True to confirm it gets cleared + + retry_result = { + "original": 200, + "parsed": {"products": [], "devices": [], "actions": []}, + } + + async def fake_retry_login(): + pass + + async def fake_retry_with_backoff(_fn, _reraise_as=None): + return retry_result + + p._retry_login = fake_retry_login + p._retry_with_backoff = fake_retry_with_backoff + p.hive_refresh_tokens = AsyncMock() + p.data = MagicMock() + p.data.products = {} + p.data.devices = {} + p.data.actions = {} + p.config.last_update = MagicMock() + p.config.scan_interval = MagicMock() + + await p.get_devices("No_ID") + assert p._last_poll_slow is False + + async def test_slow_api_call_sets_last_poll_slow_true(self): + from unittest.mock import AsyncMock, MagicMock + + p = _make_polling() + p._slow_poll_threshold = 0 # any call will be "slow" + p.api = MagicMock() + + slow_result = { + "original": 200, + "parsed": {"products": [], "devices": [], "actions": []}, + } + + async def slow_get_all(): + return slow_result + + p.api.get_all = slow_get_all + p.config = MagicMock() + p.config.file = False + p.tokens = MagicMock() + p.hive_refresh_tokens = AsyncMock() + p.data = MagicMock() + p.data.products = {} + p.data.devices = {} + p.data.actions = {} + p.config.last_update = MagicMock() + p.config.scan_interval = MagicMock() + + await p.get_devices("No_ID") + assert p._last_poll_slow is True diff --git a/tests/unit/test_remaining_branches.py b/tests/unit/test_remaining_branches.py index cb5b8fb..2b652d8 100644 --- a/tests/unit/test_remaining_branches.py +++ b/tests/unit/test_remaining_branches.py @@ -525,7 +525,8 @@ class TestSensorGetSensorHiveTypesSensorPath: async def test_contactsensor_in_hive_types_sensor_takes_else_branch(self): """contactsensor is in HIVE_TYPES['Sensor'] and not in sensor_commands key set, so the elif branch is taken.""" - from apyhiveapi.helper.const import HIVE_TYPES, sensor_commands + from apyhiveapi.devices.sensor import sensor_commands + from apyhiveapi.helper.const import HIVE_TYPES # 'contactsensor' is in HIVE_TYPES['Sensor'] and NOT a key in sensor_commands assert "contactsensor" in HIVE_TYPES["Sensor"] @@ -546,7 +547,8 @@ async def test_contactsensor_in_hive_types_sensor_takes_else_branch(self): async def test_motionsensor_in_hive_types_sensor_sets_status(self): """motionsensor is in HIVE_TYPES['Sensor'] and not in sensor_commands key set.""" - from apyhiveapi.helper.const import HIVE_TYPES, sensor_commands + from apyhiveapi.devices.sensor import sensor_commands + from apyhiveapi.helper.const import HIVE_TYPES assert "motionsensor" in HIVE_TYPES["Sensor"] assert "motionsensor" not in sensor_commands From fc7d0fb2d3fb693965637adf3145fbb8d5c28521 Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sat, 23 May 2026 21:38:12 +0100 Subject: [PATCH 02/10] fix: use device_id (not hive_id) to look up data.devices in sensor HIVE_TYPES branch Co-Authored-By: Claude Sonnet 4.6 --- src/devices/sensor.py | 2 +- tests/unit/test_sensor_extended.py | 35 +++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/devices/sensor.py b/src/devices/sensor.py index 42d815e..bed1ebb 100644 --- a/src/devices/sensor.py +++ b/src/devices/sensor.py @@ -157,7 +157,7 @@ async def get_sensor(self, device: Device): device.device_data = props device.parent_device = data.get("parent", None) elif device.hive_type in HIVE_TYPES["Sensor"]: - data = self.session.data.devices.get(device.hive_id, {}) + data = self.session.data.devices.get(device.device_id, {}) device.status = {"state": await self.get_state(device)} props = data.get("props") or {} props["online"] = online diff --git a/tests/unit/test_sensor_extended.py b/tests/unit/test_sensor_extended.py index 68122f3..604c785 100644 --- a/tests/unit/test_sensor_extended.py +++ b/tests/unit/test_sensor_extended.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from apyhiveapi.devices.sensor import Sensor from apyhiveapi.helper.hivedataclasses import Device, SessionConfig @@ -146,6 +146,39 @@ async def test_contact_sensor_in_hive_types_sets_status(self): assert "state" in result.status session.attr.state_attributes.assert_awaited_once() + async def test_contact_sensor_uses_device_id_not_hive_id_for_props(self): + """HIVE_TYPES['Sensor'] branch must look up data.devices by device_id. + + Before the fix, line 160 used hive_id; data was always {} so + device.parent_device was always None even when the device existed. + """ + hive_id = "prod-abc" + device_id = "dev-xyz" # deliberately different from hive_id + + products = {} # contactsensor is NOT in products + devices = { + device_id: { + "props": {"online": True, "signal": -70}, + "parent": "hub-parent-id", + } + } + session = _make_session(products=products, devices=devices) + session.attr.online_offline = AsyncMock(return_value=True) + + device = _make_device( + hive_id=hive_id, device_id=device_id, hive_type="contactsensor" + ) + device.device_data = {"online": True} + + sensor = Sensor(session) + with patch.object(sensor, "get_state", new=AsyncMock(return_value="CLOSED")): + result = await sensor.get_sensor(device) + + assert result is not None + assert device.parent_device == "hub-parent-id", ( + "parent_device must come from data.devices[device_id], not hive_id lookup" + ) + class TestGetState: """Tests for HiveSensor.get_state covering the motionsensor branch (lines 37-42).""" From 82db9cf1648e56681d23819d896cd609df53d2a1 Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sat, 23 May 2026 21:41:33 +0100 Subject: [PATCH 03/10] fix: guard against None from .get() before .split() in async_init and get_password_authentication_key Import HiveUnknownConfiguration and raise it instead of letting AttributeError propagate when REGION or UPID are absent from the SSO login info response, and when _pool_id is None or missing an underscore in get_password_authentication_key. Co-Authored-By: Claude Sonnet 4.6 --- .secrets.baseline | 10 ++-- src/api/hive_auth_async.py | 10 +++- tests/unit/test_hive_auth_async_extended.py | 53 +++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 58ab3f9..1c3de01 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -268,28 +268,28 @@ "filename": "src/api/hive_auth_async.py", "hashed_secret": "5dc786e32e3a0a4611daaf397721c6ef64cd71b0", "is_verified": false, - "line_number": 48 + "line_number": 49 }, { "type": "Secret Keyword", "filename": "src/api/hive_auth_async.py", "hashed_secret": "ac9f290e69cee683ba3c63461f1f3fa02765032a", "is_verified": false, - "line_number": 49 + "line_number": 50 }, { "type": "Secret Keyword", "filename": "src/api/hive_auth_async.py", "hashed_secret": "351b174ccf89601f6f4bd3f3970a4aba7d17c98e", "is_verified": false, - "line_number": 52 + "line_number": 53 }, { "type": "Secret Keyword", "filename": "src/api/hive_auth_async.py", "hashed_secret": "576956b5291ac38d04ef5f82cc974286a857f0b2", "is_verified": false, - "line_number": 104 + "line_number": 110 } ], "src/api/srp_crypto.py": [ @@ -580,5 +580,5 @@ } ] }, - "generated_at": "2026-05-23T15:37:16Z" + "generated_at": "2026-05-23T20:41:24Z" } diff --git a/src/api/hive_auth_async.py b/src/api/hive_auth_async.py index 8bd3875..420c47c 100644 --- a/src/api/hive_auth_async.py +++ b/src/api/hive_auth_async.py @@ -23,6 +23,7 @@ HiveInvalidPassword, HiveInvalidUsername, HiveRefreshTokenExpired, + HiveUnknownConfiguration, ) from .device_registration import DeviceRegistrationMixin from .hive_api import HiveApi @@ -92,7 +93,12 @@ async def async_init(self): self.data = await self.loop.run_in_executor(None, self.api.get_login_info) self._pool_id = self.data.get("UPID") self._client_id = self.data.get("CLIID") - self._region = self.data.get("REGION").split("_")[0] + region_raw = self.data.get("REGION") + if not self._pool_id or not region_raw: + raise HiveUnknownConfiguration( + "SSO login page did not return required pool/region data" + ) + self._region = region_raw.split("_")[0] # Cognito USER_SRP_AUTH does not use IAM credentials — boto3 requires non-None values. self.client = await self.loop.run_in_executor( None, @@ -151,6 +157,8 @@ def get_password_authentication_key(self, username, password, server_b_value, sa u_value = calculate_u(self.large_a_value, server_b_value) if u_value == 0: raise ValueError("U cannot be zero.") + if not self._pool_id or "_" not in self._pool_id: + raise HiveUnknownConfiguration(f"Invalid pool ID format: {self._pool_id!r}") pool_id = self._pool_id.split("_")[1] username_password = f"{pool_id}{username}:{password}" username_password_hash = hash_sha256(username_password.encode("utf-8")) diff --git a/tests/unit/test_hive_auth_async_extended.py b/tests/unit/test_hive_auth_async_extended.py index b74a02b..bc03922 100644 --- a/tests/unit/test_hive_auth_async_extended.py +++ b/tests/unit/test_hive_auth_async_extended.py @@ -966,3 +966,56 @@ async def test_wrong_name_endpoint_error_raises_api_error(self): with pytest.raises(HiveApiError): await auth.refresh_token("some-refresh-token") + + +# --------------------------------------------------------------------------- +# Tests: async_init() — missing REGION or UPID keys +# --------------------------------------------------------------------------- + + +class TestAsyncInitMissingKeys: + """async_init must raise HiveUnknownConfiguration when login info keys are absent.""" + + async def test_async_init_missing_region_raises_configuration_error(self): + """If REGION is absent from login info, raise HiveUnknownConfiguration.""" + from apyhiveapi.api.hive_auth_async import HiveAuthAsync + from apyhiveapi.helper.hive_exceptions import HiveUnknownConfiguration + + auth = HiveAuthAsync(username="user@test.com", password="pass") + bad_login_info = {"UPID": "eu-west-1_TestPool", "CLIID": "test-client-id"} + mock_loop = MagicMock() + mock_loop.run_in_executor = AsyncMock(side_effect=[bad_login_info]) + with patch("asyncio.get_running_loop", return_value=mock_loop): + with pytest.raises(HiveUnknownConfiguration): + await auth.async_init() + + async def test_async_init_missing_upid_raises_configuration_error(self): + """If UPID is absent from login info, raise HiveUnknownConfiguration.""" + from apyhiveapi.api.hive_auth_async import HiveAuthAsync + from apyhiveapi.helper.hive_exceptions import HiveUnknownConfiguration + + auth = HiveAuthAsync(username="user@test.com", password="pass") + bad_login_info = {"CLIID": "test-client-id", "REGION": "eu-west-1_TestPool"} + mock_loop = MagicMock() + mock_loop.run_in_executor = AsyncMock(side_effect=[bad_login_info]) + with patch("asyncio.get_running_loop", return_value=mock_loop): + with pytest.raises(HiveUnknownConfiguration): + await auth.async_init() + + +# --------------------------------------------------------------------------- +# Tests: get_password_authentication_key() — None _pool_id +# --------------------------------------------------------------------------- + + +class TestGetPasswordAuthKeyNonePoolId: + """get_password_authentication_key must not crash with AttributeError when _pool_id is None.""" + + async def test_none_pool_id_raises_configuration_error(self): + """If _pool_id is None, raise HiveUnknownConfiguration (not AttributeError).""" + from apyhiveapi.helper.hive_exceptions import HiveUnknownConfiguration + + auth = await _make_auth() + auth._pool_id = None + with pytest.raises(HiveUnknownConfiguration): + auth.get_password_authentication_key("user", "pass", "DEADBEEF", "ABCDEF") From 27d4a31ce0250b18c637e9c2776ee5c4268c0b98 Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sat, 23 May 2026 22:26:37 +0100 Subject: [PATCH 04/10] fix: catch ZeroDivisionError in colour-temperature conversion methods Extend the except clause in get_min_color_temp, get_max_color_temp, and get_color_temp from KeyError-only to (KeyError, ZeroDivisionError) so that a zero colourTemperature value returned by the Hive API returns None instead of raising an unhandled ZeroDivisionError. Tests added for all three cases. Co-Authored-By: Claude Sonnet 4.6 --- src/devices/color.py | 6 ++-- tests/unit/test_color_extended.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/devices/color.py b/src/devices/color.py index ade4cb2..2442ae9 100644 --- a/src/devices/color.py +++ b/src/devices/color.py @@ -33,7 +33,7 @@ async def get_min_color_temp(self, device: Device): data = self.session.data.products[device.hive_id] state = data["props"]["colourTemperature"]["max"] return round((1 / state) * 1000000) - except KeyError as e: + except (KeyError, ZeroDivisionError) as e: _LOGGER.error(e) return None @@ -50,7 +50,7 @@ async def get_max_color_temp(self, device: Device): data = self.session.data.products[device.hive_id] state = data["props"]["colourTemperature"]["min"] return round((1 / state) * 1000000) - except KeyError as e: + except (KeyError, ZeroDivisionError) as e: _LOGGER.error(e) return None @@ -67,7 +67,7 @@ async def get_color_temp(self, device: Device): data = self.session.data.products[device.hive_id] state = data["state"]["colourTemperature"] return round((1 / state) * 1000000) - except KeyError as e: + except (KeyError, ZeroDivisionError) as e: _LOGGER.error(e) return None diff --git a/tests/unit/test_color_extended.py b/tests/unit/test_color_extended.py index 6795724..b605e0e 100644 --- a/tests/unit/test_color_extended.py +++ b/tests/unit/test_color_extended.py @@ -99,3 +99,49 @@ async def test_keyerror_on_missing_product_returns_none(self): result = await handler.get_max_color_temp(device) assert result is None + + +class TestZeroDivisionGuards: + """Colour-temperature methods must return None instead of raising ZeroDivisionError.""" + + async def test_get_min_color_temp_zero_returns_none(self): + """min colourTemperature == 0 must return None, not raise ZeroDivisionError. + + get_min_color_temp reads colourTemperature['max'] and divides by it, + so 'max' must be 0 to trigger ZeroDivisionError. + """ + session = _make_session( + products={ + "light-1": {"props": {"colourTemperature": {"max": 0, "min": 153}}} + } + ) + h = _make_handler(session) + device = _make_device() + result = await h.get_min_color_temp(device) + assert result is None + + async def test_get_max_color_temp_zero_returns_none(self): + """max colourTemperature == 0 must return None, not raise ZeroDivisionError. + + get_max_color_temp reads colourTemperature['min'] and divides by it, + so 'min' must be 0 to trigger ZeroDivisionError. + """ + session = _make_session( + products={ + "light-1": {"props": {"colourTemperature": {"max": 500, "min": 0}}} + } + ) + h = _make_handler(session) + device = _make_device() + result = await h.get_max_color_temp(device) + assert result is None + + async def test_get_color_temp_zero_returns_none(self): + """state colourTemperature == 0 must return None, not raise ZeroDivisionError.""" + session = _make_session( + products={"light-1": {"state": {"colourTemperature": 0}}} + ) + h = _make_handler(session) + device = _make_device() + result = await h.get_color_temp(device) + assert result is None From 487bc10d05db510f3b871fbaf7210537435d97b3 Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sat, 23 May 2026 22:28:31 +0100 Subject: [PATCH 05/10] fix: epoch_time to_epoch now honours the pattern argument instead of hardcoding it Removes the line that overwrote the caller-supplied `pattern` with a hardcoded Hive format string, so custom format strings are respected. Adds TestEpochTimePattern tests to confirm the fix and prevent regression. Co-Authored-By: Claude Sonnet 4.6 --- src/helper/hive_helper.py | 1 - tests/unit/test_helpers.py | 6 +----- tests/unit/test_hive_helper_extended.py | 26 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/helper/hive_helper.py b/src/helper/hive_helper.py index 35955c9..150a027 100644 --- a/src/helper/hive_helper.py +++ b/src/helper/hive_helper.py @@ -25,7 +25,6 @@ def epoch_time(date_time: Any, pattern: str, action: str) -> Any: Converted value, or ``None`` if *action* is unrecognised. """ if action == "to_epoch": - pattern = "%d.%m.%Y %H:%M:%S" return int(time.mktime(time.strptime(str(date_time), pattern))) if action == "from_epoch": return datetime.datetime.fromtimestamp(int(date_time)).strftime(pattern) diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py index 1f84c3e..9f9578f 100644 --- a/tests/unit/test_helpers.py +++ b/tests/unit/test_helpers.py @@ -38,11 +38,7 @@ class TestEpochTime: """Tests for the top-level epoch_time() helper function.""" def test_to_epoch_returns_int(self): - """to_epoch converts a date string to an integer Unix timestamp. - - Note: epoch_time ignores the *pattern* argument for "to_epoch" — - it always applies "%d.%m.%Y %H:%M:%S" internally. - """ + """to_epoch converts a date string to an integer Unix timestamp.""" result = epoch_time("01.01.2024 12:00:00", "%d.%m.%Y %H:%M:%S", "to_epoch") assert isinstance(result, int) diff --git a/tests/unit/test_hive_helper_extended.py b/tests/unit/test_hive_helper_extended.py index 671318e..f215a8d 100644 --- a/tests/unit/test_hive_helper_extended.py +++ b/tests/unit/test_hive_helper_extended.py @@ -109,3 +109,29 @@ def test_long_string_partially_masked(self): payload = {"password": "supersecretpassword"} result = helper.sanitize_payload(payload) assert result["password"] == "supe...word" + + +# --------------------------------------------------------------------------- +# epoch_time — to_epoch must honour the pattern argument +# --------------------------------------------------------------------------- + + +class TestEpochTimePattern: + """epoch_time to_epoch must honour the pattern argument.""" + + def test_to_epoch_uses_caller_pattern(self): + """Passing a custom pattern must parse the date string with that pattern.""" + from apyhiveapi.helper.hive_helper import epoch_time + + # ISO date — only parses if the custom pattern is respected + result = epoch_time("2024-06-15", "%Y-%m-%d", "to_epoch") + assert isinstance(result, int), "Expected int epoch timestamp" + assert result > 0 + + def test_to_epoch_standard_hive_format_still_works(self): + """The standard Hive date+time format must still parse correctly.""" + from apyhiveapi.helper.hive_helper import epoch_time + + result = epoch_time("15.06.2024 12:00:00", "%d.%m.%Y %H:%M:%S", "to_epoch") + assert isinstance(result, int) + assert result > 0 From d47ee022532f97a6fe8a1f832c2f01aaaf3660f1 Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sun, 24 May 2026 18:32:28 +0100 Subject: [PATCH 06/10] fix: remove SSL verify=False and urllib3 warning suppression; use json.dumps in set_state - Replace manual string-concatenation JSON building in set_state with json.dumps(kwargs) to prevent JSON injection when kwarg values contain double-quotes or backslashes - Remove requests.get(verify=False) SSL bypass from get_login_info - Remove urllib3 import and disable_warnings call that suppressed the SSL warning - Update TestGetLoginInfo assertion to match new call signature (no verify=False) Co-Authored-By: Claude Sonnet 4.6 --- src/api/hive_async_api.py | 13 ++------ tests/unit/test_hive_async_api_extended.py | 37 +++++++++++++++++++++- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/api/hive_async_api.py b/src/api/hive_async_api.py index 391c507..9223da4 100644 --- a/src/api/hive_async_api.py +++ b/src/api/hive_async_api.py @@ -6,7 +6,6 @@ import time import requests -import urllib3 from aiohttp import ClientResponse, ClientSession, ClientTimeout, web_exceptions from pyquery import PyQuery @@ -15,8 +14,6 @@ _LOGGER = logging.getLogger(__name__) -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - class HiveApiAsync: """Hive API Code.""" @@ -111,7 +108,7 @@ def get_login_info(self): """Get login properties to make the login request.""" url = "https://sso.hivehome.com/" - data = requests.get(url=url, verify=False, timeout=self.timeout) + data = requests.get(url=url, timeout=self.timeout) html = PyQuery(data.content) json_data = json.loads( '{"' @@ -251,13 +248,7 @@ async def set_state(self, n_type, n_id, **kwargs): """Set the state of a Device.""" _LOGGER.debug("set_state - Setting state for %s/%s: %s", n_type, n_id, kwargs) json_return = {} - jsc = ( - "{" - + ",".join( - ('"' + str(i) + '": "' + str(t) + '" ' for i, t in kwargs.items()) - ) - + "}" - ) + jsc = json.dumps(kwargs) url = self.urls["nodes"].format(n_type, n_id) try: diff --git a/tests/unit/test_hive_async_api_extended.py b/tests/unit/test_hive_async_api_extended.py index 5a9848c..4de0e13 100644 --- a/tests/unit/test_hive_async_api_extended.py +++ b/tests/unit/test_hive_async_api_extended.py @@ -111,7 +111,7 @@ def test_makes_request_to_sso_url(self): api.get_login_info() mock_get.assert_called_once_with( - url="https://sso.hivehome.com/", verify=False, timeout=api.timeout + url="https://sso.hivehome.com/", timeout=api.timeout ) def test_uses_first_script_tag(self): @@ -425,3 +425,38 @@ async def test_session_none_skips_token_data_read(self): await api.refresh_tokens() except (NameError, UnboundLocalError, AttributeError): pass # expected — tokens was never defined since session is None + + +# --------------------------------------------------------------------------- +# Tests: set_state() JSON encoding — Fix A +# --------------------------------------------------------------------------- + + +class TestSetStateJsonEncoding: + """set_state must produce valid JSON even when kwarg values contain special characters.""" + + async def test_set_state_escapes_quotes_in_value(self): + """A value containing double-quotes must produce valid, parseable JSON.""" + import json # noqa: PLC0415 + + session = MagicMock() + session.tokens.token_data = {"token": "tok"} + session.config.file = False + api = HiveApiAsync(hive_session=session) + api.urls = {"nodes": "https://beekeeper.hivehome.com/1.0/nodes/{}/{}"} + + captured = {} + + async def fake_request(_method, _url, **kwargs): + captured["data"] = kwargs.get("data") + resp = MagicMock() + resp.status = 200 + resp.json = AsyncMock(return_value={}) + return resp + + with patch.object(api, "request", side_effect=fake_request): + with patch.object(api, "is_file_being_used", new=AsyncMock()): + await api.set_state("heating", "node-1", mode='MANUAL"injected') + + parsed = json.loads(captured["data"]) + assert parsed["mode"] == 'MANUAL"injected' From b7b6494ca0c990ad6892e31955d460fc05ffcaa0 Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sun, 24 May 2026 18:39:08 +0100 Subject: [PATCH 07/10] chore: remove unused POOL, deprecated refresh_tokens and its tests, and dead url/status guard Co-Authored-By: Claude Sonnet 4.6 --- .secrets.baseline | 34 ++--- src/api/hive_async_api.py | 42 ++----- src/api/srp_crypto.py | 2 - tests/unit/test_hive_api.py | 113 ----------------- tests/unit/test_hive_async_api.py | 14 --- tests/unit/test_hive_async_api_extended.py | 139 --------------------- 6 files changed, 24 insertions(+), 320 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 1c3de01..28b888f 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -298,112 +298,112 @@ "filename": "src/api/srp_crypto.py", "hashed_secret": "3e619ee0820ecf213c2f38c634e416b53defe3b0", "is_verified": false, - "line_number": 11 + "line_number": 10 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "b8e0d506d969f09a9af89ce89fd9759b72c63262", "is_verified": false, - "line_number": 12 + "line_number": 11 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "e97a751edc71e9afbe0c0f63ec94873392833f9f", "is_verified": false, - "line_number": 13 + "line_number": 12 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "92488c021dd524a2f4e116666b3645308fa0e35c", "is_verified": false, - "line_number": 14 + "line_number": 13 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "d4571e2f026f458aecd2950b0eb6aec190276177", "is_verified": false, - "line_number": 15 + "line_number": 14 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "8109d3c2f659f13cb61fc9e71eed574efe8c8fd8", "is_verified": false, - "line_number": 16 + "line_number": 15 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "08cac7461d7b624b88c53ee47da09cbbb84ea290", "is_verified": false, - "line_number": 17 + "line_number": 16 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "95523fea7e6136c6148299dcc3077debfa2976b3", "is_verified": false, - "line_number": 18 + "line_number": 17 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "c978fb77621e86f5e9077653fe5345ac1616b466", "is_verified": false, - "line_number": 19 + "line_number": 18 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "fc02990268ecf8a35a4912d60dab3754e5f43846", "is_verified": false, - "line_number": 20 + "line_number": 19 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "2c2c0ca491a73e95c8965b6641731057b65f6462", "is_verified": false, - "line_number": 21 + "line_number": 20 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "672b25c6be065170206f3fc6346ebb8e84cbb9d3", "is_verified": false, - "line_number": 22 + "line_number": 21 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "99d02e268ea3ee849fb6e359c6c1b019e4d07efd", "is_verified": false, - "line_number": 23 + "line_number": 22 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "e677fc4cb09d99e1e0d30af31f2e209e541e380e", "is_verified": false, - "line_number": 24 + "line_number": 23 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "05b69b06f40cae0c910a15b1ac75b1f7a847eccb", "is_verified": false, - "line_number": 25 + "line_number": 24 }, { "type": "Hex High Entropy String", "filename": "src/api/srp_crypto.py", "hashed_secret": "c7f914bac2d66eb3f8ae3888fa47bf1ada6caaf5", "is_verified": false, - "line_number": 26 + "line_number": 25 } ], "tests/unit/test_device_registration.py": [ @@ -580,5 +580,5 @@ } ] }, - "generated_at": "2026-05-23T20:41:24Z" + "generated_at": "2026-05-24T17:39:03Z" } diff --git a/src/api/hive_async_api.py b/src/api/hive_async_api.py index 9223da4..f88ff5e 100644 --- a/src/api/hive_async_api.py +++ b/src/api/hive_async_api.py @@ -9,7 +9,7 @@ from aiohttp import ClientResponse, ClientSession, ClientTimeout, web_exceptions from pyquery import PyQuery -from ..helper.const import HTTP_FORBIDDEN, HTTP_OK, HTTP_UNAUTHORIZED +from ..helper.const import HTTP_FORBIDDEN, HTTP_UNAUTHORIZED from ..helper.hive_exceptions import FileInUse, HiveApiError, HiveAuthError, NoApiToken _LOGGER = logging.getLogger(__name__) @@ -94,14 +94,12 @@ async def request(self, method: str, url: str, **kwargs) -> ClientResponse: raise HiveAuthError( f"Token expired or forbidden calling {url} — HTTP {resp.status}" ) - if url is not None and resp.status is not None: - _LOGGER.error( - "Something has gone wrong calling %s - HTTP status is - %s — response: %s", - url, - resp.status, - resp_body[:200], - ) - + _LOGGER.error( + "Something has gone wrong calling %s - HTTP status is - %s — response: %s", + url, + resp.status, + resp_body[:200], + ) raise HiveApiError def get_login_info(self): @@ -125,32 +123,6 @@ def get_login_info(self): login_data.update({"REGION": json_data["HiveSSOPoolId"]}) return login_data - async def refresh_tokens(self): - """Refresh tokens - DEPRECATED NOW BY AWS TOKEN MANAGEMENT.""" - url = self.urls["refresh"] - tokens = self.session.tokens.token_data if self.session is not None else {} - jsc = ( - "{" - + ",".join( - ('"' + str(i) + '": "' + str(t) + '" ' for i, t in tokens.items()) - ) - + "}" - ) - try: - await self.request("post", url, data=jsc) - - if self.json_return["original"] == HTTP_OK: - info = self.json_return["parsed"] - if "token" in info: - await self.session.update_tokens(info) - # pylint: disable-next=invalid-sequence-index - self.base_url = info["platform"]["endpoint"] - return True - except (ConnectionError, OSError, RuntimeError, ZeroDivisionError): - await self.error() - - return self.json_return - async def get_all(self): """Build and query all endpoint.""" json_return = {} diff --git a/src/api/srp_crypto.py b/src/api/srp_crypto.py index faf141b..74cea33 100644 --- a/src/api/srp_crypto.py +++ b/src/api/srp_crypto.py @@ -1,7 +1,6 @@ """Pure SRP/HKDF crypto helpers for AWS Cognito authentication.""" import binascii -import concurrent.futures import hashlib import hmac import os @@ -28,7 +27,6 @@ # https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L49 G_HEX = "2" INFO_BITS = bytearray("Caldera Derived Key", "utf-8") -POOL = concurrent.futures.ThreadPoolExecutor() def hex_to_long(hex_string): diff --git a/tests/unit/test_hive_api.py b/tests/unit/test_hive_api.py index 8b6a182..1cd1093 100644 --- a/tests/unit/test_hive_api.py +++ b/tests/unit/test_hive_api.py @@ -213,119 +213,6 @@ def test_key_error_calls_error_and_returns_none(self): assert result is None -# --------------------------------------------------------------------------- -# Tests: HiveApi.refresh_tokens -# --------------------------------------------------------------------------- - - -class TestRefreshTokens: - def test_successful_with_token_key_updates_session(self): - """When the response contains 'token', session.update_tokens is called.""" - api = _make_api() - refresh_data = { - "token": "new-token", - "platform": {"endpoint": "https://new.endpoint.com"}, - } - mock_resp = _make_mock_response( - 200, json_data=refresh_data, text=json.dumps(refresh_data) - ) - - with patch.object(api, "request", return_value=mock_resp): - result = api.refresh_tokens() - - api.session.update_tokens.assert_called_once_with(refresh_data) - assert result["original"] == 200 - - def test_no_token_in_response_no_session_update(self): - """When response lacks 'token' key, update_tokens is not called.""" - api = _make_api() - response_data = {"other_key": "value"} - mock_resp = _make_mock_response( - 200, json_data=response_data, text=json.dumps(response_data) - ) - - with patch.object(api, "request", return_value=mock_resp): - api.refresh_tokens() - - api.session.update_tokens.assert_not_called() - - def test_none_tokens_defaults_to_empty_dict(self): - """Calling refresh_tokens() without arguments uses session.token_data.""" - api = _make_api() - response_data = {"other": "val"} - mock_resp = _make_mock_response( - 200, json_data=response_data, text=json.dumps(response_data) - ) - - with patch.object(api, "request", return_value=mock_resp) as mock_req: - api.refresh_tokens() - # Should have been called (session provides the tokens dict) - mock_req.assert_called_once() - - def test_os_error_calls_error(self): - api = _make_api() - with patch.object(api, "request", side_effect=OSError("connection failed")): - api.refresh_tokens() - - assert api.json_return["original"] == "Error making API call" - - def test_runtime_error_calls_error(self): - api = _make_api() - with patch.object(api, "request", side_effect=RuntimeError("fail")): - api.refresh_tokens() - - assert api.json_return["original"] == "Error making API call" - - def test_json_decode_error_calls_error(self): - """Bad JSON in response text triggers error().""" - api = _make_api() - mock_resp = MagicMock() - mock_resp.status_code = 200 - mock_resp.text = "not-json" - - with patch.object(api, "request", return_value=mock_resp): - api.refresh_tokens() - - assert api.json_return["original"] == "Error making API call" - - def test_explicit_tokens_arg_skips_none_branch(self): - """Passing a non-None tokens arg covers the 80->82 False branch.""" - api = _make_api() - explicit_tokens = {"key": "val"} - response_data = {"other": "x"} - mock_resp = _make_mock_response(200, json_data=response_data) - - with patch.object(api, "request", return_value=mock_resp): - api.refresh_tokens(tokens=explicit_tokens) - # Session is not None so session tokens overwrite, but no crash - api.session.update_tokens.assert_not_called() - - def test_session_none_skips_token_overwrite(self): - """When session is None the 83->85 False branch is taken (no token overwrite).""" - api = _make_api_no_session(token="standalone-token") - response_data = {"other": "x"} - mock_resp = _make_mock_response(200, json_data=response_data) - - with patch.object(api, "request", return_value=mock_resp): - api.refresh_tokens(tokens={"key": "val"}) - - def test_urls_base_updated_on_token_refresh(self): - """After a successful refresh the base URL is updated from the response.""" - api = _make_api() - refresh_data = { - "token": "new-tok", - "platform": {"endpoint": "https://new-platform.com/1.0"}, - } - mock_resp = _make_mock_response( - 200, json_data=refresh_data, text=json.dumps(refresh_data) - ) - - with patch.object(api, "request", return_value=mock_resp): - api.refresh_tokens() - - assert api.urls["base"] == "https://new-platform.com/1.0" - - # --------------------------------------------------------------------------- # Tests: HiveApi.get_all # --------------------------------------------------------------------------- diff --git a/tests/unit/test_hive_async_api.py b/tests/unit/test_hive_async_api.py index ade420c..c630182 100644 --- a/tests/unit/test_hive_async_api.py +++ b/tests/unit/test_hive_async_api.py @@ -280,20 +280,6 @@ async def test_motion_sensor_returns_parsed_json(self): assert result["parsed"] == payload -# --------------------------------------------------------------------------- -# Tests: HiveApiAsync.refresh_tokens -# --------------------------------------------------------------------------- - - -class TestRefreshTokens: - async def test_no_name_error_when_session_is_none(self): - websession = _make_mock_websession(status=200) - api = HiveApiAsync(hive_session=None, websession=websession) - api.request = AsyncMock() - result = await api.refresh_tokens() - assert result == api.json_return - - # --------------------------------------------------------------------------- # Tests: HiveApiAsync.error # --------------------------------------------------------------------------- diff --git a/tests/unit/test_hive_async_api_extended.py b/tests/unit/test_hive_async_api_extended.py index 4de0e13..eabf190 100644 --- a/tests/unit/test_hive_async_api_extended.py +++ b/tests/unit/test_hive_async_api_extended.py @@ -135,103 +135,6 @@ def test_uses_first_script_tag(self): assert result["UPID"] == "eu-west-1_first" -# --------------------------------------------------------------------------- -# Tests: refresh_tokens() — lines 131-156 -# --------------------------------------------------------------------------- - - -class TestRefreshTokens: - """Cover lines 133-156: refresh_tokens() success, no-token, and error paths.""" - - async def test_successful_request_with_non_ok_json_return_returns_json_return(self): - """When request succeeds but json_return["original"] != HTTP_OK, returns json_return.""" - api = _make_api(status=200) - # request() will succeed (200) but json_return is not updated by refresh_tokens - # so json_return["original"] stays as the default string, not HTTP_OK (200) - result = await api.refresh_tokens() - # Returns self.json_return (the default dict) - assert result == api.json_return - - async def test_session_tokens_read_before_request(self): - """tokens are read from session.tokens.token_data before constructing the request.""" - api = _make_api(status=200, token="my-session-token") - api.session.tokens.token_data = { - "token": "my-session-token", - "refreshToken": "r-tok", - } - result = await api.refresh_tokens() - # No exception raised — tokens were read without error - assert result is not None - - async def test_connection_error_raises_http_error(self): - """ConnectionError inside the try block causes error() → HTTPError.""" - api = _make_api(status=200) - api.websession.request.side_effect = ConnectionError("connection refused") - with pytest.raises(web_exceptions.HTTPError): - await api.refresh_tokens() - - async def test_os_error_raises_http_error(self): - """OSError inside the try block causes error() → HTTPError.""" - api = _make_api(status=200) - api.websession.request.side_effect = OSError("network error") - with pytest.raises(web_exceptions.HTTPError): - await api.refresh_tokens() - - async def test_runtime_error_raises_http_error(self): - """RuntimeError inside the try block causes error() → HTTPError.""" - api = _make_api(status=200) - api.websession.request.side_effect = RuntimeError("bad state") - with pytest.raises(web_exceptions.HTTPError): - await api.refresh_tokens() - - async def test_zero_division_raises_http_error(self): - """ZeroDivisionError inside the try block causes error() → HTTPError.""" - api = _make_api(status=200) - api.websession.request.side_effect = ZeroDivisionError("division by zero") - with pytest.raises(web_exceptions.HTTPError): - await api.refresh_tokens() - - async def test_json_return_true_when_ok_status_in_json_return(self): - """When json_return["original"] equals HTTP_OK (200) and token is present, - update_tokens is called and base_url is updated, returning True.""" - api = _make_api(status=200) - # Manually set json_return to simulate a successful response - api.json_return = { - "original": 200, - "parsed": { - "token": "new-token", - "platform": {"endpoint": "https://new.endpoint"}, - }, - } - api.session.update_tokens = AsyncMock() - - # Patch request to be a no-op (doesn't modify json_return) - with patch.object(api, "request", new_callable=AsyncMock) as mock_req: - mock_req.return_value = MagicMock() - result = await api.refresh_tokens() - - assert result is True - api.session.update_tokens.assert_called_once_with(api.json_return["parsed"]) - assert api.base_url == "https://new.endpoint" - - async def test_json_return_true_without_token_in_parsed(self): - """When json_return["original"] == HTTP_OK but no 'token' in parsed, - update_tokens is NOT called and returns True.""" - api = _make_api(status=200) - api.json_return = { - "original": 200, - "parsed": {"other_key": "value"}, - } - api.session.update_tokens = AsyncMock() - - with patch.object(api, "request", new_callable=AsyncMock) as mock_req: - mock_req.return_value = MagicMock() - result = await api.refresh_tokens() - - assert result is True - api.session.update_tokens.assert_not_called() - - # --------------------------------------------------------------------------- # Tests: motion_sensor() — lines 213-235 # --------------------------------------------------------------------------- @@ -385,48 +288,6 @@ async def test_connection_error_raises_http_error(self): await api.get_weather("?lat=51.5") -# --------------------------------------------------------------------------- -# Tests: request() — url=None and resp.status=None skips the logging branch -# --------------------------------------------------------------------------- - - -class TestRequestUrlOrStatusNone: - """Lines 100->108: when url is None or resp.status is None, skip log → raise directly.""" - - async def test_none_status_skips_log_and_raises_hive_api_error(self): - """resp.status=None causes branch 100->108 (skips the log lines) then raises.""" - api = _make_api(status=200) - # Replace the websession response with one having status=None - bad_resp = _make_mock_response(status=None) - bad_resp.text = AsyncMock(return_value="") - api.websession.request.return_value = bad_resp - with pytest.raises(HiveApiError): - await api.request("get", None) - - -# --------------------------------------------------------------------------- -# Tests: refresh_tokens() — session=None (134->136) -# --------------------------------------------------------------------------- - - -class TestRefreshTokensSessionNone: - """Line 134->136: when self.session is None, skip token_data read (line 135).""" - - async def test_session_none_skips_token_data_read(self): - """When session is None, tokens is not set from session → jsc uses undefined.""" - ws = MagicMock() - ws.request.return_value = _make_mock_response(status=200) - ws.closed = False - ws.close = AsyncMock() - api = HiveApiAsync(hive_session=None, websession=ws) - # tokens is not defined before jsc, so this will raise NameError or UnboundLocalError; - # what we need is that line 134's False branch (134->136) is traversed. - try: - await api.refresh_tokens() - except (NameError, UnboundLocalError, AttributeError): - pass # expected — tokens was never defined since session is None - - # --------------------------------------------------------------------------- # Tests: set_state() JSON encoding — Fix A # --------------------------------------------------------------------------- From f6faebc548d361ba9ee1dd24667d069f6cbdd187 Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sun, 24 May 2026 18:40:45 +0100 Subject: [PATCH 08/10] fix: add device context to bare _LOGGER.error(e) calls in get_mode and boost helpers --- src/devices/boost.py | 4 ++-- src/devices/heating.py | 3 ++- src/devices/hotwater.py | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/devices/boost.py b/src/devices/boost.py index 2e3f1b7..b1e22e9 100644 --- a/src/devices/boost.py +++ b/src/devices/boost.py @@ -29,7 +29,7 @@ async def get_boost_status(self, device: Device): data = self.session.data.products[device.hive_id] return HIVETOHA["Boost"].get(data["state"].get("boost", False), "ON") except KeyError as e: - _LOGGER.error(e) + _LOGGER.error("get_boost_status - KeyError for %s: %s", device.ha_name, e) return None async def get_boost_time(self, device: Device): @@ -43,5 +43,5 @@ async def get_boost_time(self, device: Device): data = self.session.data.products[device.hive_id] return data["state"]["boost"] except KeyError as e: - _LOGGER.error(e) + _LOGGER.error("get_boost_time - KeyError for %s: %s", device.ha_name, e) return None diff --git a/src/devices/heating.py b/src/devices/heating.py index 2b81e1e..2b07d51 100644 --- a/src/devices/heating.py +++ b/src/devices/heating.py @@ -170,6 +170,7 @@ async def get_mode(self, device: Device): """ state = None final = None + device_name = device.ha_name try: data = self.session.data.products[device.hive_id] @@ -178,7 +179,7 @@ async def get_mode(self, device: Device): state = data["props"]["previous"]["mode"] final = HIVETOHA[self.heating_type].get(state, state) except KeyError as e: - _LOGGER.error(e) + _LOGGER.error("get_mode - KeyError getting mode for %s: %s", device_name, e) return final diff --git a/src/devices/hotwater.py b/src/devices/hotwater.py index b6985b2..2de73d9 100644 --- a/src/devices/hotwater.py +++ b/src/devices/hotwater.py @@ -33,6 +33,7 @@ async def get_mode(self, device: Device): """ state = None final = None + device_name = device.ha_name try: data = self.session.data.products[device.hive_id] @@ -41,7 +42,7 @@ async def get_mode(self, device: Device): state = data["props"]["previous"]["mode"] final = HIVETOHA[self.hotwater_type].get(state, state) except KeyError as e: - _LOGGER.error(e) + _LOGGER.error("get_mode - KeyError getting mode for %s: %s", device_name, e) return final From 24612561f1c31edc65944409fdb9bb85cdd1270d Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sun, 24 May 2026 18:43:30 +0100 Subject: [PATCH 09/10] fix: updateInterval now sets config.scan_interval instead of silently returning True Co-Authored-By: Claude Sonnet 4.6 --- src/helper/compat_aliases.py | 3 +- tests/unit/test_compat_aliases.py | 47 +++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/helper/compat_aliases.py b/src/helper/compat_aliases.py index b1fe79e..e65a485 100644 --- a/src/helper/compat_aliases.py +++ b/src/helper/compat_aliases.py @@ -138,6 +138,7 @@ async def updateData(self, device: Device): # pylint: disable=invalid-name """Backwards-compatible alias for update_data.""" return await self.update_data(device) # type: ignore[attr-defined] - async def updateInterval(self, new_interval: int): # pylint: disable=invalid-name,unused-argument + async def updateInterval(self, new_interval: int): # pylint: disable=invalid-name """Backwards-compatible alias for Home Assistant Scan Interval.""" + self.config.scan_interval = new_interval # type: ignore[attr-defined] return True diff --git a/tests/unit/test_compat_aliases.py b/tests/unit/test_compat_aliases.py index 1c066e7..e1dbd6c 100644 --- a/tests/unit/test_compat_aliases.py +++ b/tests/unit/test_compat_aliases.py @@ -11,7 +11,7 @@ SwitchCompatMixin, WaterHeaterCompatMixin, ) -from apyhiveapi.helper.hivedataclasses import Device +from apyhiveapi.helper.hivedataclasses import Device, SessionConfig def _make_device(): @@ -319,13 +319,56 @@ class Stub(SessionCompatMixin): assert s.deviceList is s.device_list async def test_update_interval_returns_true(self): - """updateInterval always returns True (deprecated no-op).""" + """updateInterval returns True and updates config.scan_interval.""" class Stub(SessionCompatMixin): """Stub for updateInterval test.""" device_list = {} + def __init__(self): + self.config = SessionConfig() + s = Stub() result = await s.updateInterval(60) assert result is True + + +# --------------------------------------------------------------------------- +# SessionCompatMixin.updateInterval — bug fix tests +# --------------------------------------------------------------------------- + + +def _make_concrete_session(): + """Return a minimal SessionCompatMixin subclass with a real SessionConfig.""" + + class ConcreteSession(SessionCompatMixin): + """Minimal concrete SessionCompatMixin for updateInterval tests.""" + + def __init__(self): + self.config = SessionConfig() + self.device_list = {} + + async def start_session(self, config=None): # pylint: disable=unused-argument + """Stub.""" + + async def update_data(self, device): # pylint: disable=unused-argument + """Stub.""" + + return ConcreteSession() + + +class TestSessionCompatMixinUpdateInterval: + """updateInterval must actually update config.scan_interval.""" + + async def test_update_interval_sets_scan_interval(self): + """updateInterval(300) must set self.config.scan_interval = 300.""" + session = _make_concrete_session() + await session.updateInterval(300) + assert session.config.scan_interval == 300 + + async def test_update_interval_returns_true(self): + """updateInterval must return True on success.""" + session = _make_concrete_session() + result = await session.updateInterval(60) + assert result is True From d5154b4e9e2096369263202166cbd91b4265eccb Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sun, 24 May 2026 18:48:19 +0100 Subject: [PATCH 10/10] fix: wrap updateInterval new_interval in timedelta(seconds=) to match SessionConfig type Co-Authored-By: Claude Sonnet 4.6 --- src/helper/compat_aliases.py | 3 ++- tests/unit/test_compat_aliases.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/helper/compat_aliases.py b/src/helper/compat_aliases.py index e65a485..8cdfc0e 100644 --- a/src/helper/compat_aliases.py +++ b/src/helper/compat_aliases.py @@ -7,6 +7,7 @@ from __future__ import annotations +from datetime import timedelta from typing import Any from .hivedataclasses import Device @@ -140,5 +141,5 @@ async def updateData(self, device: Device): # pylint: disable=invalid-name async def updateInterval(self, new_interval: int): # pylint: disable=invalid-name """Backwards-compatible alias for Home Assistant Scan Interval.""" - self.config.scan_interval = new_interval # type: ignore[attr-defined] + self.config.scan_interval = timedelta(seconds=new_interval) # type: ignore[attr-defined] return True diff --git a/tests/unit/test_compat_aliases.py b/tests/unit/test_compat_aliases.py index e1dbd6c..ec6c44e 100644 --- a/tests/unit/test_compat_aliases.py +++ b/tests/unit/test_compat_aliases.py @@ -362,10 +362,12 @@ class TestSessionCompatMixinUpdateInterval: """updateInterval must actually update config.scan_interval.""" async def test_update_interval_sets_scan_interval(self): - """updateInterval(300) must set self.config.scan_interval = 300.""" + """updateInterval(300) must set self.config.scan_interval to timedelta(seconds=300).""" + from datetime import timedelta + session = _make_concrete_session() await session.updateInterval(300) - assert session.config.scan_interval == 300 + assert session.config.scan_interval == timedelta(seconds=300) async def test_update_interval_returns_true(self): """updateInterval must return True on success."""