pyhive-integration 1.0.9.dev2__tar.gz → 1.0.9.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {pyhive_integration-1.0.9.dev2/pyhive_integration.egg-info → pyhive_integration-1.0.9.dev3}/PKG-INFO +1 -1
  2. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3/pyhive_integration.egg-info}/PKG-INFO +1 -1
  3. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/setup.py +1 -1
  4. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/hive_auth_async.py +129 -54
  5. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/session.py +105 -47
  6. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/LICENSE +0 -0
  7. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/MANIFEST.in +0 -0
  8. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/README.md +0 -0
  9. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyhive_integration.egg-info/SOURCES.txt +0 -0
  10. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyhive_integration.egg-info/dependency_links.txt +0 -0
  11. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyhive_integration.egg-info/requires.txt +0 -0
  12. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyhive_integration.egg-info/top_level.txt +0 -0
  13. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyproject.toml +0 -0
  14. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/requirements.txt +0 -0
  15. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/requirements_test.txt +0 -0
  16. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/setup.cfg +0 -0
  17. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/__init__.py +0 -0
  18. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/action.py +0 -0
  19. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/alarm.py +0 -0
  20. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/__init__.py +0 -0
  21. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/hive_api.py +0 -0
  22. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/hive_async_api.py +0 -0
  23. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/hive_auth.py +0 -0
  24. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/camera.py +0 -0
  25. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/device_attributes.py +0 -0
  26. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/heating.py +0 -0
  27. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/__init__.py +0 -0
  28. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/const.py +0 -0
  29. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/debugger.py +0 -0
  30. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/hive_exceptions.py +0 -0
  31. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/hive_helper.py +0 -0
  32. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/hivedataclasses.py +0 -0
  33. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/map.py +0 -0
  34. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/hive.py +0 -0
  35. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/hotwater.py +0 -0
  36. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/hub.py +0 -0
  37. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/light.py +0 -0
  38. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/plug.py +0 -0
  39. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/sensor.py +0 -0
  40. {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/tests/test_hub.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyhive-integration
3
- Version: 1.0.9.dev2
3
+ Version: 1.0.9.dev3
4
4
  Summary: A Python library to interface with the Hive API
5
5
  Home-page: https://github.com/Pyhive/pyhiveapi
6
6
  Author: KJonline24
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyhive-integration
3
- Version: 1.0.9.dev2
3
+ Version: 1.0.9.dev3
4
4
  Summary: A Python library to interface with the Hive API
5
5
  Home-page: https://github.com/Pyhive/pyhiveapi
6
6
  Author: KJonline24
@@ -17,7 +17,7 @@ def requirements_from_file(filename="requirements.txt"):
17
17
 
18
18
 
19
19
  setup(
20
- version="1.0.9.dev2",
20
+ version="1.0.9.dev3",
21
21
  packages=["apyhiveapi", "apyhiveapi.api", "apyhiveapi.helper"],
22
22
  package_dir={"apyhiveapi": "src"},
23
23
  package_data={"data": ["*.json"]},
@@ -23,7 +23,6 @@ from ..helper.hive_exceptions import (
23
23
  HiveInvalidDeviceAuthentication,
24
24
  HiveInvalidPassword,
25
25
  HiveInvalidUsername,
26
- HiveReauthRequired,
27
26
  HiveRefreshTokenExpired,
28
27
  )
29
28
  from .hive_api import HiveApi
@@ -362,13 +361,14 @@ class HiveAuthAsync:
362
361
  return response
363
362
 
364
363
  async def login(self):
365
- """Login into a Hive account."""
364
+ """Login into a Hive account - handles initial SRP auth only."""
366
365
  if self.use_file:
367
366
  _LOGGER.debug("login - Using file-based authentication.")
368
367
  return self.file_response
369
368
 
370
369
  if self.client is None:
371
370
  await self.async_init()
371
+
372
372
  auth_params = await self.get_auth_params()
373
373
  response = None
374
374
  result = None
@@ -423,6 +423,8 @@ class HiveAuthAsync:
423
423
  )
424
424
  raise HiveApiError from err
425
425
 
426
+ _LOGGER.debug("login - SRP auth challenge completed successfully.")
427
+
426
428
  if (
427
429
  "AuthenticationResult" in result
428
430
  and "NewDeviceMetadata" in result["AuthenticationResult"]
@@ -434,73 +436,56 @@ class HiveAuthAsync:
434
436
  self.device_key = result["AuthenticationResult"]["NewDeviceMetadata"][
435
437
  "DeviceKey"
436
438
  ]
439
+ _LOGGER.debug("login - Device keys stored successfully.")
437
440
 
438
- _LOGGER.debug("login - SRP auth challenge completed successfully.")
439
441
  return result
442
+
440
443
  challenge_name = response["ChallengeName"]
441
444
  _LOGGER.error("Unsupported Cognito challenge: %s", challenge_name)
442
445
  raise NotImplementedError(f"The {challenge_name} challenge is not supported")
443
446
 
444
447
  async def device_login(self):
445
- """Perform device login instead."""
448
+ """Perform device login - handles DEVICE_SRP_AUTH challenge.
449
+
450
+ Returns:
451
+ dict: Authentication result with tokens.
452
+ """
446
453
  _LOGGER.debug("device_login - Starting device SRP authentication.")
447
- login_result = await self.login()
448
454
 
449
- if "AuthenticationResult" in login_result:
450
- # Login succeeded without a device challenge (e.g. device tracking
451
- # not enforced). Tokens are already valid, return them directly.
452
- _LOGGER.debug(
453
- "device_login - Device login succeeded with only "
454
- + "username/password challenge: %s",
455
- login_result,
456
- )
457
- return login_result
455
+ if self.client is None:
456
+ await self.async_init()
458
457
 
459
458
  auth_params = await self.get_auth_params(is_device_login=True)
460
459
 
461
- if login_result.get("ChallengeName") == self.DEVICE_VERIFIER_CHALLENGE:
462
- _LOGGER.debug("device_login - Processing DEVICE_SRP_AUTH challenge.")
463
- try:
464
- initial_result = await self.loop.run_in_executor(
465
- None,
466
- functools.partial(
467
- self.client.respond_to_auth_challenge,
468
- ClientId=self.__client_id,
469
- ChallengeName=self.DEVICE_VERIFIER_CHALLENGE,
470
- ChallengeResponses=auth_params,
471
- ),
472
- )
460
+ _LOGGER.debug("device_login - Processing DEVICE_SRP_AUTH challenge.")
461
+ try:
462
+ initial_result = await self.loop.run_in_executor(
463
+ None,
464
+ functools.partial(
465
+ self.client.respond_to_auth_challenge,
466
+ ClientId=self.__client_id,
467
+ ChallengeName=self.DEVICE_VERIFIER_CHALLENGE,
468
+ ChallengeResponses=auth_params,
469
+ ),
470
+ )
473
471
 
474
- device_challenge_response = await self.process_device_challenge(
475
- initial_result["ChallengeParameters"]
476
- )
477
- result = await self.loop.run_in_executor(
478
- None,
479
- functools.partial(
480
- self.client.respond_to_auth_challenge,
481
- ClientId=self.__client_id,
482
- ChallengeName=self.DEVICE_PASSWORD_CHALLENGE,
483
- ChallengeResponses=device_challenge_response,
484
- ),
485
- )
486
- except botocore.exceptions.EndpointConnectionError as err:
487
- if err.__class__.__name__ == "EndpointConnectionError":
488
- _LOGGER.error("Device login failed: cannot reach endpoint.")
489
- raise HiveApiError from err
490
- elif login_result.get("ChallengeName") == self.SMS_MFA_CHALLENGE:
491
- # Account has 2FA enabled and device is not remembered by Cognito.
492
- # Automatic re-authentication is not possible without user interaction.
493
- _LOGGER.error(
494
- "Device login failed: SMS MFA challenge not supported "
495
- + "when device is not remembered."
472
+ device_challenge_response = await self.process_device_challenge(
473
+ initial_result["ChallengeParameters"]
496
474
  )
497
- raise HiveReauthRequired
498
- else:
499
- _LOGGER.error(
500
- "Device login failed: expected DEVICE_SRP_AUTH challenge, got %s.",
501
- login_result.get("ChallengeName"),
475
+ result = await self.loop.run_in_executor(
476
+ None,
477
+ functools.partial(
478
+ self.client.respond_to_auth_challenge,
479
+ ClientId=self.__client_id,
480
+ ChallengeName=self.DEVICE_PASSWORD_CHALLENGE,
481
+ ChallengeResponses=device_challenge_response,
482
+ ),
502
483
  )
503
- raise HiveInvalidDeviceAuthentication
484
+ except botocore.exceptions.EndpointConnectionError as err:
485
+ if err.__class__.__name__ == "EndpointConnectionError":
486
+ _LOGGER.error("Device login failed: cannot reach endpoint.")
487
+ raise HiveApiError from err
488
+ raise HiveInvalidDeviceAuthentication from err
504
489
 
505
490
  _LOGGER.debug("device_login - Device authentication completed successfully.")
506
491
  return result
@@ -671,6 +656,96 @@ class HiveAuthAsync:
671
656
  _LOGGER.debug("refresh_token - Cognito token refresh completed successfully.")
672
657
  return result
673
658
 
659
+ async def is_device_registered(self, access_token=None, device_key=None):
660
+ """Check if the current device is registered with Cognito.
661
+
662
+ Args:
663
+ access_token (str, optional): Access token. Defaults to self.access_token.
664
+ device_key (str, optional): Device key. Defaults to self.device_key.
665
+
666
+ Returns:
667
+ bool: True if device is registered and remembered, False otherwise.
668
+
669
+ Raises:
670
+ HiveApiError: If unable to reach Cognito endpoint.
671
+ """
672
+ if self.client is None:
673
+ await self.async_init()
674
+
675
+ token = access_token or self.access_token
676
+ key = device_key or self.device_key
677
+
678
+ if not token or not key:
679
+ _LOGGER.debug(
680
+ "is_device_registered - Missing access token or device key, "
681
+ "device not registered"
682
+ )
683
+ return False
684
+
685
+ _LOGGER.debug(
686
+ "is_device_registered - Checking device registration status for device: %s",
687
+ key,
688
+ )
689
+
690
+ try:
691
+ result = await self.loop.run_in_executor(
692
+ None,
693
+ functools.partial(
694
+ self.client.get_device,
695
+ AccessToken=token,
696
+ DeviceKey=key,
697
+ ),
698
+ )
699
+
700
+ if result and "Device" in result:
701
+ device_status = result["Device"].get("DeviceAttributes", [])
702
+ # Check if device is in "remembered" status
703
+ for attr in device_status:
704
+ if (
705
+ attr.get("Name") == "dev:device_remembered_status"
706
+ and attr.get("Value") == "remembered"
707
+ ):
708
+ _LOGGER.debug(
709
+ "is_device_registered - Device %s is registered and remembered",
710
+ key,
711
+ )
712
+ return True
713
+
714
+ _LOGGER.debug(
715
+ "is_device_registered - Device %s is registered but not remembered",
716
+ key,
717
+ )
718
+
719
+ except botocore.exceptions.ClientError as err:
720
+ error = (err.response or {}).get("Error", {})
721
+ error_code = error.get("Code")
722
+ error_message = error.get("Message", "")
723
+
724
+ if error_code == "ResourceNotFoundException":
725
+ _LOGGER.debug(
726
+ "is_device_registered - Device %s not found in Cognito", key
727
+ )
728
+ elif error_code == "NotAuthorizedException":
729
+ _LOGGER.warning(
730
+ "is_device_registered - Not authorized to check device status: %s",
731
+ error_message,
732
+ )
733
+ else:
734
+ _LOGGER.error(
735
+ "is_device_registered - Error checking device status: %s - %s",
736
+ error_code,
737
+ error_message,
738
+ )
739
+
740
+ except botocore.exceptions.EndpointConnectionError as err:
741
+ _LOGGER.error(
742
+ "is_device_registered - Cannot reach Cognito endpoint: %s", str(err)
743
+ )
744
+ raise HiveApiError from err
745
+
746
+ # Default: device not registered or status unknown
747
+ return False
748
+
674
749
  async def forget_device(self, access_token, device_key):
675
750
  """Forget device registered with Hive."""
676
751
  if self.client is None:
@@ -293,7 +293,14 @@ class HiveSession:
293
293
  return self.tokens
294
294
 
295
295
  async def login(self):
296
- """Login to hive account.
296
+ """Login to hive account with business logic routing.
297
+
298
+ Business Rules:
299
+ 1) Login successfully - tokens returned, no device login or SMS2FA needed
300
+ 2) Check for device login or SMS challenges
301
+ 3) Direct flow to one of the two
302
+ 4) If device login, process ends but check if device is registered
303
+ 5) If SMS, follow on with device registration
297
304
 
