blebox-uniapi 2.5.9__tar.gz → 2.5.10__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 (51) hide show
  1. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/CHANGELOG.md +7 -0
  2. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/PKG-INFO +9 -2
  3. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/__init__.py +1 -1
  4. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/session.py +45 -11
  5. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi.egg-info/PKG-INFO +9 -2
  6. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi.egg-info/requires.txt +1 -1
  7. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/setup.py +2 -2
  8. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_session.py +86 -4
  9. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/CONTRIBUTING.rst +0 -0
  10. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/LICENSE +0 -0
  11. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/MANIFEST.in +0 -0
  12. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/README.md +0 -0
  13. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/binary_sensor.py +0 -0
  14. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/box.py +0 -0
  15. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/box_types.py +0 -0
  16. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/button.py +0 -0
  17. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/climate.py +0 -0
  18. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/cover.py +0 -0
  19. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/error.py +0 -0
  20. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/feature.py +0 -0
  21. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/jfollow.py +0 -0
  22. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/light.py +0 -0
  23. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/sensor.py +0 -0
  24. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/switch.py +0 -0
  25. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi/update.py +0 -0
  26. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi.egg-info/SOURCES.txt +0 -0
  27. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
  28. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi.egg-info/not-zip-safe +0 -0
  29. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/blebox_uniapi.egg-info/top_level.txt +0 -0
  30. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/Makefile +0 -0
  31. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/authors.rst +0 -0
  32. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/conf.py +0 -0
  33. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/contributing.rst +0 -0
  34. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/history.rst +0 -0
  35. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/index.rst +0 -0
  36. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/installation.rst +0 -0
  37. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/make.bat +0 -0
  38. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/readme.rst +0 -0
  39. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/docs/usage.rst +0 -0
  40. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/setup.cfg +0 -0
  41. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_box.py +0 -0
  42. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_box_types.py +0 -0
  43. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_button.py +0 -0
  44. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_climate.py +0 -0
  45. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_cover.py +0 -0
  46. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_light.py +0 -0
  47. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_light_unit.py +0 -0
  48. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_sensor.py +0 -0
  49. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_switch.py +0 -0
  50. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_unknown.py +0 -0
  51. {blebox_uniapi-2.5.9 → blebox_uniapi-2.5.10}/tests/test_update.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.10](https://github.com/blebox/blebox_uniapi/compare/v2.5.9...v2.5.10) (2026-11-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * retry once when device connection times out ([#208](https://github.com/blebox/blebox_uniapi/issues/208)) ([16e34cf](https://github.com/blebox/blebox_uniapi/commit/16e34cf62d25502aab9922db82d78237e08bd53b))
9
+
3
10
  ## [2.5.9](https://github.com/blebox/blebox_uniapi/compare/v2.5.8...v2.5.9) (2026-10-18)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blebox_uniapi
3
- Version: 2.5.9
3
+ Version: 2.5.10
4
4
  Summary: Python API for accessing BleBox smart home devices
5
5
  Home-page: https://github.com/blebox/blebox_uniapi
6
6
  Author: BleBox
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
17
17
  Requires-Python: >=3.9
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: aiohttp>=3
20
+ Requires-Dist: aiohttp>=3.10
21
21
  Requires-Dist: jmespath>1.0.0
22
22
  Dynamic: author
23
23
  Dynamic: author-email
@@ -61,6 +61,13 @@ Contributions are welcome!
61
61
 
62
62
  # Changelog
63
63
 
64
+ ## [2.5.10](https://github.com/blebox/blebox_uniapi/compare/v2.5.9...v2.5.10) (2026-11-09)
65
+
66
+
67
+ ### Bug Fixes
68
+
69
+ * retry once when device connection times out ([#208](https://github.com/blebox/blebox_uniapi/issues/208)) ([16e34cf](https://github.com/blebox/blebox_uniapi/commit/16e34cf62d25502aab9922db82d78237e08bd53b))
70
+
64
71
  ## [2.5.9](https://github.com/blebox/blebox_uniapi/compare/v2.5.8...v2.5.9) (2026-10-18)
65
72
 
66
73
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  __author__ = """BleBox"""
4
4
  __email__ = "opensource@blebox.eu"
5
- __version__ = "2.5.9"
5
+ __version__ = "2.5.10"
@@ -6,18 +6,32 @@ import logging
6
6
 
7
7
  from . import error
8
8
 
9
- DEFAULT_TIMEOUT = aiohttp.ClientTimeout(total=None, sock_connect=5, sock_read=5)
9
+ CONNECT_TIMEOUT = 3
10
+ CONNECT_ATTEMPTS = 2
11
+ DEFAULT_TIMEOUT = aiohttp.ClientTimeout(
12
+ total=None, sock_connect=CONNECT_TIMEOUT, sock_read=5
13
+ )
10
14
  DEFAULT_PORT = 80
11
15
 
12
16
  logger = logging.getLogger(__name__)
13
17
 
14
18
 
19
+ def _client_timeout(
20
+ timeout: Union[int, aiohttp.ClientTimeout, None],
21
+ ) -> aiohttp.ClientTimeout:
22
+ if not timeout:
23
+ return DEFAULT_TIMEOUT
24
+ if isinstance(timeout, aiohttp.ClientTimeout):
25
+ return timeout
26
+ return aiohttp.ClientTimeout(total=timeout, sock_connect=CONNECT_TIMEOUT)
27
+
28
+
15
29
  class ApiHost:
16
30
  def __init__(
17
31
  self,
18
32
  host: str,
19
33
  port: int,
20
- timeout: int,
34
+ timeout: Union[int, aiohttp.ClientTimeout, None],
21
35
  session: Any,
22
36
  loop: Any,
23
37
  logger: logging.Logger = logger,
@@ -30,7 +44,7 @@ class ApiHost:
30
44
  # TODO: handle empty logger?
31
45
  self._logger = logger
32
46
 
33
- self._timeout = timeout if timeout else DEFAULT_TIMEOUT
47
+ self._timeout = _client_timeout(timeout)
34
48
 
35
49
  self._session = session
36
50
 
@@ -40,7 +54,9 @@ class ApiHost:
40
54
  auth = aiohttp.BasicAuth(login=self._username, password=self._password)
41
55
 
42
56
  if not self._session:
43
- self._session = aiohttp.ClientSession(loop=loop, timeout=timeout, auth=auth)
57
+ self._session = aiohttp.ClientSession(
58
+ loop=loop, timeout=self._timeout, auth=auth
59
+ )
44
60
 
45
61
  # TODO: remove?
46
62
  self._loop = loop
@@ -52,14 +68,13 @@ class ApiHost:
52
68
  data: Union[dict, str, None] = None,
53
69
  allow_ota_check_response: bool = False,
54
70
  ) -> Optional[dict]:
55
- # TODO: check timeout
56
71
  client_timeout = self._timeout
57
72
  url = self.api_path(path)
73
+ kwargs: dict[str, Any] = {"timeout": client_timeout}
74
+ if data is not None:
75
+ kwargs["data"] = data
58
76
  try:
59
- if data is not None:
60
- response = await async_method(url, timeout=client_timeout, data=data)
61
- else:
62
- response = await async_method(url, timeout=client_timeout)
77
+ response = await self._async_send(async_method, url, **kwargs)
63
78
 
64
79
  accepted_statuses = (200, 202, 204) if allow_ota_check_response else (200,)
65
80
  if response.status not in accepted_statuses:
@@ -76,11 +91,16 @@ class ApiHost:
76
91
 
77
92
  return await response.json()
78
93
 
79
- except asyncio.TimeoutError as ex:
94
+ except aiohttp.ConnectionTimeoutError as ex:
95
+ connect_timeout = client_timeout.sock_connect or client_timeout.connect
80
96
  raise error.TimeoutError(
81
- f"Failed to connect to {self.host}:{self.port} within {client_timeout}s: ({ex})"
97
+ f"Failed to connect to {self.host}:{self.port} within "
98
+ f"{connect_timeout}s ({CONNECT_ATTEMPTS} attempts)"
82
99
  ) from ex
83
100
 
101
+ except asyncio.TimeoutError as ex:
102
+ raise error.TimeoutError(f"Request to {url} timed out") from ex
103
+
84
104
  except aiohttp.ClientConnectionError as ex:
85
105
  raise error.ConnectionError(
86
106
  f"Failed to connect to {self.host}:{self.port}: {ex}"
@@ -94,6 +114,20 @@ class ApiHost:
94
114
  f"Invalid response encoding from {url}: {ex}"
95
115
  ) from ex
96
116
 
117
+ async def _async_send(self, async_method: Any, url: str, **kwargs: Any) -> Any:
118
+ # Nothing was sent yet, so retrying on a new connection is safe.
119
+ for attempt in range(1, CONNECT_ATTEMPTS):
120
+ try:
121
+ return await async_method(url, **kwargs)
122
+ except aiohttp.ConnectionTimeoutError:
123
+ self._logger.debug(
124
+ "Connection to %s timed out, retrying (attempt %s/%s)",
125
+ url,
126
+ attempt + 1,
127
+ CONNECT_ATTEMPTS,
128
+ )
129
+ return await async_method(url, **kwargs)
130
+
97
131
  async def async_api_get(self, path: str) -> Optional[dict]:
98
132
  try:
99
133
  return await self.async_request(path, self._session.get)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blebox_uniapi
3
- Version: 2.5.9
3
+ Version: 2.5.10
4
4
  Summary: Python API for accessing BleBox smart home devices
5
5
  Home-page: https://github.com/blebox/blebox_uniapi
6
6
  Author: BleBox
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
17
17
  Requires-Python: >=3.9
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: aiohttp>=3
20
+ Requires-Dist: aiohttp>=3.10
21
21
  Requires-Dist: jmespath>1.0.0
22
22
  Dynamic: author
23
23
  Dynamic: author-email
@@ -61,6 +61,13 @@ Contributions are welcome!
61
61
 
62
62
  # Changelog
63
63
 
64
+ ## [2.5.10](https://github.com/blebox/blebox_uniapi/compare/v2.5.9...v2.5.10) (2026-11-09)
65
+
66
+
67
+ ### Bug Fixes
68
+
69
+ * retry once when device connection times out ([#208](https://github.com/blebox/blebox_uniapi/issues/208)) ([16e34cf](https://github.com/blebox/blebox_uniapi/commit/16e34cf62d25502aab9922db82d78237e08bd53b))
70
+
64
71
  ## [2.5.9](https://github.com/blebox/blebox_uniapi/compare/v2.5.8...v2.5.9) (2026-10-18)
65
72
 
66
73
 
@@ -1,2 +1,2 @@
1
- aiohttp>=3
1
+ aiohttp>=3.10
2
2
  jmespath>1.0.0
@@ -10,7 +10,7 @@ with open("README.md") as readme_file:
10
10
  with open("CHANGELOG.md") as changelog_file:
11
11
  changelog = changelog_file.read()
12
12
 
13
- requirements = ["aiohttp>=3", "jmespath>1.0.0"]
13
+ requirements = ["aiohttp>=3.10", "jmespath>1.0.0"]
14
14
 
15
15
  setup_requirements = [
16
16
  "pytest-runner",
@@ -23,7 +23,7 @@ test_requirements = [
23
23
  ]
24
24
 
25
25
  setup(
26
- version="2.5.9",
26
+ version="2.5.10",
27
27
  author="BleBox",
28
28
  author_email="opensource@blebox.eu",
29
29
  python_requires=">=3.9",
@@ -6,7 +6,7 @@ import aiohttp
6
6
 
7
7
  from unittest.mock import patch, Mock, AsyncMock
8
8
 
9
- from blebox_uniapi.session import ApiHost as Session
9
+ from blebox_uniapi.session import CONNECT_TIMEOUT, ApiHost as Session
10
10
  from blebox_uniapi import error
11
11
 
12
12
 
@@ -59,7 +59,10 @@ async def test_session_api_get(logger, client):
59
59
 
60
60
  result = await api_session.async_api_get("/api/foo")
61
61
 
62
- client.get.assert_called_once_with("http://127.0.0.4:88/api/foo", timeout=2)
62
+ client.get.assert_called_once_with(
63
+ "http://127.0.0.4:88/api/foo",
64
+ timeout=aiohttp.ClientTimeout(total=2, sock_connect=CONNECT_TIMEOUT),
65
+ )
63
66
 
64
67
  assert result == 123
65
68
 
@@ -70,7 +73,10 @@ async def test_session_default_client_created(mocked_client, logger):
70
73
 
71
74
  result = await api_session.async_api_get("/api/foo")
72
75
 
73
- mocked_client.get.assert_called_once_with("http://127.0.0.4:88/api/foo", timeout=2)
76
+ mocked_client.get.assert_called_once_with(
77
+ "http://127.0.0.4:88/api/foo",
78
+ timeout=aiohttp.ClientTimeout(total=2, sock_connect=CONNECT_TIMEOUT),
79
+ )
74
80
  assert result == 123
75
81
 
76
82
 
@@ -80,7 +86,7 @@ async def test_session_default_timeout_used(mocked_client, logger):
80
86
 
81
87
  await api_session.async_api_get("/api/foo")
82
88
  expected_timeout = aiohttp.ClientTimeout(
83
- total=None, connect=None, sock_read=5, sock_connect=5
89
+ total=None, connect=None, sock_read=5, sock_connect=CONNECT_TIMEOUT
84
90
  )
85
91
 
86
92
  mocked_client.get.assert_called_once_with(
@@ -231,3 +237,79 @@ async def test_session_api_get_unicode_decode_error_raises_connection_error(
231
237
  api_session = Session("127.0.0.4", "88", 2, client, None, logger)
232
238
  with pytest.raises(error.ConnectionError, match="Invalid response encoding"):
233
239
  await api_session.async_api_get("/api/device/state")
240
+
241
+
242
+ async def test_session_retries_after_connection_timeout(logger, client):
243
+ client.get = AsyncMock(
244
+ side_effect=[aiohttp.ConnectionTimeoutError(), valid_response()]
245
+ )
246
+ api_session = Session("127.0.0.4", "88", 2, client, None, logger)
247
+
248
+ result = await api_session.async_api_get("/api/foo")
249
+
250
+ assert result == 123
251
+ assert client.get.call_count == 2
252
+
253
+
254
+ async def test_session_post_retries_after_connection_timeout(logger, client):
255
+ client.post = AsyncMock(
256
+ side_effect=[aiohttp.ConnectionTimeoutError(), valid_response()]
257
+ )
258
+ api_session = Session("127.0.0.4", "88", 2, client, None, logger)
259
+
260
+ result = await api_session.async_api_post("/api/foo", {"a": 1})
261
+
262
+ assert result == 123
263
+ assert client.post.call_count == 2
264
+ client.post.assert_called_with(
265
+ "http://127.0.0.4:88/api/foo",
266
+ timeout=aiohttp.ClientTimeout(total=2, sock_connect=CONNECT_TIMEOUT),
267
+ data={"a": 1},
268
+ )
269
+
270
+
271
+ async def test_session_connection_timeout_after_all_attempts(logger, client):
272
+ client.get = AsyncMock(side_effect=aiohttp.ConnectionTimeoutError())
273
+ api_session = Session("127.0.0.4", "88", 2, client, None, logger)
274
+
275
+ with pytest.raises(
276
+ error.TimeoutError,
277
+ match=rf"Failed to connect to 127\.0\.0\.4:88 within {CONNECT_TIMEOUT}s",
278
+ ):
279
+ await api_session.async_api_get("/api/foo")
280
+
281
+ assert client.get.call_count == 2
282
+
283
+
284
+ async def test_session_read_timeout_is_not_retried(logger, client):
285
+ client.get = AsyncMock(side_effect=aiohttp.SocketTimeoutError())
286
+ api_session = Session("127.0.0.4", "88", 2, client, None, logger)
287
+
288
+ with pytest.raises(
289
+ error.TimeoutError,
290
+ match=r"Request to http://127\.0\.0\.4:88/api/foo timed out",
291
+ ):
292
+ await api_session.async_api_get("/api/foo")
293
+
294
+ client.get.assert_called_once()
295
+
296
+
297
+ async def test_session_client_timeout_passed_through(logger, client):
298
+ client.get = AsyncMock(return_value=valid_response())
299
+ timeout = aiohttp.ClientTimeout(total=7, sock_connect=1)
300
+ api_session = Session("127.0.0.4", "88", timeout, client, None, logger)
301
+
302
+ await api_session.async_api_get("/api/foo")
303
+
304
+ client.get.assert_called_once_with("http://127.0.0.4:88/api/foo", timeout=timeout)
305
+
306
+
307
+ async def test_session_connection_timeout_message_uses_connect(logger, client):
308
+ client.get = AsyncMock(side_effect=aiohttp.ConnectionTimeoutError())
309
+ timeout = aiohttp.ClientTimeout(total=10, connect=4)
310
+ api_session = Session("127.0.0.4", "88", timeout, client, None, logger)
311
+
312
+ with pytest.raises(
313
+ error.TimeoutError, match=r"Failed to connect to 127\.0\.0\.4:88 within 4s"
314
+ ):
315
+ await api_session.async_api_get("/api/foo")
File without changes
File without changes
File without changes