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.
Files changed (27) hide show
  1. {comwatt_client-0.2.2/comwatt_client.egg-info → comwatt_client-0.2.4}/PKG-INFO +47 -14
  2. comwatt_client-0.2.2/PKG-INFO → comwatt_client-0.2.4/README.md +38 -22
  3. comwatt_client-0.2.4/comwatt_client/__init__.py +4 -0
  4. comwatt_client-0.2.4/comwatt_client/client.py +440 -0
  5. comwatt_client-0.2.4/comwatt_client/exceptions.py +30 -0
  6. comwatt_client-0.2.4/comwatt_client/py.typed +0 -0
  7. comwatt_client-0.2.2/README.md → comwatt_client-0.2.4/comwatt_client.egg-info/PKG-INFO +55 -5
  8. comwatt_client-0.2.4/comwatt_client.egg-info/SOURCES.txt +18 -0
  9. comwatt_client-0.2.4/comwatt_client.egg-info/requires.txt +1 -0
  10. comwatt_client-0.2.4/pyproject.toml +40 -0
  11. comwatt_client-0.2.4/tests/test_aggregations.py +435 -0
  12. comwatt_client-0.2.4/tests/test_authenticate.py +118 -0
  13. comwatt_client-0.2.4/tests/test_context_manager.py +43 -0
  14. comwatt_client-0.2.4/tests/test_devices.py +171 -0
  15. comwatt_client-0.2.4/tests/test_exceptions.py +43 -0
  16. comwatt_client-0.2.4/tests/test_reauth.py +153 -0
  17. comwatt_client-0.2.4/tests/test_user_and_sites.py +73 -0
  18. comwatt_client-0.2.2/.gitignore +0 -160
  19. comwatt_client-0.2.2/comwatt_client/__init__.py +0 -3
  20. comwatt_client-0.2.2/comwatt_client/client.py +0 -290
  21. comwatt_client-0.2.2/comwatt_client.egg-info/SOURCES.txt +0 -11
  22. comwatt_client-0.2.2/comwatt_client.egg-info/requires.txt +0 -1
  23. comwatt_client-0.2.2/requirements.txt +0 -2
  24. comwatt_client-0.2.2/setup.py +0 -28
  25. {comwatt_client-0.2.2 → comwatt_client-0.2.4}/comwatt_client.egg-info/dependency_links.txt +0 -0
  26. {comwatt_client-0.2.2 → comwatt_client-0.2.4}/comwatt_client.egg-info/top_level.txt +0 -0
  27. {comwatt_client-0.2.2 → comwatt_client-0.2.4}/setup.cfg +0 -0
@@ -1,19 +1,19 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: comwatt-client
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
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
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 = "VIRTUAL_QUANTITY", aggregation_level = "HOUR", aggregation_type = "SUM", time_ago_unit = "DAY", time_ago_value = 1)`: Retrieves the time series data for the networks of a specific site, based on the provided parameters.
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 comwatt.client import ComwattClient
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 = "VIRTUAL_QUANTITY", aggregation_level = "HOUR", aggregation_type = "SUM", time_ago_unit = "DAY", time_ago_value = 1)`: Retrieves the time series data for the networks of a specific site, based on the provided parameters.
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 comwatt.client import ComwattClient
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,4 @@
1
+ from .client import ComwattClient
2
+ from .exceptions import ComwattError, ComwattAuthError, ComwattAPIError
3
+
4
+ __all__ = ["ComwattClient", "ComwattError", "ComwattAuthError", "ComwattAPIError"]
@@ -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