298
305
  Raises:
299
306
  HiveUnknownConfiguration: Login information is unknown.
@@ -318,82 +325,122 @@ class HiveSession:
318
325
  _LOGGER.error("Login failed: API error or no internet connection.")
319
326
  raise
320
327
 
328
+ # Rule 1: Login successful - tokens returned, no challenges needed
321
329
  if result and "AuthenticationResult" in result:
322
330
  auth_keys = list(result["AuthenticationResult"].keys())
323
331
  _LOGGER.debug(
324
332
  "login - Login successful — AuthenticationResult keys: %s", auth_keys
325
333
  )
326
334
  await self.updateTokens(result)
327
- return result
335
+ return result
336
+
337
+ # Rule 2 & 3: Check for device login or SMS challenges and route
338
+ challenge_name = result.get("ChallengeName")
339
+ _LOGGER.debug("login - Challenge detected: %s", challenge_name)
340
+
341
+ if challenge_name == self.auth.DEVICE_VERIFIER_CHALLENGE:
342
+ # Rule 4: Device login flow - check if device is registered
343
+ _LOGGER.debug("login - Routing to device login flow")
344
+ return await self._handleDeviceLoginChallenge(result)
345
+ elif challenge_name == self.auth.SMS_MFA_CHALLENGE:
346
+ # Rule 5: SMS flow - will need device registration after 2FA
347
+ _LOGGER.debug("login - Routing to SMS 2FA flow (requires user input)")
348
+ return result
349
+ else:
350
+ _LOGGER.error("login - Unsupported challenge: %s", challenge_name)
351
+ raise HiveUnknownConfiguration
328
352
 
