pyhive-integration 1.0.8.dev0__tar.gz → 1.0.8.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.8.dev0/pyhive_integration.egg-info → pyhive_integration-1.0.8.dev3}/PKG-INFO +1 -1
  2. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3/pyhive_integration.egg-info}/PKG-INFO +1 -1
  3. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/setup.py +1 -1
  4. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/api/hive_async_api.py +27 -20
  5. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/helper/hive_exceptions.py +8 -0
  6. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/session.py +40 -8
  7. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/LICENSE +0 -0
  8. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/MANIFEST.in +0 -0
  9. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/README.md +0 -0
  10. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/pyhive_integration.egg-info/SOURCES.txt +0 -0
  11. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/pyhive_integration.egg-info/dependency_links.txt +0 -0
  12. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/pyhive_integration.egg-info/requires.txt +0 -0
  13. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/pyhive_integration.egg-info/top_level.txt +0 -0
  14. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/pyproject.toml +0 -0
  15. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/requirements.txt +0 -0
  16. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/requirements_test.txt +0 -0
  17. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/setup.cfg +0 -0
  18. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/__init__.py +0 -0
  19. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/action.py +0 -0
  20. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/alarm.py +0 -0
  21. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/api/__init__.py +0 -0
  22. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/api/hive_api.py +0 -0
  23. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/api/hive_auth.py +0 -0
  24. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/api/hive_auth_async.py +0 -0
  25. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/camera.py +0 -0
  26. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/device_attributes.py +0 -0
  27. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/heating.py +0 -0
  28. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/helper/__init__.py +0 -0
  29. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/helper/const.py +0 -0
  30. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/helper/debugger.py +0 -0
  31. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/helper/hive_helper.py +0 -0
  32. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/helper/hivedataclasses.py +0 -0
  33. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/helper/map.py +0 -0
  34. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/hive.py +0 -0
  35. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/hotwater.py +0 -0
  36. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/hub.py +0 -0
  37. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/light.py +0 -0
  38. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/plug.py +0 -0
  39. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.dev3}/src/sensor.py +0 -0
  40. {pyhive_integration-1.0.8.dev0 → pyhive_integration-1.0.8.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.8.dev0
3
+ Version: 1.0.8.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.8.dev0
3
+ Version: 1.0.8.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.8_dev",
20
+ version="1.0.8.dev3",
21
21
  packages=["apyhiveapi", "apyhiveapi.api", "apyhiveapi.helper"],
22
22
  package_dir={"apyhiveapi": "src"},
23
23
  package_data={"data": ["*.json"]},
@@ -10,8 +10,8 @@ import urllib3
10
10
  from aiohttp import ClientResponse, ClientSession, web_exceptions
11
11
  from pyquery import PyQuery
12
12
 
13
- from ..helper.const import HTTP_UNAUTHORIZED
14
- from ..helper.hive_exceptions import FileInUse, HiveApiError, NoApiToken
13
+ from ..helper.const import HTTP_FORBIDDEN, HTTP_UNAUTHORIZED
14
+ from ..helper.hive_exceptions import FileInUse, HiveApiError, HiveAuthError, NoApiToken
15
15
 
16
16
  _LOGGER = logging.getLogger(__name__)
17
17
 
@@ -56,45 +56,52 @@ class HiveApiAsync:
56
56
  _LOGGER.debug("API %s request to %s", method.upper(), url)
57
57
  data = kwargs.get("data", None)
58
58
 
59
+ headers = {
60
+ "content-type": "application/json",
61
+ "Accept": "*/*",
62
+ "User-Agent": "Hive/12.04.0 iOS/18.3.1 Apple",
63
+ }
59
64
  try:
60
65
  if camera:
61
- headers = {
62
- "content-type": "application/json",
63
- "Accept": "*/*",
64
- "Authorization": f"Bearer {self.session.tokens.tokenData['token']}",
65
- "x-jwt-token": self.session.tokens.tokenData["token"],
66
- "User-Agent": "Hive/12.04.0 iOS/18.3.1 Apple",
67
- }
66
+ headers["Authorization"] = (
67
+ f"Bearer {self.session.tokens.tokenData['token']}"
68
+ )
69
+ headers["x-jwt-token"] = self.session.tokens.tokenData["token"]
68
70
  else:
69
- headers = {
70
- "content-type": "application/json",
71
- "Accept": "*/*",
72
- "Authorization": self.session.tokens.tokenData["token"],
73
- "User-Agent": "Hive/12.04.0 iOS/18.3.1 Apple",
74
- }
71
+ headers["Authorization"] = self.session.tokens.tokenData["token"]
75
72
  except KeyError:
76
73
  if "sso" in url:
77
74
  pass
78
75
  else:
79
76
  raise NoApiToken
80
77
 
78
+ auth_token = headers.get("Authorization", "")
79
+ _LOGGER.debug(
80
+ "Using token (len=%d, tail=…%s)",
81
+ len(auth_token),
82
+ auth_token[-4:] if len(auth_token) >= 4 else auth_token,
83
+ )
84
+
81
85
  async with self.websession.request(
82
86
  method, url, headers=headers, data=data
83
87
  ) as resp:
84
- await resp.text()
88
+ resp_body = await resp.text()
85
89
  if str(resp.status).startswith("20"):
86
90
  _LOGGER.debug("API response %s from %s", resp.status, url)
87
91
  return resp
88
92
 
89
- if resp.status == HTTP_UNAUTHORIZED:
93
+ if resp.status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
90
94
  _LOGGER.error(
91
- f"Hive token has expired when calling {url} - "
92
- f"HTTP status is - {resp.status}"
95
+ f"Hive token rejected calling {url} - "
96
+ f"HTTP {resp.status} — response: {resp_body[:200]}"
97
+ )
98
+ raise HiveAuthError(
99
+ f"Token expired or forbidden calling {url} — HTTP {resp.status}"
93
100
  )
94
101
  elif url is not None and resp.status is not None:
95
102
  _LOGGER.error(
96
103
  f"Something has gone wrong calling {url} - "
97
- f"HTTP status is - {resp.status}"
104
+ f"HTTP status is - {resp.status} — response: {resp_body[:200]}"
98
105
  )
99
106
 
100
107
  raise HiveApiError
@@ -27,6 +27,14 @@ class HiveApiError(Exception):
27
27
  """
28
28
 
29
29
 
30
+ class HiveAuthError(HiveApiError):
31
+ """Auth error (401/403) — token may be expired or invalid.
32
+
33
+ Args:
34
+ HiveApiError (object): Parent API error class
35
+ """
36
+
37
+
30
38
  class HiveRefreshTokenExpired(Exception):
31
39
  """Refresh token expired.
32
40
 
@@ -17,6 +17,7 @@ from .device_attributes import HiveAttributes
17
17
  from .helper.const import ACTIONS, DEVICES, HIVE_TYPES, PRODUCTS
18
18
  from .helper.hive_exceptions import (
19
19
  HiveApiError,
20
+ HiveAuthError,
20
21
  HiveFailedToRefreshTokens,
21
22
  HiveInvalid2FACode,
22
23
  HiveInvalidDeviceAuthentication,
@@ -73,7 +74,7 @@ class HiveSession:
73
74
  self.tokens = Map(
74
75
  {
75
76
  "tokenData": {},
76
- "tokenCreated": datetime.now() - timedelta(seconds=4000),
77
+ "tokenCreated": datetime.min,
77
78
  "tokenExpiry": timedelta(seconds=3600),
78
79
  }
79
80
  )
@@ -356,6 +357,11 @@ class HiveSession:
356
357
  else:
357
358
  expiry_time = self.tokens.tokenCreated + (self.tokens.tokenExpiry * 0.95)
358
359
  # Refresh at 95% of token lifetime to prevent expiration during API calls
360
+ _LOGGER.debug(
361
+ "Checking token expiry time ( Current: %s | Expiry: %s)",
362
+ datetime.now(),
363
+ expiry_time,
364
+ )
359
365
  if datetime.now() >= expiry_time:
360
366
  async with self._refreshLock:
361
367
  # Re-check after acquiring lock — another caller may have already refreshed
@@ -364,10 +370,14 @@ class HiveSession:
364
370
  )
365
371
  if datetime.now() < expiry_time:
366
372
  return result
373
+ actual_expiry = self.tokens.tokenCreated + self.tokens.tokenExpiry
367
374
  _LOGGER.debug(
368
- "Token nearing expiry (created: %s, expiry window: %s), refreshing.",
375
+ "Token created: %s | Actual expiry: %s | "
376
+ "Early refresh (×0.95): %s | Now: %s",
369
377
  self.tokens.tokenCreated,
370
- self.tokens.tokenExpiry,
378
+ actual_expiry,
379
+ expiry_time,
380
+ datetime.now(),
371
381
  )
372
382
  try:
373
383
  result = await self.auth.refresh_token(
@@ -407,12 +417,16 @@ class HiveSession:
407
417
  if datetime.now() < ep:
408
418
  return updated
409
419
  _LOGGER.debug("Polling Hive API for device updates.")
410
- await self.getDevices(device["hiveID"])
411
- if len(self.deviceList["camera"]) > 0:
420
+ updated = await self.getDevices(device["hiveID"])
421
+ if updated and len(self.deviceList["camera"]) > 0:
412
422
  for camera in self.data.camera:
413
423
  await self.getCamera(self.devices[camera])
414
- updated = True
415
- _LOGGER.debug("Device update completed successfully.")
424
+ if updated:
425
+ _LOGGER.debug("Device update completed successfully.")
426
+ else:
427
+ _LOGGER.debug(
428
+ "Device update failed, will retry after scan interval."
429
+ )
416
430
 
417
431
  return updated
418
432
 
@@ -505,7 +519,18 @@ class HiveSession:
505
519
  elif self.tokens is not None:
506
520
  await self.hiveRefreshTokens()
507
521
  _LOGGER.debug("Fetching all devices from Hive API.")
508
- api_resp_d = await self.api.getAll()
522
+ try:
523
+ api_resp_d = await self.api.getAll()
524
+ except HiveAuthError:
525
+ _LOGGER.warning(
526
+ "Auth error (401/403) after token refresh, "
527
+ "falling back to full device re-login."
528
+ )
529
+ await self._retryDeviceLogin()
530
+ try:
531
+ api_resp_d = await self.api.getAll()
532
+ except HiveAuthError as retry_err:
533
+ raise HiveReauthRequired from retry_err
509
534
  if operator.contains(str(api_resp_d["original"]), "20") is False:
510
535
  raise HTTPException
511
536
  elif api_resp_d["parsed"] is None:
@@ -551,6 +576,10 @@ class HiveSession:
551
576
  await self.getAlarm()
552
577
  self.config.lastUpdate = datetime.now()
553
578
  get_nodes_successful = True
579
+ except HiveReauthRequired:
580
+ _LOGGER.error("Reauthentication required, propagating to caller.")
581
+ self.config.lastUpdate = datetime.now()
582
+ raise
554
583
  except (
555
584
  OSError,
556
585
  RuntimeError,
@@ -559,6 +588,9 @@ class HiveSession:
559
588
  HTTPException,
560
589
  ) as err:
561
590
  _LOGGER.error("Failed to fetch devices: %s", err)
591
+ self.config.lastUpdate = (
592
+ datetime.now() - self.config.scanInterval + timedelta(seconds=30)
593
+ )
562
594
  get_nodes_successful = False
563
595
 
564
596
  return get_nodes_successful