pyhive-integration 1.0.8.dev3__tar.gz → 1.0.8.dev5__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.dev3/pyhive_integration.egg-info → pyhive_integration-1.0.8.dev5}/PKG-INFO +1 -1
  2. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5/pyhive_integration.egg-info}/PKG-INFO +1 -1
  3. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/setup.py +1 -1
  4. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/api/hive_async_api.py +17 -3
  5. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/api/hive_auth_async.py +1 -1
  6. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/heating.py +8 -0
  7. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/hotwater.py +8 -0
  8. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/light.py +8 -0
  9. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/plug.py +8 -0
  10. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/sensor.py +8 -0
  11. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/session.py +39 -13
  12. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/LICENSE +0 -0
  13. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/MANIFEST.in +0 -0
  14. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/README.md +0 -0
  15. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/pyhive_integration.egg-info/SOURCES.txt +0 -0
  16. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/pyhive_integration.egg-info/dependency_links.txt +0 -0
  17. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/pyhive_integration.egg-info/requires.txt +0 -0
  18. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/pyhive_integration.egg-info/top_level.txt +0 -0
  19. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/pyproject.toml +0 -0
  20. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/requirements.txt +0 -0
  21. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/requirements_test.txt +0 -0
  22. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/setup.cfg +0 -0
  23. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/__init__.py +0 -0
  24. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/action.py +0 -0
  25. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/alarm.py +0 -0
  26. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/api/__init__.py +0 -0
  27. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/api/hive_api.py +0 -0
  28. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/api/hive_auth.py +0 -0
  29. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/camera.py +0 -0
  30. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/device_attributes.py +0 -0
  31. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/helper/__init__.py +0 -0
  32. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/helper/const.py +0 -0
  33. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/helper/debugger.py +0 -0
  34. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/helper/hive_exceptions.py +0 -0
  35. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/helper/hive_helper.py +0 -0
  36. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/helper/hivedataclasses.py +0 -0
  37. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/helper/map.py +0 -0
  38. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/hive.py +0 -0
  39. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/src/hub.py +0 -0
  40. {pyhive_integration-1.0.8.dev3 → pyhive_integration-1.0.8.dev5}/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.dev3
3
+ Version: 1.0.8.dev5
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.dev3
3
+ Version: 1.0.8.dev5
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.dev3",
20
+ version="1.0.8.dev5",
21
21
  packages=["apyhiveapi", "apyhiveapi.api", "apyhiveapi.helper"],
22
22
  package_dir={"apyhiveapi": "src"},
23
23
  package_data={"data": ["*.json"]},
@@ -1,13 +1,15 @@
1
1
  """Hive API Module."""
2
2
 
3
3
  # pylint: skip-file
4
+ import asyncio
4
5
  import json
5
6
  import logging
7
+ import time
6
8
  from typing import Optional
7
9
 
8
10
  import requests
9
11
  import urllib3
10
- from aiohttp import ClientResponse, ClientSession, web_exceptions
12
+ from aiohttp import ClientResponse, ClientSession, ClientTimeout, web_exceptions
11
13
  from pyquery import PyQuery
12
14
 
13
15
  from ..helper.const import HTTP_FORBIDDEN, HTTP_UNAUTHORIZED
@@ -82,12 +84,21 @@ class HiveApiAsync:
82
84
  auth_token[-4:] if len(auth_token) >= 4 else auth_token,
83
85
  )
84
86
 
87
+ timeout = ClientTimeout(total=10)
88
+ req_start = time.monotonic()
85
89
  async with self.websession.request(
86
- method, url, headers=headers, data=data
90
+ method, url, headers=headers, data=data, timeout=timeout
87
91
  ) as resp:
88
92
  resp_body = await resp.text()
93
+ req_duration = time.monotonic() - req_start
94
+ _LOGGER.debug(
95
+ "API %s %s completed in %.2fs — HTTP %s",
96
+ method.upper(),
97
+ url,
98
+ req_duration,
99
+ resp.status,
100
+ )
89
101
  if str(resp.status).startswith("20"):
90
- _LOGGER.debug("API response %s from %s", resp.status, url)
91
102
  return resp
92
103
 
93
104
  if resp.status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
@@ -163,6 +174,9 @@ class HiveApiAsync:
163
174
  resp = await self.request("get", url)
164
175
  json_return.update({"original": resp.status})
165
176
  json_return.update({"parsed": await resp.json(content_type=None)})
177
+ except asyncio.TimeoutError:
178
+ _LOGGER.warning("Hive API request timed out fetching all nodes.")
179
+ raise
166
180
  except (OSError, RuntimeError, ZeroDivisionError):