329
- async def sms2fa(self, code, session):
330
- """Login to hive account with 2 factor authentication.
353
+ async def _handleDeviceLoginChallenge(self, login_result):
354
+ """Handle device login challenge.
331
355
 
332
- Raises:
333
- HiveUnknownConfiguration: Login information is unknown.
356
+ Args:
357
+ login_result (dict): Result from initial login with DEVICE_SRP_AUTH challenge.
334
358
 
335
359
  Returns:
336
- dict: result of the authentication request.
360
+ dict: Authentication result with tokens.
361
+
362
+ Raises:
363
+ HiveReauthRequired: If device login encounters SMS_MFA (device not remembered).
364
+ HiveInvalidDeviceAuthentication: If device is not registered.
337
365
  """
338
- result = None
339
- if not self.auth:
340
- _LOGGER.error("2FA failed: authentication not initialised.")
341
- raise HiveUnknownConfiguration
366
+ _LOGGER.debug("_handleDeviceLoginChallenge - Processing device login")
367
+
368
+ # Check if device is registered before attempting device login
369
+ is_registered = await self.auth.is_device_registered()
370
+ if not is_registered:
371
+ _LOGGER.warning(
372
+ "_handleDeviceLoginChallenge - Device not registered, "
373
+ "cannot complete device login. User must complete SMS 2FA."
374
+ )
375
+ raise HiveInvalidDeviceAuthentication
342
376
 
343
- _LOGGER.debug("sms_2fa - Submitting 2FA code.")
344
- try:
345
- result = await self.auth.sms_2fa(code, session)
346
- except HiveInvalid2FACode:
347
- _LOGGER.error("2FA failed: invalid code entered.")
348
- raise
349
- except HiveApiError:
350
- _LOGGER.error("2FA failed: API error or no internet connection.")
351
- raise
377
+ # Device is registered, proceed with device login
378
+ _LOGGER.debug("_handleDeviceLoginChallenge - Device is registered, proceeding")
379
+ result = await self.auth.device_login()
380
+
381
+ # Check if device login returned SMS_MFA challenge (device not remembered by Cognito)
382
+ if result and result.get("ChallengeName") == self.auth.SMS_MFA_CHALLENGE:
383
+ _LOGGER.error(
384
+ "_handleDeviceLoginChallenge - Device login failed: SMS MFA challenge detected. "
385
+ "Device is not remembered by Cognito. User must reauthenticate."
386
+ )
387
+ raise HiveReauthRequired
352
388
 
353
389
  if result and "AuthenticationResult" in result:
354
390
  auth_keys = list(result["AuthenticationResult"].keys())
355
391
  _LOGGER.debug(
356
- "sms_2fa - 2FA login successful — AuthenticationResult keys: %s",
392
+ "_handleDeviceLoginChallenge - Device login successful — AuthenticationResult keys: %s",
357
393
  auth_keys,
358
394
  )
359
395
  await self.updateTokens(result)
396
+
360
397
  return result
361
398
 
362
- async def deviceLogin(self):
363
- """Login to hive account using device authentication.
399
+ async def sms2fa(self, code, session):
400
+ """Login to hive account with 2 factor authentication.
401
+
402
+ After successful SMS 2FA, checks if device needs registration and
403
+ handles it automatically (Rule 5).
364
404
 
365
405
  Raises:
366
406
  HiveUnknownConfiguration: Login information is unknown.
367
- HiveInvalidDeviceAuthentication: Device information is unknown.
368
407
 
369
408
  Returns:
370
- dict: result of the authentication request.
409
+ dict: result of the authentication request with device data if registered.
371
410
  """
