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.
- {pyhive_integration-1.0.9.dev2/pyhive_integration.egg-info → pyhive_integration-1.0.9.dev3}/PKG-INFO +1 -1
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3/pyhive_integration.egg-info}/PKG-INFO +1 -1
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/setup.py +1 -1
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/hive_auth_async.py +129 -54
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/session.py +105 -47
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/LICENSE +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/MANIFEST.in +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/README.md +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyhive_integration.egg-info/SOURCES.txt +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyhive_integration.egg-info/dependency_links.txt +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyhive_integration.egg-info/requires.txt +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyhive_integration.egg-info/top_level.txt +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/pyproject.toml +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/requirements.txt +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/requirements_test.txt +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/setup.cfg +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/__init__.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/action.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/alarm.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/__init__.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/hive_api.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/hive_async_api.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/api/hive_auth.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/camera.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/device_attributes.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/heating.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/__init__.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/const.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/debugger.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/hive_exceptions.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/hive_helper.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/hivedataclasses.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/map.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/hive.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/hotwater.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/hub.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/light.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/plug.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/sensor.py +0 -0
- {pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/tests/test_hub.py +0 -0
|
@@ -17,7 +17,7 @@ def requirements_from_file(filename="requirements.txt"):
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
setup(
|
|
20
|
-
version="1.0.9.
|
|
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
|
|
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
|
|
450
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
330
|
-
"""
|
|
353
|
+
async def _handleDeviceLoginChallenge(self, login_result):
|
|
354
|
+
"""Handle device login challenge.
|
|
331
355
|
|
|
332
|
-
|
|
333
|
-
|
|
356
|
+
Args:
|
|
357
|
+
login_result (dict): Result from initial login with DEVICE_SRP_AUTH challenge.
|
|
334
358
|
|
|
335
359
|
Returns:
|
|
336
|
-
dict: result
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
"
|
|
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
|
|
363
|
-
"""Login to hive account
|
|
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("
|
|
413
|
+
_LOGGER.error("2FA failed: authentication not initialised.")
|
|
375
414
|
raise HiveUnknownConfiguration
|
|
376
415
|
|
|
377
|
-
_LOGGER.debug("
|
|
416
|
+
_LOGGER.debug("sms_2fa - Submitting 2FA code.")
|
|
378
417
|
try:
|
|
379
|
-
result = await self.auth.
|
|
380
|
-
except
|
|
381
|
-
_LOGGER.error("
|
|
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
|
-
"
|
|
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
|
|
394
|
-
"""Attempt
|
|
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
|
-
"
|
|
452
|
+
"_retryLogin - Retrying login in %s seconds.", delay_s
|
|
406
453
|
)
|
|
407
454
|
await asyncio.sleep(delay_s)
|
|
408
|
-
await self.
|
|
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
|
|
469
|
+
except (HiveInvalidUsername, HiveInvalidPassword):
|
|
412
470
|
_LOGGER.error(
|
|
413
|
-
"
|
|
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("
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
709
|
+
await self._retryLogin()
|
|
652
710
|
last_auth_err = None
|
|
653
711
|
for api_retry_delay in (0, 5, 10):
|
|
654
712
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/hive_exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyhive_integration-1.0.9.dev2 → pyhive_integration-1.0.9.dev3}/src/helper/hivedataclasses.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|