167
181
  await self.error()
168
182
 
@@ -625,7 +625,7 @@ class HiveAuthAsync:
625
625
  _LOGGER.error("Token refresh failed: cannot reach Cognito endpoint.")
626
626
  raise HiveApiError from err
627
627
 
628
- _LOGGER.debug("Token refresh completed successfully.")
628
+ _LOGGER.debug("Cognito token refresh completed successfully.")
629
629
  return result
630
630
 
631
631
  async def forget_device(self, access_token, device_key):
@@ -495,6 +495,14 @@ class Climate(HiveHeating):
495
495
  await self.session.helper.errorCheck(
496
496
  device["device_id"], "ERROR", device["deviceData"]["online"]
497
497
  )
498
+ if self.session._lastPollSlow:
499
+ cached = self.session.devices.get(device["hiveID"])
500
+ if cached is not None:
501
+ _LOGGER.debug(
502
+ "Returning cached state for offline climate %s (slow poll).",
503
+ device["haName"],
504
+ )
505
+ return cached
498
506
  return device
499
507
 
500
508
  async def getScheduleNowNextLater(self, device: dict):
@@ -263,6 +263,14 @@ class WaterHeater(HiveHotwater):
263
263
  await self.session.helper.errorCheck(
264
264
  device["device_id"], "ERROR", device["deviceData"]["online"]
265
265
  )
266
+ if self.session._lastPollSlow:
267
+ cached = self.session.devices.get(device["hiveID"])
268
+ if cached is not None:
269
+ _LOGGER.debug(
270
+ "Returning cached state for offline water heater %s (slow poll).",
271
+ device["haName"],
272
+ )
273
+ return cached
266
274
  return device
267
275
 
268
276
  async def getScheduleNowNextLater(self, device: dict):
@@ -420,6 +420,14 @@ class Light(HiveLight):
420
420
  await self.session.helper.errorCheck(
421
421
  device["device_id"], "ERROR", device["deviceData"]["online"]
422
422
  )
423
+ if self.session._lastPollSlow:
424
+ cached = self.session.devices.get(device["hiveID"])
425
+ if cached is not None:
426
+ _LOGGER.debug(
427
+ "Returning cached state for light %s (slow poll).",
428
+ device["haName"],
429
+ )
430
+ return cached
423
431
  return device
424
432
 
425
433
  async def turnOn(self, device: dict, brightness: int, color_temp: int, color: list):
@@ -180,6 +180,14 @@ class Switch(HiveSmartPlug):
180
180
  await self.session.helper.errorCheck(
181
181
  device["device_id"], "ERROR", device["deviceData"]["online"]
182
182
  )
183
+ if self.session._lastPollSlow:
184
+ cached = self.session.devices.get(device["hiveID"])
185
+ if cached is not None:
186
+ _LOGGER.debug(
187
+ "Returning cached state for offline switch %s (slow poll).",
188
+ device["haName"],
189
+ )
190
+ return cached
183
191
  return device
184
192
 
185
193
  async def getSwitchState(self, device: dict):
@@ -152,4 +152,12 @@ class Sensor(HiveSensor):
152
152
  await self.session.helper.errorCheck(
153
153
  device["device_id"], "ERROR", device["deviceData"]["online"]
154
154
  )
155
+ if self.session._lastPollSlow:
156
+ cached = self.session.devices.get(device["hiveID"])
157
+ if cached is not None:
158
+ _LOGGER.debug(
159
+ "Returning cached state for offline sensor %s (slow poll).",
160
+ device["haName"],
161
+ )
162
+ return cached
155
163
  return device
@@ -107,6 +107,7 @@ class HiveSession:
107
107
  self.devices = {}
108
108
  self.deviceList = {}
109
109
  self.hub_id = None
110
+ self._lastPollSlow = False
110
111
 
111
112
  def openFile(self, file: str):