372
411
  result = None
373
412
  if not self.auth:
374
- _LOGGER.error("Device login failed: authentication not initialised.")
413
+ _LOGGER.error("2FA failed: authentication not initialised.")
375
414
  raise HiveUnknownConfiguration
376
415
 
377
- _LOGGER.debug("deviceLogin - Attempting device login.")
416
+ _LOGGER.debug("sms_2fa - Submitting 2FA code.")
378
417
  try:
379
- result = await self.auth.device_login()
380
- except HiveInvalidDeviceAuthentication:
381
- _LOGGER.error("Device login failed: invalid device credentials.")
418
+ result = await self.auth.sms_2fa(code, session)
419
+ except HiveInvalid2FACode:
420
+ _LOGGER.error("2FA failed: invalid code entered.")
421
+ raise
422
+ except HiveApiError:
423
+ _LOGGER.error("2FA failed: API error or no internet connection.")
382
424
  raise
383
425
 
384
426
  if result and "AuthenticationResult" in result:
385
427
  auth_keys = list(result["AuthenticationResult"].keys())
386
428
  _LOGGER.debug(
387
- "deviceLogin - Device login successful — AuthenticationResult keys: %s",
429
+ "sms_2fa - 2FA login successful — AuthenticationResult keys: %s",
388
430
  auth_keys,
389
431
  )
