daikin-onecta 0.1.0__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.
- daikin_onecta-0.1.0/PKG-INFO +58 -0
- daikin_onecta-0.1.0/README.md +31 -0
- daikin_onecta-0.1.0/pyproject.toml +53 -0
- daikin_onecta-0.1.0/setup.cfg +4 -0
- daikin_onecta-0.1.0/src/daikin_onecta/__init__.py +23 -0
- daikin_onecta-0.1.0/src/daikin_onecta/client.py +138 -0
- daikin_onecta-0.1.0/src/daikin_onecta/exceptions.py +32 -0
- daikin_onecta-0.1.0/src/daikin_onecta/models.py +5 -0
- daikin_onecta-0.1.0/src/daikin_onecta/rate_limit.py +39 -0
- daikin_onecta-0.1.0/src/daikin_onecta.egg-info/PKG-INFO +58 -0
- daikin_onecta-0.1.0/src/daikin_onecta.egg-info/SOURCES.txt +14 -0
- daikin_onecta-0.1.0/src/daikin_onecta.egg-info/dependency_links.txt +1 -0
- daikin_onecta-0.1.0/src/daikin_onecta.egg-info/requires.txt +12 -0
- daikin_onecta-0.1.0/src/daikin_onecta.egg-info/top_level.txt +1 -0
- daikin_onecta-0.1.0/tests/test_client.py +82 -0
- daikin_onecta-0.1.0/tests/test_rate_limit.py +32 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: daikin-onecta
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Python client for the Daikin Onecta cloud API
|
|
5
|
+
Author: Johnny Willemsen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jwillemsen/daikin-onecta
|
|
8
|
+
Project-URL: Repository, https://github.com/jwillemsen/daikin-onecta
|
|
9
|
+
Project-URL: Issues, https://github.com/jwillemsen/daikin-onecta/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: aiohttp<3.14; extra == "test"
|
|
20
|
+
Requires-Dist: aioresponses>=0.7.8; extra == "test"
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
|
23
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: daikin-onecta[test]; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.13; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# daikin-onecta
|
|
29
|
+
|
|
30
|
+
Async Python client for the Daikin Onecta cloud API.
|
|
31
|
+
|
|
32
|
+
This library contains the Daikin-specific API layer used by the Home Assistant
|
|
33
|
+
Daikin Onecta integration. It deliberately has no dependency on Home Assistant.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import aiohttp
|
|
39
|
+
|
|
40
|
+
from daikin_onecta import OnectaClient
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def token_provider() -> str:
|
|
44
|
+
return "access-token"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async with aiohttp.ClientSession() as session:
|
|
48
|
+
client = OnectaClient(session, token_provider)
|
|
49
|
+
devices = await client.get_gateway_devices()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Authentication is supplied by an asynchronous token callback so applications
|
|
53
|
+
remain responsible for their own OAuth flow and token refresh.
|
|
54
|
+
|
|
55
|
+
## Status
|
|
56
|
+
|
|
57
|
+
The API is currently alpha. The first release intentionally exposes gateway
|
|
58
|
+
devices as raw dictionaries while the stable typed device model is developed.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# daikin-onecta
|
|
2
|
+
|
|
3
|
+
Async Python client for the Daikin Onecta cloud API.
|
|
4
|
+
|
|
5
|
+
This library contains the Daikin-specific API layer used by the Home Assistant
|
|
6
|
+
Daikin Onecta integration. It deliberately has no dependency on Home Assistant.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
import aiohttp
|
|
12
|
+
|
|
13
|
+
from daikin_onecta import OnectaClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def token_provider() -> str:
|
|
17
|
+
return "access-token"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async with aiohttp.ClientSession() as session:
|
|
21
|
+
client = OnectaClient(session, token_provider)
|
|
22
|
+
devices = await client.get_gateway_devices()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Authentication is supplied by an asynchronous token callback so applications
|
|
26
|
+
remain responsible for their own OAuth flow and token refresh.
|
|
27
|
+
|
|
28
|
+
## Status
|
|
29
|
+
|
|
30
|
+
The API is currently alpha. The first release intentionally exposes gateway
|
|
31
|
+
devices as raw dictionaries while the stable typed device model is developed.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "daikin-onecta"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async Python client for the Daikin Onecta cloud API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Johnny Willemsen" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"aiohttp>=3.9.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/jwillemsen/daikin-onecta"
|
|
28
|
+
Repository = "https://github.com/jwillemsen/daikin-onecta"
|
|
29
|
+
Issues = "https://github.com/jwillemsen/daikin-onecta/issues"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
test = [
|
|
33
|
+
"aiohttp<3.14",
|
|
34
|
+
"aioresponses>=0.7.8",
|
|
35
|
+
"pytest>=8.0",
|
|
36
|
+
"pytest-asyncio>=0.24",
|
|
37
|
+
"pytest-cov>=5.0",
|
|
38
|
+
]
|
|
39
|
+
dev = [
|
|
40
|
+
"daikin-onecta[test]",
|
|
41
|
+
"ruff>=0.13",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
asyncio_mode = "auto"
|
|
46
|
+
addopts = "--cov=daikin_onecta --cov-report=term-missing"
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 120
|
|
50
|
+
target-version = "py312"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "UP"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Async client for the Daikin Onecta cloud API."""
|
|
2
|
+
|
|
3
|
+
from .client import OnectaClient
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
OnectaApiError,
|
|
6
|
+
OnectaAuthenticationError,
|
|
7
|
+
OnectaConnectionError,
|
|
8
|
+
OnectaError,
|
|
9
|
+
OnectaRateLimitError,
|
|
10
|
+
)
|
|
11
|
+
from .models import GatewayDevice
|
|
12
|
+
from .rate_limit import RateLimit
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GatewayDevice",
|
|
16
|
+
"OnectaApiError",
|
|
17
|
+
"OnectaAuthenticationError",
|
|
18
|
+
"OnectaClient",
|
|
19
|
+
"OnectaConnectionError",
|
|
20
|
+
"OnectaError",
|
|
21
|
+
"OnectaRateLimitError",
|
|
22
|
+
"RateLimit",
|
|
23
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Client for the Daikin Onecta cloud API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
OnectaApiError,
|
|
11
|
+
OnectaAuthenticationError,
|
|
12
|
+
OnectaConnectionError,
|
|
13
|
+
OnectaRateLimitError,
|
|
14
|
+
)
|
|
15
|
+
from .models import GatewayDevice
|
|
16
|
+
from .rate_limit import RateLimit
|
|
17
|
+
|
|
18
|
+
ONECTA_API_URL = "https://api.onecta.daikineurope.com"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OnectaClient:
|
|
22
|
+
"""Async client for the Daikin Onecta cloud API."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
session: aiohttp.ClientSession,
|
|
27
|
+
token_provider: Callable[[], Awaitable[str]],
|
|
28
|
+
*,
|
|
29
|
+
base_url: str = ONECTA_API_URL,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize the client."""
|
|
32
|
+
self._session = session
|
|
33
|
+
self._token_provider = token_provider
|
|
34
|
+
self._base_url = base_url.rstrip("/")
|
|
35
|
+
self.rate_limit = RateLimit()
|
|
36
|
+
|
|
37
|
+
async def _request(
|
|
38
|
+
self,
|
|
39
|
+
method: str,
|
|
40
|
+
path: str,
|
|
41
|
+
*,
|
|
42
|
+
json_data: Any = None,
|
|
43
|
+
) -> Any:
|
|
44
|
+
"""Perform an authenticated request to the Daikin Onecta API."""
|
|
45
|
+
token = await self._token_provider()
|
|
46
|
+
headers = {
|
|
47
|
+
"Accept-Encoding": "gzip",
|
|
48
|
+
"Authorization": f"Bearer {token}",
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
async with self._session.request(
|
|
54
|
+
method,
|
|
55
|
+
f"{self._base_url}{path}",
|
|
56
|
+
headers=headers,
|
|
57
|
+
json=json_data,
|
|
58
|
+
) as response:
|
|
59
|
+
self.rate_limit = RateLimit.from_headers(response.headers)
|
|
60
|
+
response_text = await response.text()
|
|
61
|
+
|
|
62
|
+
if response.status in (401, 403):
|
|
63
|
+
raise OnectaAuthenticationError(
|
|
64
|
+
f"Daikin Onecta API authentication failed with HTTP {response.status}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if response.status == 429:
|
|
68
|
+
raise OnectaRateLimitError(self.rate_limit.retry_after)
|
|
69
|
+
|
|
70
|
+
if response.status >= 400:
|
|
71
|
+
raise OnectaApiError(response.status, response_text)
|
|
72
|
+
|
|
73
|
+
if response.status == 204 or not response_text:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
return json.loads(response_text)
|
|
78
|
+
except json.JSONDecodeError as err:
|
|
79
|
+
raise OnectaApiError(response.status, "Invalid JSON response") from err
|
|
80
|
+
except (OnectaApiError, OnectaAuthenticationError, OnectaRateLimitError):
|
|
81
|
+
raise
|
|
82
|
+
except (TimeoutError, aiohttp.ClientError) as err:
|
|
83
|
+
raise OnectaConnectionError(str(err)) from err
|
|
84
|
+
|
|
85
|
+
async def get_gateway_devices(self) -> list[GatewayDevice]:
|
|
86
|
+
"""Return all gateway devices available to the account."""
|
|
87
|
+
data = await self._request("GET", "/v1/gateway-devices")
|
|
88
|
+
if not isinstance(data, list):
|
|
89
|
+
raise OnectaApiError(200, "Expected a list of gateway devices")
|
|
90
|
+
return data
|
|
91
|
+
|
|
92
|
+
async def patch_characteristic(
|
|
93
|
+
self,
|
|
94
|
+
gateway_id: str,
|
|
95
|
+
management_point_id: str,
|
|
96
|
+
characteristic: str,
|
|
97
|
+
value: Any,
|
|
98
|
+
*,
|
|
99
|
+
path: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Set a management-point characteristic."""
|
|
102
|
+
body: dict[str, Any] = {"value": value}
|
|
103
|
+
if path:
|
|
104
|
+
body["path"] = path
|
|
105
|
+
await self._request(
|
|
106
|
+
"PATCH",
|
|
107
|
+
f"/v1/gateway-devices/{gateway_id}/management-points/{management_point_id}"
|
|
108
|
+
f"/characteristics/{characteristic}",
|
|
109
|
+
json_data=body,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def post_management_point(
|
|
113
|
+
self,
|
|
114
|
+
gateway_id: str,
|
|
115
|
+
management_point_id: str,
|
|
116
|
+
resource: str,
|
|
117
|
+
value: Any,
|
|
118
|
+
) -> Any:
|
|
119
|
+
"""POST data to a management-point resource."""
|
|
120
|
+
return await self._request(
|
|
121
|
+
"POST",
|
|
122
|
+
f"/v1/gateway-devices/{gateway_id}/management-points/{management_point_id}/{resource}",
|
|
123
|
+
json_data=value,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def put_management_point(
|
|
127
|
+
self,
|
|
128
|
+
gateway_id: str,
|
|
129
|
+
management_point_id: str,
|
|
130
|
+
resource: str,
|
|
131
|
+
value: Any = None,
|
|
132
|
+
) -> Any:
|
|
133
|
+
"""PUT data to a management-point resource."""
|
|
134
|
+
return await self._request(
|
|
135
|
+
"PUT",
|
|
136
|
+
f"/v1/gateway-devices/{gateway_id}/management-points/{management_point_id}/{resource}",
|
|
137
|
+
json_data=value,
|
|
138
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Exceptions raised by the Daikin Onecta client."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OnectaError(Exception):
|
|
5
|
+
"""Base exception for Daikin Onecta errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OnectaConnectionError(OnectaError):
|
|
9
|
+
"""Raised when communication with the Daikin Onecta API fails."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OnectaAuthenticationError(OnectaError):
|
|
13
|
+
"""Raised when authentication with the Daikin Onecta API fails."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OnectaRateLimitError(OnectaError):
|
|
17
|
+
"""Raised when the Daikin Onecta API rate limit is exceeded."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, retry_after: int | None, message: str = "Daikin Onecta API rate limit exceeded") -> None:
|
|
20
|
+
"""Initialize the rate limit error."""
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.retry_after = retry_after
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OnectaApiError(OnectaError):
|
|
26
|
+
"""Raised when the Daikin Onecta API returns an unexpected response."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, status: int, message: str) -> None:
|
|
29
|
+
"""Initialize the API error."""
|
|
30
|
+
super().__init__(f"Daikin Onecta API returned HTTP {status}: {message}")
|
|
31
|
+
self.status = status
|
|
32
|
+
self.message = message
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Daikin Onecta API rate-limit information."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _header_int(headers: Mapping[str, str], name: str) -> int | None:
|
|
8
|
+
"""Return an integer response header when it contains a valid value."""
|
|
9
|
+
value = headers.get(name)
|
|
10
|
+
if value is None:
|
|
11
|
+
return None
|
|
12
|
+
try:
|
|
13
|
+
return int(value)
|
|
14
|
+
except ValueError:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class RateLimit:
|
|
20
|
+
"""Rate-limit state returned by the Daikin Onecta API."""
|
|
21
|
+
|
|
22
|
+
minute_limit: int | None = None
|
|
23
|
+
day_limit: int | None = None
|
|
24
|
+
minute_remaining: int | None = None
|
|
25
|
+
day_remaining: int | None = None
|
|
26
|
+
retry_after: int | None = None
|
|
27
|
+
reset: int | None = None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_headers(cls, headers: Mapping[str, str]) -> "RateLimit":
|
|
31
|
+
"""Create rate-limit state from Daikin response headers."""
|
|
32
|
+
return cls(
|
|
33
|
+
minute_limit=_header_int(headers, "X-RateLimit-Limit-minute"),
|
|
34
|
+
day_limit=_header_int(headers, "X-RateLimit-Limit-day"),
|
|
35
|
+
minute_remaining=_header_int(headers, "X-RateLimit-Remaining-minute"),
|
|
36
|
+
day_remaining=_header_int(headers, "X-RateLimit-Remaining-day"),
|
|
37
|
+
retry_after=_header_int(headers, "Retry-After"),
|
|
38
|
+
reset=_header_int(headers, "RateLimit-Reset"),
|
|
39
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: daikin-onecta
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Python client for the Daikin Onecta cloud API
|
|
5
|
+
Author: Johnny Willemsen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jwillemsen/daikin-onecta
|
|
8
|
+
Project-URL: Repository, https://github.com/jwillemsen/daikin-onecta
|
|
9
|
+
Project-URL: Issues, https://github.com/jwillemsen/daikin-onecta/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: aiohttp<3.14; extra == "test"
|
|
20
|
+
Requires-Dist: aioresponses>=0.7.8; extra == "test"
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
|
23
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: daikin-onecta[test]; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.13; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# daikin-onecta
|
|
29
|
+
|
|
30
|
+
Async Python client for the Daikin Onecta cloud API.
|
|
31
|
+
|
|
32
|
+
This library contains the Daikin-specific API layer used by the Home Assistant
|
|
33
|
+
Daikin Onecta integration. It deliberately has no dependency on Home Assistant.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import aiohttp
|
|
39
|
+
|
|
40
|
+
from daikin_onecta import OnectaClient
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def token_provider() -> str:
|
|
44
|
+
return "access-token"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async with aiohttp.ClientSession() as session:
|
|
48
|
+
client = OnectaClient(session, token_provider)
|
|
49
|
+
devices = await client.get_gateway_devices()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Authentication is supplied by an asynchronous token callback so applications
|
|
53
|
+
remain responsible for their own OAuth flow and token refresh.
|
|
54
|
+
|
|
55
|
+
## Status
|
|
56
|
+
|
|
57
|
+
The API is currently alpha. The first release intentionally exposes gateway
|
|
58
|
+
devices as raw dictionaries while the stable typed device model is developed.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/daikin_onecta/__init__.py
|
|
4
|
+
src/daikin_onecta/client.py
|
|
5
|
+
src/daikin_onecta/exceptions.py
|
|
6
|
+
src/daikin_onecta/models.py
|
|
7
|
+
src/daikin_onecta/rate_limit.py
|
|
8
|
+
src/daikin_onecta.egg-info/PKG-INFO
|
|
9
|
+
src/daikin_onecta.egg-info/SOURCES.txt
|
|
10
|
+
src/daikin_onecta.egg-info/dependency_links.txt
|
|
11
|
+
src/daikin_onecta.egg-info/requires.txt
|
|
12
|
+
src/daikin_onecta.egg-info/top_level.txt
|
|
13
|
+
tests/test_client.py
|
|
14
|
+
tests/test_rate_limit.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
daikin_onecta
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Tests for the Daikin Onecta client."""
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
import pytest
|
|
5
|
+
from aioresponses import aioresponses
|
|
6
|
+
|
|
7
|
+
from daikin_onecta import (
|
|
8
|
+
OnectaApiError,
|
|
9
|
+
OnectaAuthenticationError,
|
|
10
|
+
OnectaClient,
|
|
11
|
+
OnectaRateLimitError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
BASE_URL = "https://api.onecta.daikineurope.com"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def token_provider() -> str:
|
|
18
|
+
"""Return a test access token."""
|
|
19
|
+
return "token"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_get_gateway_devices() -> None:
|
|
24
|
+
"""Return gateway devices from the API."""
|
|
25
|
+
payload = [{"id": "gateway-1", "deviceModel": "test"}]
|
|
26
|
+
|
|
27
|
+
async with aiohttp.ClientSession() as session:
|
|
28
|
+
with aioresponses() as mocked:
|
|
29
|
+
mocked.get(
|
|
30
|
+
f"{BASE_URL}/v1/gateway-devices",
|
|
31
|
+
payload=payload,
|
|
32
|
+
headers={"X-RateLimit-Remaining-day": "123"},
|
|
33
|
+
)
|
|
34
|
+
client = OnectaClient(session, token_provider)
|
|
35
|
+
|
|
36
|
+
assert await client.get_gateway_devices() == payload
|
|
37
|
+
assert client.rate_limit.day_remaining == 123
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_rate_limit_error() -> None:
|
|
42
|
+
"""Expose Daikin retry-after information on HTTP 429."""
|
|
43
|
+
async with aiohttp.ClientSession() as session:
|
|
44
|
+
with aioresponses() as mocked:
|
|
45
|
+
mocked.get(
|
|
46
|
+
f"{BASE_URL}/v1/gateway-devices",
|
|
47
|
+
status=429,
|
|
48
|
+
headers={"Retry-After": "60"},
|
|
49
|
+
)
|
|
50
|
+
client = OnectaClient(session, token_provider)
|
|
51
|
+
|
|
52
|
+
with pytest.raises(OnectaRateLimitError) as exc_info:
|
|
53
|
+
await client.get_gateway_devices()
|
|
54
|
+
|
|
55
|
+
assert exc_info.value.retry_after == 60
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
@pytest.mark.parametrize("status", [401, 403])
|
|
60
|
+
async def test_authentication_error(status: int) -> None:
|
|
61
|
+
"""Translate authentication failures to a dedicated exception."""
|
|
62
|
+
async with aiohttp.ClientSession() as session:
|
|
63
|
+
with aioresponses() as mocked:
|
|
64
|
+
mocked.get(f"{BASE_URL}/v1/gateway-devices", status=status)
|
|
65
|
+
client = OnectaClient(session, token_provider)
|
|
66
|
+
|
|
67
|
+
with pytest.raises(OnectaAuthenticationError):
|
|
68
|
+
await client.get_gateway_devices()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.asyncio
|
|
72
|
+
async def test_unexpected_api_error() -> None:
|
|
73
|
+
"""Raise an API error instead of returning an empty device list."""
|
|
74
|
+
async with aiohttp.ClientSession() as session:
|
|
75
|
+
with aioresponses() as mocked:
|
|
76
|
+
mocked.get(f"{BASE_URL}/v1/gateway-devices", status=500, body="server error")
|
|
77
|
+
client = OnectaClient(session, token_provider)
|
|
78
|
+
|
|
79
|
+
with pytest.raises(OnectaApiError) as exc_info:
|
|
80
|
+
await client.get_gateway_devices()
|
|
81
|
+
|
|
82
|
+
assert exc_info.value.status == 500
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Tests for Daikin Onecta rate-limit parsing."""
|
|
2
|
+
|
|
3
|
+
from daikin_onecta import RateLimit
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_rate_limit_from_headers() -> None:
|
|
7
|
+
"""Parse all supported Daikin rate-limit headers."""
|
|
8
|
+
rate_limit = RateLimit.from_headers(
|
|
9
|
+
{
|
|
10
|
+
"X-RateLimit-Limit-minute": "20",
|
|
11
|
+
"X-RateLimit-Limit-day": "200",
|
|
12
|
+
"X-RateLimit-Remaining-minute": "7",
|
|
13
|
+
"X-RateLimit-Remaining-day": "123",
|
|
14
|
+
"Retry-After": "42",
|
|
15
|
+
"RateLimit-Reset": "99",
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
assert rate_limit.minute_limit == 20
|
|
20
|
+
assert rate_limit.day_limit == 200
|
|
21
|
+
assert rate_limit.minute_remaining == 7
|
|
22
|
+
assert rate_limit.day_remaining == 123
|
|
23
|
+
assert rate_limit.retry_after == 42
|
|
24
|
+
assert rate_limit.reset == 99
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_rate_limit_ignores_invalid_headers() -> None:
|
|
28
|
+
"""Invalid or absent headers should not break a request."""
|
|
29
|
+
rate_limit = RateLimit.from_headers({"Retry-After": "invalid"})
|
|
30
|
+
|
|
31
|
+
assert rate_limit.retry_after is None
|
|
32
|
+
assert rate_limit.minute_limit is None
|