112
113
  """Open a file.
@@ -358,7 +359,7 @@ class HiveSession:
358
359
  expiry_time = self.tokens.tokenCreated + (self.tokens.tokenExpiry * 0.95)
359
360
  # Refresh at 95% of token lifetime to prevent expiration during API calls
360
361
  _LOGGER.debug(
361
- "Checking token expiry time ( Current: %s | Expiry: %s)",
362
+ "Session token expiry time ( Current: %s | Expiry: %s)",
362
363
  datetime.now(),
363
364
  expiry_time,
364
365
  )
@@ -372,7 +373,7 @@ class HiveSession:
372
373
  return result
373
374
  actual_expiry = self.tokens.tokenCreated + self.tokens.tokenExpiry
374
375
  _LOGGER.debug(
375
- "Token created: %s | Actual expiry: %s | "
376
+ "Session Token created: %s | Actual expiry: %s | "
376
377
  "Early refresh (×0.95): %s | Now: %s",
377
378
  self.tokens.tokenCreated,
378
379
  actual_expiry,
@@ -385,11 +386,17 @@ class HiveSession:
385
386
  )
386
387
 
387
388
  if result and "AuthenticationResult" in result:
388
- _LOGGER.debug("Token refresh successful.")
389
389
  await self.updateTokens(result)
390
+ new_expiry = (
391
+ self.tokens.tokenCreated + self.tokens.tokenExpiry
392
+ )
393
+ _LOGGER.debug(
394
+ "Session Token refresh successful. New expiry: %s",
395
+ new_expiry,
396
+ )
390
397
  except (HiveRefreshTokenExpired, HiveFailedToRefreshTokens) as exc:
391
398
  _LOGGER.warning(
392
- "Token refresh failed (%s), falling back to device login.",
399
+ "Session Token refresh failed (%s), falling back to device login.",
393
400
  type(exc).__name__,
394
401
  )
395
402
  await self._retryDeviceLogin()
@@ -519,6 +526,7 @@ class HiveSession:
519
526
  elif self.tokens is not None:
520
527
  await self.hiveRefreshTokens()
521
528
  _LOGGER.debug("Fetching all devices from Hive API.")
529
+ api_call_start = time.monotonic()
522
530
  try:
523
531
  api_resp_d = await self.api.getAll()
524
532
  except HiveAuthError:
@@ -531,6 +539,15 @@ class HiveSession:
531
539
  api_resp_d = await self.api.getAll()
532
540
  except HiveAuthError as retry_err:
533
541
  raise HiveReauthRequired from retry_err
542
+ api_call_duration = time.monotonic() - api_call_start
543
+ if api_call_duration > 8:
544
+ _LOGGER.warning(
545
+ "Hive API response took %.1fs — marking poll as slow.",
546
+ api_call_duration,
547
+ )
548
+ self._lastPollSlow = True
549
+ else:
550
+ self._lastPollSlow = False
534
551
  if operator.contains(str(api_resp_d["original"]), "20") is False:
535
552
  raise HTTPException
536
553
  elif api_resp_d["parsed"] is None:
@@ -580,6 +597,13 @@ class HiveSession:
580
597
  _LOGGER.error("Reauthentication required, propagating to caller.")
581
598
  self.config.lastUpdate = datetime.now()
582
599
  raise
600
+ except asyncio.TimeoutError:
601
+ _LOGGER.warning("Hive API request timed out — keeping cached device data.")
602
+ self._lastPollSlow = True
603
+ self.config.lastUpdate = (
604
+ datetime.now() - self.config.scanInterval + timedelta(seconds=30)
605
+ )
606
+ get_nodes_successful = False
583
607
  except (
584
608
  OSError,
585
609
  RuntimeError,
@@ -682,15 +706,6 @@ class HiveSession:
682
706
  a = self.data["actions"][action] # noqa: F841
683
707
  eval("self." + ACTIONS)
684
708
 
685
- _LOGGER.debug(
686
- "Device discovery found: %d climate, %d light, %d switch, %d sensor, %d camera, %d alarm.",
687
- len(self.deviceList["climate"]),
688
- len(self.deviceList["light"]),
689
- len(self.deviceList["switch"]),
690
- len(self.deviceList["sensor"]),
691
- len(self.deviceList["camera"]),
692
- len(self.deviceList["alarm_control_panel"]),
693
- )
694
709
  hive_type = HIVE_TYPES["Heating"] + HIVE_TYPES["Switch"] + HIVE_TYPES["Light"]
695
710
  for aProduct in self.data.products:
696
711
  p = self.data.products[aProduct]
@@ -713,6 +728,17 @@ class HiveSession:
713
728
  if self.data.products[aProduct]["type"] in hive_type:
714
729
  self.config.mode.append(p["id"])
715
730
 
731
+ _LOGGER.debug(
732
+ "Device discovery found: %d parent, %d binary_sensor, %d climate, %d light, %d sensor, %d switch, %d water_heater",
733
+ len(self.deviceList.get("parent", [])),
734
+ len(self.deviceList.get("binary_sensor", [])),
735
+ len(self.deviceList.get("climate", [])),
736
+ len(self.deviceList.get("light", [])),
737
+ len(self.deviceList.get("sensor", [])),
738
+ len(self.deviceList.get("switch", [])),
739
+ len(self.deviceList.get("water_heater", [])),
740
+ )
741
+
716
742
  return self.deviceList
717
743
 
718
744
  @staticmethod