comwatt-client 0.2.2__tar.gz → 0.2.4__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.
- {comwatt_client-0.2.2/comwatt_client.egg-info → comwatt_client-0.2.4}/PKG-INFO +47 -14
- comwatt_client-0.2.2/PKG-INFO → comwatt_client-0.2.4/README.md +38 -22
- comwatt_client-0.2.4/comwatt_client/__init__.py +4 -0
- comwatt_client-0.2.4/comwatt_client/client.py +440 -0
- comwatt_client-0.2.4/comwatt_client/exceptions.py +30 -0
- comwatt_client-0.2.4/comwatt_client/py.typed +0 -0
- comwatt_client-0.2.2/README.md → comwatt_client-0.2.4/comwatt_client.egg-info/PKG-INFO +55 -5
- comwatt_client-0.2.4/comwatt_client.egg-info/SOURCES.txt +18 -0
- comwatt_client-0.2.4/comwatt_client.egg-info/requires.txt +1 -0
- comwatt_client-0.2.4/pyproject.toml +40 -0
- comwatt_client-0.2.4/tests/test_aggregations.py +435 -0
- comwatt_client-0.2.4/tests/test_authenticate.py +118 -0
- comwatt_client-0.2.4/tests/test_context_manager.py +43 -0
- comwatt_client-0.2.4/tests/test_devices.py +171 -0
- comwatt_client-0.2.4/tests/test_exceptions.py +43 -0
- comwatt_client-0.2.4/tests/test_reauth.py +153 -0
- comwatt_client-0.2.4/tests/test_user_and_sites.py +73 -0
- comwatt_client-0.2.2/.gitignore +0 -160
- comwatt_client-0.2.2/comwatt_client/__init__.py +0 -3
- comwatt_client-0.2.2/comwatt_client/client.py +0 -290
- comwatt_client-0.2.2/comwatt_client.egg-info/SOURCES.txt +0 -11
- comwatt_client-0.2.2/comwatt_client.egg-info/requires.txt +0 -1
- comwatt_client-0.2.2/requirements.txt +0 -2
- comwatt_client-0.2.2/setup.py +0 -28
- {comwatt_client-0.2.2 → comwatt_client-0.2.4}/comwatt_client.egg-info/dependency_links.txt +0 -0
- {comwatt_client-0.2.2 → comwatt_client-0.2.4}/comwatt_client.egg-info/top_level.txt +0 -0
- {comwatt_client-0.2.2 → comwatt_client-0.2.4}/setup.cfg +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: comwatt-client
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Python Client for Comwatt API
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Author-email: contact@greil.fr
|
|
5
|
+
Author-email: Matéo Greil <contact@greil.fr>
|
|
6
|
+
Project-URL: Homepage, https://github.com/MateoGreil/python-comwatt-client
|
|
8
7
|
Classifier: Development Status :: 3 - Alpha
|
|
9
8
|
Classifier: Intended Audience :: Developers
|
|
10
9
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
|
-
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: requests>=2.32.4
|
|
17
17
|
|
|
18
18
|
# Comwatt Python Client
|
|
19
19
|
|
|
@@ -25,13 +25,26 @@ Please note that the Comwatt client is exclusively for gen4 devices: it use `ene
|
|
|
25
25
|
## Features
|
|
26
26
|
The client currently supports the following methods:
|
|
27
27
|
|
|
28
|
-
- `authenticate(self, username, password)`: Authenticates a user with the provided username and password.
|
|
28
|
+
- `authenticate(self, username, password)`: Authenticates a user with the provided username and password. The client re-authenticates automatically on session expiry (HTTP 401) and retries the request once; pass `ComwattClient(auto_reauth=False)` to disable this.
|
|
29
|
+
- `is_authenticated(self)`: Returns whether the current session is still valid (probes the API; `True`/`False`, re-raises unexpected errors).
|
|
29
30
|
- `get_authenticated_user(self)`: Retrieves information about the authenticated user.
|
|
30
31
|
- `get_sites(self)`: Retrieves a list of sites associated with the authenticated user.
|
|
31
|
-
- `get_site_networks_ts_time_ago(self, site_id, measure_kind = "
|
|
32
|
-
- `get_site_consumption_breakdown_time_ago(self, site_id, aggregation_level = "HOUR", time_ago_unit = "DAY", time_ago_value = 1)` Retrieves the consumption breakdown data for a specific site, based on the provided parameters.
|
|
32
|
+
- `get_site_networks_ts_time_ago(self, site_id, measure_kind = "FLOW", aggregation_level = "NONE", aggregation_type = None, time_ago_unit = "HOUR", time_ago_value = 1, start = None, end = None)`: Retrieves the time series data for the networks of a specific site, based on the provided parameters.
|
|
33
|
+
- `get_site_consumption_breakdown_time_ago(self, site_id, aggregation_level = "HOUR", time_ago_unit = "DAY", time_ago_value = 1, start = None, end = None)` Retrieves the consumption breakdown data for a specific site, based on the provided parameters.
|
|
33
34
|
- `get_devices(self, site_id)`: Retrieves a list of devices for the specified site.
|
|
34
|
-
- `get_device_ts_time_ago(self, device_id, measure_kind = "FLOW", aggregation_level = "HOUR", aggregation_type = "MAX", time_ago_unit = "DAY", time_ago_value = "1")`: Retrieves the time series data for a specific device, based on the provided parameters.
|
|
35
|
+
- `get_device_ts_time_ago(self, device_id, measure_kind = "FLOW", aggregation_level = "HOUR", aggregation_type = "MAX", time_ago_unit = "DAY", time_ago_value = "1", start = None, end = None)`: Retrieves the time series data for a specific device, based on the provided parameters.
|
|
36
|
+
|
|
37
|
+
`start`/`end` accept a `datetime` or ISO-8601 string and select an absolute window instead of the relative `time_ago_*` params; a naive `datetime` is treated as UTC:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from datetime import datetime
|
|
41
|
+
|
|
42
|
+
client.get_device_ts_time_ago(
|
|
43
|
+
"device-1",
|
|
44
|
+
start=datetime(2026, 7, 4, 0, 0, 0),
|
|
45
|
+
end=datetime(2026, 7, 5, 0, 0, 0),
|
|
46
|
+
)
|
|
47
|
+
```
|
|
35
48
|
- `get_device(self, device_id)`: Retrieves information about a specific device.
|
|
36
49
|
- `put_device(self, device_id, payload)`: Updates a specific device with the provided payload.
|
|
37
50
|
|
|
@@ -46,7 +59,7 @@ pip install comwatt-client
|
|
|
46
59
|
Here's a simple example of how to use the Comwatt Python Client:
|
|
47
60
|
|
|
48
61
|
```python
|
|
49
|
-
from
|
|
62
|
+
from comwatt_client import ComwattClient
|
|
50
63
|
|
|
51
64
|
# Create a Comwatt client instance
|
|
52
65
|
client = ComwattClient()
|
|
@@ -95,5 +108,25 @@ client.switch_capacity(capacity_id, True)
|
|
|
95
108
|
|
|
96
109
|
Make sure to replace `'username'`, `'password'` with the actual values for your Comwatt account.
|
|
97
110
|
|
|
111
|
+
## Error handling
|
|
112
|
+
|
|
113
|
+
All errors raised by the client subclass `ComwattError`:
|
|
114
|
+
|
|
115
|
+
- `ComwattAuthError` — login failed or the session expired (HTTP 401). Re-authenticate.
|
|
116
|
+
- `ComwattAPIError` — any other unexpected HTTP status. Exposes `.status_code`, `.url`, `.detail`.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from comwatt_client import ComwattClient, ComwattAuthError, ComwattAPIError
|
|
120
|
+
|
|
121
|
+
client = ComwattClient()
|
|
122
|
+
try:
|
|
123
|
+
client.authenticate("username", "password")
|
|
124
|
+
sites = client.get_sites()
|
|
125
|
+
except ComwattAuthError:
|
|
126
|
+
... # credentials wrong or session expired — re-authenticate
|
|
127
|
+
except ComwattAPIError as e:
|
|
128
|
+
print(e.status_code, e.url, e.detail)
|
|
129
|
+
```
|
|
130
|
+
|
|
98
131
|
## Contributing
|
|
99
132
|
Contributions to the Comwatt Python Client are welcome! If you find any issues or have suggestions for improvement, please open an issue or submit a pull request on the GitHub repository.
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: comwatt-client
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Summary: Python Client for Comwatt API
|
|
5
|
-
Home-page: https://github.com/MateoGreil/python-comwatt-client
|
|
6
|
-
Author: Matéo Greil
|
|
7
|
-
Author-email: contact@greil.fr
|
|
8
|
-
Classifier: Development Status :: 3 - Alpha
|
|
9
|
-
Classifier: Intended Audience :: Developers
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
Requires-Dist: requests
|
|
17
|
-
|
|
18
1
|
# Comwatt Python Client
|
|
19
2
|
|
|
20
3
|
## Overview
|
|
@@ -25,13 +8,26 @@ Please note that the Comwatt client is exclusively for gen4 devices: it use `ene
|
|
|
25
8
|
## Features
|
|
26
9
|
The client currently supports the following methods:
|
|
27
10
|
|
|
28
|
-
- `authenticate(self, username, password)`: Authenticates a user with the provided username and password.
|
|
11
|
+
- `authenticate(self, username, password)`: Authenticates a user with the provided username and password. The client re-authenticates automatically on session expiry (HTTP 401) and retries the request once; pass `ComwattClient(auto_reauth=False)` to disable this.
|
|
12
|
+
- `is_authenticated(self)`: Returns whether the current session is still valid (probes the API; `True`/`False`, re-raises unexpected errors).
|
|
29
13
|
- `get_authenticated_user(self)`: Retrieves information about the authenticated user.
|
|
30
14
|
- `get_sites(self)`: Retrieves a list of sites associated with the authenticated user.
|
|
31
|
-
- `get_site_networks_ts_time_ago(self, site_id, measure_kind = "
|
|
32
|
-
- `get_site_consumption_breakdown_time_ago(self, site_id, aggregation_level = "HOUR", time_ago_unit = "DAY", time_ago_value = 1)` Retrieves the consumption breakdown data for a specific site, based on the provided parameters.
|
|
15
|
+
- `get_site_networks_ts_time_ago(self, site_id, measure_kind = "FLOW", aggregation_level = "NONE", aggregation_type = None, time_ago_unit = "HOUR", time_ago_value = 1, start = None, end = None)`: Retrieves the time series data for the networks of a specific site, based on the provided parameters.
|
|
16
|
+
- `get_site_consumption_breakdown_time_ago(self, site_id, aggregation_level = "HOUR", time_ago_unit = "DAY", time_ago_value = 1, start = None, end = None)` Retrieves the consumption breakdown data for a specific site, based on the provided parameters.
|
|
33
17
|
- `get_devices(self, site_id)`: Retrieves a list of devices for the specified site.
|
|
34
|
-
- `get_device_ts_time_ago(self, device_id, measure_kind = "FLOW", aggregation_level = "HOUR", aggregation_type = "MAX", time_ago_unit = "DAY", time_ago_value = "1")`: Retrieves the time series data for a specific device, based on the provided parameters.
|
|
18
|
+
- `get_device_ts_time_ago(self, device_id, measure_kind = "FLOW", aggregation_level = "HOUR", aggregation_type = "MAX", time_ago_unit = "DAY", time_ago_value = "1", start = None, end = None)`: Retrieves the time series data for a specific device, based on the provided parameters.
|
|
19
|
+
|
|
20
|
+
`start`/`end` accept a `datetime` or ISO-8601 string and select an absolute window instead of the relative `time_ago_*` params; a naive `datetime` is treated as UTC:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
|
|
25
|
+
client.get_device_ts_time_ago(
|
|
26
|
+
"device-1",
|
|
27
|
+
start=datetime(2026, 7, 4, 0, 0, 0),
|
|
28
|
+
end=datetime(2026, 7, 5, 0, 0, 0),
|
|
29
|
+
)
|
|
30
|
+
```
|
|
35
31
|
- `get_device(self, device_id)`: Retrieves information about a specific device.
|
|
36
32
|
- `put_device(self, device_id, payload)`: Updates a specific device with the provided payload.
|
|
37
33
|
|
|
@@ -46,7 +42,7 @@ pip install comwatt-client
|
|
|
46
42
|
Here's a simple example of how to use the Comwatt Python Client:
|
|
47
43
|
|
|
48
44
|
```python
|
|
49
|
-
from
|
|
45
|
+
from comwatt_client import ComwattClient
|
|
50
46
|
|
|
51
47
|
# Create a Comwatt client instance
|
|
52
48
|
client = ComwattClient()
|
|
@@ -95,5 +91,25 @@ client.switch_capacity(capacity_id, True)
|
|
|
95
91
|
|
|
96
92
|
Make sure to replace `'username'`, `'password'` with the actual values for your Comwatt account.
|
|
97
93
|
|
|
94
|
+
## Error handling
|
|
95
|
+
|
|
96
|
+
All errors raised by the client subclass `ComwattError`:
|
|
97
|
+
|
|
98
|
+
- `ComwattAuthError` — login failed or the session expired (HTTP 401). Re-authenticate.
|
|
99
|
+
- `ComwattAPIError` — any other unexpected HTTP status. Exposes `.status_code`, `.url`, `.detail`.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from comwatt_client import ComwattClient, ComwattAuthError, ComwattAPIError
|
|
103
|
+
|
|
104
|
+
client = ComwattClient()
|
|
105
|
+
try:
|
|
106
|
+
client.authenticate("username", "password")
|
|
107
|
+
sites = client.get_sites()
|
|
108
|
+
except ComwattAuthError:
|
|
109
|
+
... # credentials wrong or session expired — re-authenticate
|
|
110
|
+
except ComwattAPIError as e:
|
|
111
|
+
print(e.status_code, e.url, e.detail)
|
|
112
|
+
```
|
|
113
|
+
|
|
98
114
|
## Contributing
|
|
99
115
|
Contributions to the Comwatt Python Client are welcome! If you find any issues or have suggestions for improvement, please open an issue or submit a pull request on the GitHub repository.
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import json
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Any, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from .exceptions import ComwattAPIError, ComwattAuthError, ComwattError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _format_timestamp(value: datetime | str) -> str:
|
|
15
|
+
if isinstance(value, datetime):
|
|
16
|
+
if value.tzinfo is None:
|
|
17
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
18
|
+
return value.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
19
|
+
if isinstance(value, str):
|
|
20
|
+
return value
|
|
21
|
+
raise TypeError(f"Expected datetime or str, got {type(value).__name__}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _aggregations_query(*, id_param: str, id_value: int | str, aggregation_level: str,
|
|
25
|
+
measure_kind: str | None = None, aggregation_type: str | None = None,
|
|
26
|
+
time_ago_unit: str | None = None, time_ago_value: int | str | None = None,
|
|
27
|
+
start: datetime | str | None = None, end: datetime | str | None = None) -> dict[str, str]:
|
|
28
|
+
if end is not None and start is None:
|
|
29
|
+
raise ValueError("end requires start")
|
|
30
|
+
|
|
31
|
+
params: dict[str, str] = {id_param: str(id_value), "aggregationLevel": aggregation_level}
|
|
32
|
+
|
|
33
|
+
if measure_kind is not None:
|
|
34
|
+
params["measureKind"] = measure_kind
|
|
35
|
+
if aggregation_type is not None:
|
|
36
|
+
params["aggregationType"] = aggregation_type
|
|
37
|
+
|
|
38
|
+
if start is not None:
|
|
39
|
+
params["start"] = _format_timestamp(start)
|
|
40
|
+
if end is not None:
|
|
41
|
+
params["end"] = _format_timestamp(end)
|
|
42
|
+
else:
|
|
43
|
+
if time_ago_unit is not None:
|
|
44
|
+
params["timeAgoUnit"] = time_ago_unit
|
|
45
|
+
if time_ago_value is not None:
|
|
46
|
+
params["timeAgoValue"] = str(time_ago_value)
|
|
47
|
+
|
|
48
|
+
return params
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _response_detail(response: requests.Response) -> str | None:
|
|
52
|
+
try:
|
|
53
|
+
body = response.json()
|
|
54
|
+
except ValueError:
|
|
55
|
+
return None
|
|
56
|
+
if isinstance(body, dict):
|
|
57
|
+
return body.get("detail")
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _api_error(response: requests.Response) -> ComwattError:
|
|
62
|
+
detail = _response_detail(response)
|
|
63
|
+
exc_cls = ComwattAuthError if response.status_code == 401 else ComwattAPIError
|
|
64
|
+
return exc_cls(status_code=response.status_code, url=response.url, detail=detail, response=response)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ComwattClient:
|
|
68
|
+
"""
|
|
69
|
+
A client for interacting with the Comwatt API.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
None
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
base_url (str): The base URL of the Comwatt API.
|
|
76
|
+
session (requests.Session): The session object for making HTTP requests.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
def __init__(self, timeout: float = 30, auto_reauth: bool = True) -> None:
|
|
80
|
+
self.base_url = 'https://energy.comwatt.com/api'
|
|
81
|
+
self.session = requests.Session()
|
|
82
|
+
self.timeout = timeout
|
|
83
|
+
self.auto_reauth = auto_reauth
|
|
84
|
+
self._username: str | None = None
|
|
85
|
+
self._auth_hash: str | None = None
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _hash_password(password: str) -> str:
|
|
89
|
+
return hashlib.sha256(f'jbjaonfusor_{password}_4acuttbuik9'.encode()).hexdigest()
|
|
90
|
+
|
|
91
|
+
def _post_authent(self, username: str, password_hash: str) -> None:
|
|
92
|
+
url = f'{self.base_url}/v1/authent'
|
|
93
|
+
data = {'username': username, 'password': password_hash}
|
|
94
|
+
|
|
95
|
+
response = self.session.post(url, json=data, timeout=self.timeout)
|
|
96
|
+
|
|
97
|
+
if response.status_code != 200:
|
|
98
|
+
detail = _response_detail(response)
|
|
99
|
+
raise ComwattAuthError(status_code=response.status_code, url=response.url, detail=detail, response=response)
|
|
100
|
+
|
|
101
|
+
if not self.session.cookies.get("cwt_session"):
|
|
102
|
+
raise ComwattAuthError("Authentication succeeded (HTTP 200) but no cwt_session cookie was set")
|
|
103
|
+
|
|
104
|
+
def authenticate(self, username: str, password: str) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Authenticates a user with the provided username and password.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
username (str): The username of the user.
|
|
110
|
+
password (str): The password of the user.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
None
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
Exception: If the authentication fails.
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
password_hash = self._hash_password(password)
|
|
121
|
+
self._post_authent(username, password_hash)
|
|
122
|
+
|
|
123
|
+
self._username = username
|
|
124
|
+
self._auth_hash = password_hash
|
|
125
|
+
|
|
126
|
+
def _reauthenticate(self) -> None:
|
|
127
|
+
if self._username and self._auth_hash:
|
|
128
|
+
self._post_authent(self._username, self._auth_hash)
|
|
129
|
+
else:
|
|
130
|
+
raise ComwattAuthError("Session expired and no stored credentials to re-authenticate")
|
|
131
|
+
|
|
132
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> requests.Response:
|
|
133
|
+
url = f'{self.base_url}{path}'
|
|
134
|
+
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
|
135
|
+
if response.status_code == 200:
|
|
136
|
+
return response
|
|
137
|
+
|
|
138
|
+
if response.status_code == 401 and self.auto_reauth and self._username and self._auth_hash:
|
|
139
|
+
self._reauthenticate()
|
|
140
|
+
retry_response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
|
141
|
+
if retry_response.status_code == 200:
|
|
142
|
+
return retry_response
|
|
143
|
+
raise _api_error(retry_response)
|
|
144
|
+
|
|
145
|
+
raise _api_error(response)
|
|
146
|
+
|
|
147
|
+
def is_authenticated(self) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Checks whether the current session cookie is still accepted by the API.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
None
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
bool: True if the session is authenticated, False if the API
|
|
156
|
+
rejected it (401/403).
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
Exception: If an unexpected error occurs while probing the API.
|
|
160
|
+
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
url = f'{self.base_url}/users/authenticated'
|
|
164
|
+
|
|
165
|
+
response = self.session.get(url, timeout=self.timeout)
|
|
166
|
+
if response.status_code == 200:
|
|
167
|
+
return True
|
|
168
|
+
elif response.status_code in (401, 403):
|
|
169
|
+
return False
|
|
170
|
+
else:
|
|
171
|
+
raise _api_error(response)
|
|
172
|
+
|
|
173
|
+
def get_authenticated_user(self) -> dict[str, Any]:
|
|
174
|
+
"""
|
|
175
|
+
Retrieves information about the authenticated user.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
None
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
dict: Information about the authenticated user.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
Exception: If an error occurs while retrieving the information.
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
return self._request("GET", "/users/authenticated").json()
|
|
189
|
+
|
|
190
|
+
def get_sites(self) -> list[dict[str, Any]]:
|
|
191
|
+
"""
|
|
192
|
+
Retrieves a list of sites associated with the authenticated user.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
None
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
list: A list of sites.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
Exception: If an error occurs while retrieving the sites.
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
return self._request("GET", "/sites").json()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_site_networks_ts_time_ago(self, site_id: int | str,
|
|
209
|
+
measure_kind: str = "FLOW",
|
|
210
|
+
aggregation_level: str = "NONE",
|
|
211
|
+
aggregation_type: str | None = None,
|
|
212
|
+
time_ago_unit: str = "HOUR",
|
|
213
|
+
time_ago_value: int | str = 1,
|
|
214
|
+
start: datetime | str | None = None,
|
|
215
|
+
end: datetime | str | None = None) -> dict[str, Any]:
|
|
216
|
+
"""
|
|
217
|
+
Retrieves the time series data for the networks of a specific site, based on the provided parameters.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
site_id (str): The ID of the site.
|
|
221
|
+
measure_kind (str): The kind of measure (default: "FLOW").
|
|
222
|
+
aggregation_level (str): The aggregation level (default: "NONE").
|
|
223
|
+
aggregation_type (str): The aggregation type (default: None, can be : None, "SUM", "MAX").
|
|
224
|
+
time_ago_unit (str): The unit of time ago (default: "HOUR").
|
|
225
|
+
time_ago_value (int): The value of time ago (default: 1).
|
|
226
|
+
start (datetime | str): The start of an absolute time window (default: None).
|
|
227
|
+
Accepts a `datetime` or an ISO-8601 string; a naive `datetime` is treated as UTC.
|
|
228
|
+
When provided, the relative `time_ago_unit`/`time_ago_value` parameters are ignored.
|
|
229
|
+
end (datetime | str): The end of an absolute time window (default: None).
|
|
230
|
+
Accepts a `datetime` or an ISO-8601 string; a naive `datetime` is treated as UTC.
|
|
231
|
+
Defaults server-side to "now" when omitted. Passing `end` without `start` raises `ValueError`.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
dict: The time series data.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
ValueError: If `end` is given without `start`.
|
|
238
|
+
Exception: If an error occurs while retrieving the data.
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
params = _aggregations_query(
|
|
243
|
+
id_param="siteId", id_value=site_id, aggregation_level=aggregation_level,
|
|
244
|
+
measure_kind=measure_kind, aggregation_type=aggregation_type,
|
|
245
|
+
time_ago_unit=time_ago_unit, time_ago_value=time_ago_value,
|
|
246
|
+
start=start, end=end,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return self._request("GET", "/aggregations/site-networks-ts-time-ago", params=params).json()
|
|
250
|
+
|
|
251
|
+
def get_site_consumption_breakdown_time_ago(self, site_id: int | str,
|
|
252
|
+
aggregation_level: str = "HOUR",
|
|
253
|
+
time_ago_unit: str = "DAY",
|
|
254
|
+
time_ago_value: int | str = 1,
|
|
255
|
+
start: datetime | str | None = None,
|
|
256
|
+
end: datetime | str | None = None) -> dict[str, Any]:
|
|
257
|
+
"""
|
|
258
|
+
Retrieves the consumption breakdown data for a specific site, based on the provided parameters.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
site_id (str): The ID of the site.
|
|
262
|
+
aggregation_level (str): The aggregation level (default: "HOUR").
|
|
263
|
+
time_ago_unit (str): The unit of time ago (default: "DAY").
|
|
264
|
+
time_ago_value (int): The value of time ago (default: 1).
|
|
265
|
+
start (datetime | str): The start of an absolute time window (default: None).
|
|
266
|
+
Accepts a `datetime` or an ISO-8601 string; a naive `datetime` is treated as UTC.
|
|
267
|
+
When provided, the relative `time_ago_unit`/`time_ago_value` parameters are ignored.
|
|
268
|
+
end (datetime | str): The end of an absolute time window (default: None).
|
|
269
|
+
Accepts a `datetime` or an ISO-8601 string; a naive `datetime` is treated as UTC.
|
|
270
|
+
Defaults server-side to "now" when omitted. Passing `end` without `start` raises `ValueError`.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
dict: The consumption breakdown data.
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ValueError: If `end` is given without `start`.
|
|
277
|
+
Exception: If an error occurs while retrieving the data.
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
params = _aggregations_query(
|
|
282
|
+
id_param="siteId", id_value=site_id, aggregation_level=aggregation_level,
|
|
283
|
+
time_ago_unit=time_ago_unit, time_ago_value=time_ago_value,
|
|
284
|
+
start=start, end=end,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return self._request("GET", "/aggregations/consumption-breakdown-time-ago", params=params).json()
|
|
288
|
+
|
|
289
|
+
def get_devices(self, site_id: int | str) -> list[dict[str, Any]]:
|
|
290
|
+
"""
|
|
291
|
+
Retrieves a list of devices for the specified site.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
site_id (str): The ID of the site.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
list: A list of devices.
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
Exception: If an error occurs while retrieving the devices.
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
return self._request("GET", f"/devices?siteId={site_id}").json()
|
|
305
|
+
|
|
306
|
+
def get_device(self, device_id: int | str) -> dict[str, Any]:
|
|
307
|
+
"""
|
|
308
|
+
Retrieves information about a specific device.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
device_id (str): The ID of the device.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
dict: A dictionary containing the device information.
|
|
315
|
+
|
|
316
|
+
"""
|
|
317
|
+
return self._request("GET", f"/devices/{device_id}").json()
|
|
318
|
+
|
|
319
|
+
def put_device(self, device_id: int | str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
320
|
+
"""
|
|
321
|
+
Updates a specific device with the provided payload.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
device_id (str): The ID of the device.
|
|
325
|
+
payload (dict): The payload to update the device.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
dict: A dictionary containing the response from the API.
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
return self._request("PUT", f"/devices/{device_id}", json=payload).json()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_device_ts_time_ago(self, device_id: int | str,
|
|
335
|
+
measure_kind: str = "FLOW",
|
|
336
|
+
aggregation_level: str = "HOUR",
|
|
337
|
+
aggregation_type: str = "MAX",
|
|
338
|
+
time_ago_unit: str = "DAY",
|
|
339
|
+
time_ago_value: int | str = "1",
|
|
340
|
+
start: datetime | str | None = None,
|
|
341
|
+
end: datetime | str | None = None) -> dict[str, Any]:
|
|
342
|
+
"""
|
|
343
|
+
Retrieves the time series data for a specific device, based on the provided parameters.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
device_id (str): The ID of the device.
|
|
347
|
+
measure_kind (str): The kind of measure (default: "FLOW").
|
|
348
|
+
aggregation_level (str): The aggregation level (default: "HOUR").
|
|
349
|
+
aggregation_type (str): The aggregation type (default: "MAX").
|
|
350
|
+
time_ago_unit (str): The unit of time ago (default: "DAY").
|
|
351
|
+
time_ago_value (str): The value of time ago (default: "1").
|
|
352
|
+
start (datetime | str): The start of an absolute time window (default: None).
|
|
353
|
+
Accepts a `datetime` or an ISO-8601 string; a naive `datetime` is treated as UTC.
|
|
354
|
+
When provided, the relative `time_ago_unit`/`time_ago_value` parameters are ignored.
|
|
355
|
+
end (datetime | str): The end of an absolute time window (default: None).
|
|
356
|
+
Accepts a `datetime` or an ISO-8601 string; a naive `datetime` is treated as UTC.
|
|
357
|
+
Defaults server-side to "now" when omitted. Passing `end` without `start` raises `ValueError`.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
dict: The time series data.
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
ValueError: If `end` is given without `start`.
|
|
364
|
+
Exception: If an error occurs while retrieving the data.
|
|
365
|
+
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
params = _aggregations_query(
|
|
369
|
+
id_param="id", id_value=device_id, aggregation_level=aggregation_level,
|
|
370
|
+
measure_kind=measure_kind, aggregation_type=aggregation_type,
|
|
371
|
+
time_ago_unit=time_ago_unit, time_ago_value=time_ago_value,
|
|
372
|
+
start=start, end=end,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return self._request("GET", "/aggregations/time-series", params=params).json()
|
|
376
|
+
|
|
377
|
+
def switch_capacity(self, capacity_id: int | str, enable: bool) -> dict[str, Any]:
|
|
378
|
+
"""
|
|
379
|
+
Switch a specific capcaity to the enable value.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
capacity_id (str): The ID of the capacity.
|
|
383
|
+
enable (bool): The target state.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
dict: A dictionary containing the response from the API.
|
|
387
|
+
|
|
388
|
+
"""
|
|
389
|
+
return self._request("PUT", f"/capacities/{capacity_id}/switch?enable={str(enable).lower()}").json()
|
|
390
|
+
|
|
391
|
+
def close(self) -> None:
|
|
392
|
+
"""
|
|
393
|
+
Releases the local HTTP resources held by the client.
|
|
394
|
+
|
|
395
|
+
This only closes the local `requests.Session` (its connection pool
|
|
396
|
+
and sockets). It does not perform any network call, so it does not
|
|
397
|
+
log out server-side (no `POST /v1/logout` is issued) and does not
|
|
398
|
+
invalidate the Comwatt server session.
|
|
399
|
+
|
|
400
|
+
Safe to call multiple times.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
None
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
None
|
|
407
|
+
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
self.session.close()
|
|
411
|
+
|
|
412
|
+
def __enter__(self) -> ComwattClient:
|
|
413
|
+
"""
|
|
414
|
+
Enters the runtime context for this client.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
None
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
ComwattClient: This client instance.
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
return self
|
|
425
|
+
|
|
426
|
+
def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None:
|
|
427
|
+
"""
|
|
428
|
+
Exits the runtime context, closing the client's local resources.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
exc_type (type): The exception type, if any.
|
|
432
|
+
exc_val (Exception): The exception instance, if any.
|
|
433
|
+
exc_tb (traceback): The exception traceback, if any.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
None
|
|
437
|
+
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
self.close()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ComwattError(Exception):
|
|
10
|
+
"""Base class for all errors raised by the Comwatt client."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str | None = None, *, status_code: int | None = None, url: str | None = None, detail: str | None = None, response: requests.Response | None = None) -> None:
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.url = url
|
|
15
|
+
self.detail = detail
|
|
16
|
+
self.response = response
|
|
17
|
+
if message is None:
|
|
18
|
+
message = f"{status_code} {url}"
|
|
19
|
+
if detail:
|
|
20
|
+
message = f"{message}: {detail}"
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ComwattAuthError(ComwattError):
|
|
25
|
+
"""The session is invalid or login failed (HTTP 401, or a failed authenticate()).
|
|
26
|
+
Callers should re-authenticate."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ComwattAPIError(ComwattError):
|
|
30
|
+
"""Any other unexpected HTTP status from the Comwatt API."""
|
|
File without changes
|