390
432
  await self.updateTokens(result)
433
+
391
434
  return result
392
435
 
393
- async def _retryDeviceLogin(self):
394
- """Attempt device login with retries and backoff.
436
+ async def _retryLogin(self):
437
+ """Attempt login with retries and backoff.
438
+
439
+ This is called when token refresh fails. It attempts to login again,
440
+ which may succeed via device login or may require user interaction (SMS 2FA).
395
441
 
396
442
  Raises:
443
+ HiveReauthRequired: User interaction required (SMS 2FA challenge).
397
444
  HiveInvalidDeviceAuthentication: Device credentials are invalid.
398
445
  HiveApiError: API error or no internet connection.
399
446
  """
@@ -402,26 +449,37 @@ class HiveSession:
402
449
  try:
403
450
  if delay_s:
404
451
  _LOGGER.debug(
405
- "deviceLogin - Retrying device login in %s seconds.", delay_s
452
+ "_retryLogin - Retrying login in %s seconds.", delay_s
406
453
  )
407
454
  await asyncio.sleep(delay_s)
408
- await self.deviceLogin()
455
+ result = await self.login()
456
+
457
+ # Check if login returned SMS_MFA challenge (requires user interaction)
458
+ if (
459
+ result
460
+ and result.get("ChallengeName") == self.auth.SMS_MFA_CHALLENGE
461
+ ):
462
+ _LOGGER.error(
463
+ "_retryLogin - Login requires SMS 2FA. User must reauthenticate."
464
+ )
465
+ raise HiveReauthRequired
466
+
409
467
  last_err = None
410
468
  break
411
- except HiveInvalidDeviceAuthentication:
469
+ except (HiveInvalidUsername, HiveInvalidPassword):
412
470
  _LOGGER.error(
413
- "Device login failed with invalid credentials, reauthentication required."
471
+ "_retryLogin - Login failed with invalid credentials, reauthentication required."
414
472
  )
473
+ raise HiveReauthRequired
474
+ except HiveReauthRequired:
475
+ # Propagate reauthentication requirement immediately
476
+ raise
415
477
  except HiveApiError as err:
416
- _LOGGER.error("Device login attempt failed: %s", err)
478
+ _LOGGER.error("_retryLogin - Login attempt failed: %s", err)
417
479
  last_err = err
418
480
  if last_err is not None:
419
- _LOGGER.error(
420
- "All device login retries exhausted, but device login may resolve the issue."
421
- )
422
- # Don't raise HiveReauthRequired - let the caller handle the error
423
- # Device login itself should resolve 403 issues when successful
424
- return
481
+ _LOGGER.error("_retryLogin - All login retries exhausted.")
482
+ raise HiveReauthRequired from last_err
425
483
 
426
484
  await self.hiveRefreshTokens(force_refresh=True)
427
485
 
@@ -488,11 +546,11 @@ class HiveSession:
488
546
  )
489
547
  except (HiveRefreshTokenExpired, HiveFailedToRefreshTokens) as exc:
490
548
  _LOGGER.warning(
491
- "Session Token refresh failed (%s), falling back to device login.",
549
+ "Session Token refresh failed (%s), falling back to login.",
492
550
  type(exc).__name__,
493
551
  )
494
552
  if not force_refresh:
495
- await self._retryDeviceLogin()
553
+ await self._retryLogin()
496
554
  else:
497
555
  _LOGGER.error(
498
556
  "Token refresh failed during retry attempt, giving up."
@@ -648,7 +706,7 @@ class HiveSession:
648
706
  "Auth error (401/403) after token refresh, "
649
707
  "falling back to full device re-login."
650
708
  )
651
- await self._retryDeviceLogin()
709
+ await self._retryLogin()
652
710
  last_auth_err = None
653
711
  for api_retry_delay in (0, 5, 10):
654
712
  try: