pybluecurrent 0.0.4__tar.gz → 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.
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/PKG-INFO +29 -45
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/README.md +26 -42
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/pyproject.toml +3 -3
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/_version.py +3 -3
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/client.py +31 -38
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/PKG-INFO +29 -45
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/requires.txt +2 -2
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/tests/test_client.py +24 -26
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/.github/workflows/publish.yaml +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/.github/workflows/test.yaml +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/.gitignore +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/.pre-commit-config.yaml +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/LICENSE +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/setup.cfg +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/__init__.py +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/exceptions.py +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/py.typed +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/utilities.py +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/SOURCES.txt +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/dependency_links.txt +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/top_level.txt +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/tests/conftest.py +0 -0
- {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/tests/test_utilities.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybluecurrent
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Python client for BlueCurrent charge points.
|
|
5
5
|
Author-email: Rogier van der Geer <rogier@vander-geer.nl>
|
|
6
6
|
License: MIT
|
|
@@ -15,9 +15,9 @@ Requires-Python: >=3.10
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: asyncio-multisubscriber-queue>=0.4.1
|
|
18
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: httpx>=0.28
|
|
19
19
|
Requires-Dist: sjcl>=0.2.1
|
|
20
|
-
Requires-Dist: websockets>=
|
|
20
|
+
Requires-Dist: websockets>=14.0
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: black==23.3.0; extra == "dev"
|
|
23
23
|
Requires-Dist: pre-commit>=3.3.3; extra == "dev"
|
|
@@ -43,36 +43,32 @@ from pybluecurrent import BlueCurrentClient
|
|
|
43
43
|
client = BlueCurrentClient("your_username", "your_secret_password")
|
|
44
44
|
|
|
45
45
|
async with client:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
transactions = client.get_transactions(charge_points[0]["evse_id"])
|
|
46
|
+
charge_points = await client.get_charge_points()
|
|
47
|
+
transactions = await client.get_transactions(charge_points[0]["evse_id"])
|
|
49
48
|
```
|
|
50
49
|
|
|
51
50
|
## Methods
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
As a result, the `BlueCurrentClient` also exposes synchronous as well as asynchronous methods:
|
|
52
|
+
The `BlueCurrentClient` exposes the following methods:
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
The async methods can only be used when the websocket client is connected. For example:
|
|
54
|
+
- [`get_account`](#getaccount---get-your-account-information)
|
|
55
|
+
- [`get_charge_cards`](#getchargecards---get-your-charge-cards)
|
|
56
|
+
- [`get_charge_points`](#getchargepoints---get-your-charge-points)
|
|
57
|
+
- [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
|
|
58
|
+
- [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
|
|
59
|
+
- [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
|
|
60
|
+
- [`set_plug_and_charge_charge_card`](#setplugandchargechargecard---set-the-charge-card-for-plug-and-charge)
|
|
61
|
+
- [`set_status`](#setstatus---enable-or-disable-a-charge-point)
|
|
62
|
+
- [`login`](#login---log-in)
|
|
63
|
+
- [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
|
|
64
|
+
- [`get_contracts`](#getcontracts---get-your-contracts)
|
|
65
|
+
- [`get_grids`](#getgrids---get-your-grid-connections)
|
|
66
|
+
- [`get_transactions`](#gettransactions---get-a-list-of-transactions)
|
|
67
|
+
- [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
|
|
68
|
+
|
|
69
|
+
### Connection
|
|
70
|
+
|
|
71
|
+
The client can only be used when the websocket client is connected. For example:
|
|
76
72
|
```python
|
|
77
73
|
client = BlueCurrentClient("your_username", "your_secret_password")
|
|
78
74
|
async with client:
|
|
@@ -248,22 +244,10 @@ async def set_status(self, evse_id: str, enabled: bool) -> None
|
|
|
248
244
|
- `evse_id`: The ID of the charge point.
|
|
249
245
|
- `enabled`: Boolean that indicates the desired status.
|
|
250
246
|
|
|
251
|
-
### Synchronous
|
|
252
|
-
|
|
253
|
-
#### `login` - Log in
|
|
254
|
-
```python
|
|
255
|
-
def login(self) -> None
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
This method does not do anything if the client is already logged in.
|
|
259
|
-
Connection to the websocket api (async with client) automatically logs in the client,
|
|
260
|
-
so this endpoint is not needed when using the async API.
|
|
261
|
-
|
|
262
|
-
|
|
263
247
|
#### `get_charge_point_status` - Get the status of a charge point.
|
|
264
248
|
|
|
265
249
|
```python
|
|
266
|
-
def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
|
|
250
|
+
async def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
|
|
267
251
|
```
|
|
268
252
|
|
|
269
253
|
##### Arguments
|
|
@@ -296,7 +280,7 @@ A dictionary with the chargepoint status:
|
|
|
296
280
|
#### `get_contracts` - Get your contracts.
|
|
297
281
|
|
|
298
282
|
```python
|
|
299
|
-
def get_contracts(self) -> list[dict[str, str]]
|
|
283
|
+
async def get_contracts(self) -> list[dict[str, str]]
|
|
300
284
|
```
|
|
301
285
|
|
|
302
286
|
##### Returns
|
|
@@ -316,7 +300,7 @@ A list of dictionaries, each representing a contract:
|
|
|
316
300
|
#### `get_grids` - Get your grid connections.
|
|
317
301
|
|
|
318
302
|
```python
|
|
319
|
-
def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
|
|
303
|
+
async def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
|
|
320
304
|
```
|
|
321
305
|
|
|
322
306
|
##### Returns
|
|
@@ -335,7 +319,7 @@ A list of dictionaries, each representing a grid:
|
|
|
335
319
|
#### `get_transactions` - Get a list of transactions.
|
|
336
320
|
|
|
337
321
|
```python
|
|
338
|
-
def get_transactions(
|
|
322
|
+
async def get_transactions(
|
|
339
323
|
self, evse_id: str, newest_first: bool = True, page: int = 1
|
|
340
324
|
) -> dict[str, int | list[dict[str, Any]]]
|
|
341
325
|
```
|
|
@@ -377,7 +361,7 @@ A dictionary like this:
|
|
|
377
361
|
#### `iterate_transactions` - Iterate through your transactions
|
|
378
362
|
|
|
379
363
|
```python
|
|
380
|
-
def iterate_transactions(self, evse_id: str, newest_first: bool = True) ->
|
|
364
|
+
async def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> AsyncIterable[dict[str, Any]]
|
|
381
365
|
```
|
|
382
366
|
|
|
383
367
|
##### Arguments
|
|
@@ -16,36 +16,32 @@ from pybluecurrent import BlueCurrentClient
|
|
|
16
16
|
client = BlueCurrentClient("your_username", "your_secret_password")
|
|
17
17
|
|
|
18
18
|
async with client:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
transactions = client.get_transactions(charge_points[0]["evse_id"])
|
|
19
|
+
charge_points = await client.get_charge_points()
|
|
20
|
+
transactions = await client.get_transactions(charge_points[0]["evse_id"])
|
|
22
21
|
```
|
|
23
22
|
|
|
24
23
|
## Methods
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
As a result, the `BlueCurrentClient` also exposes synchronous as well as asynchronous methods:
|
|
25
|
+
The `BlueCurrentClient` exposes the following methods:
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
The async methods can only be used when the websocket client is connected. For example:
|
|
27
|
+
- [`get_account`](#getaccount---get-your-account-information)
|
|
28
|
+
- [`get_charge_cards`](#getchargecards---get-your-charge-cards)
|
|
29
|
+
- [`get_charge_points`](#getchargepoints---get-your-charge-points)
|
|
30
|
+
- [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
|
|
31
|
+
- [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
|
|
32
|
+
- [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
|
|
33
|
+
- [`set_plug_and_charge_charge_card`](#setplugandchargechargecard---set-the-charge-card-for-plug-and-charge)
|
|
34
|
+
- [`set_status`](#setstatus---enable-or-disable-a-charge-point)
|
|
35
|
+
- [`login`](#login---log-in)
|
|
36
|
+
- [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
|
|
37
|
+
- [`get_contracts`](#getcontracts---get-your-contracts)
|
|
38
|
+
- [`get_grids`](#getgrids---get-your-grid-connections)
|
|
39
|
+
- [`get_transactions`](#gettransactions---get-a-list-of-transactions)
|
|
40
|
+
- [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
|
|
41
|
+
|
|
42
|
+
### Connection
|
|
43
|
+
|
|
44
|
+
The client can only be used when the websocket client is connected. For example:
|
|
49
45
|
```python
|
|
50
46
|
client = BlueCurrentClient("your_username", "your_secret_password")
|
|
51
47
|
async with client:
|
|
@@ -221,22 +217,10 @@ async def set_status(self, evse_id: str, enabled: bool) -> None
|
|
|
221
217
|
- `evse_id`: The ID of the charge point.
|
|
222
218
|
- `enabled`: Boolean that indicates the desired status.
|
|
223
219
|
|
|
224
|
-
### Synchronous
|
|
225
|
-
|
|
226
|
-
#### `login` - Log in
|
|
227
|
-
```python
|
|
228
|
-
def login(self) -> None
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
This method does not do anything if the client is already logged in.
|
|
232
|
-
Connection to the websocket api (async with client) automatically logs in the client,
|
|
233
|
-
so this endpoint is not needed when using the async API.
|
|
234
|
-
|
|
235
|
-
|
|
236
220
|
#### `get_charge_point_status` - Get the status of a charge point.
|
|
237
221
|
|
|
238
222
|
```python
|
|
239
|
-
def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
|
|
223
|
+
async def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
|
|
240
224
|
```
|
|
241
225
|
|
|
242
226
|
##### Arguments
|
|
@@ -269,7 +253,7 @@ A dictionary with the chargepoint status:
|
|
|
269
253
|
#### `get_contracts` - Get your contracts.
|
|
270
254
|
|
|
271
255
|
```python
|
|
272
|
-
def get_contracts(self) -> list[dict[str, str]]
|
|
256
|
+
async def get_contracts(self) -> list[dict[str, str]]
|
|
273
257
|
```
|
|
274
258
|
|
|
275
259
|
##### Returns
|
|
@@ -289,7 +273,7 @@ A list of dictionaries, each representing a contract:
|
|
|
289
273
|
#### `get_grids` - Get your grid connections.
|
|
290
274
|
|
|
291
275
|
```python
|
|
292
|
-
def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
|
|
276
|
+
async def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
|
|
293
277
|
```
|
|
294
278
|
|
|
295
279
|
##### Returns
|
|
@@ -308,7 +292,7 @@ A list of dictionaries, each representing a grid:
|
|
|
308
292
|
#### `get_transactions` - Get a list of transactions.
|
|
309
293
|
|
|
310
294
|
```python
|
|
311
|
-
def get_transactions(
|
|
295
|
+
async def get_transactions(
|
|
312
296
|
self, evse_id: str, newest_first: bool = True, page: int = 1
|
|
313
297
|
) -> dict[str, int | list[dict[str, Any]]]
|
|
314
298
|
```
|
|
@@ -350,7 +334,7 @@ A dictionary like this:
|
|
|
350
334
|
#### `iterate_transactions` - Iterate through your transactions
|
|
351
335
|
|
|
352
336
|
```python
|
|
353
|
-
def iterate_transactions(self, evse_id: str, newest_first: bool = True) ->
|
|
337
|
+
async def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> AsyncIterable[dict[str, Any]]
|
|
354
338
|
```
|
|
355
339
|
|
|
356
340
|
##### Arguments
|
|
@@ -21,13 +21,13 @@ classifiers = [
|
|
|
21
21
|
"License :: OSI Approved :: MIT License",
|
|
22
22
|
"Operating System :: OS Independent",
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
24
|
-
"Typing :: Typed"
|
|
24
|
+
"Typing :: Typed",
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
27
|
"asyncio-multisubscriber-queue>=0.4.1",
|
|
28
|
-
"
|
|
28
|
+
"httpx>=0.28",
|
|
29
29
|
"sjcl>=0.2.1",
|
|
30
|
-
"websockets>=
|
|
30
|
+
"websockets>=14.0",
|
|
31
31
|
]
|
|
32
32
|
dynamic = ["version"]
|
|
33
33
|
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g1fb6788fb'
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
from asyncio import Task, create_task,
|
|
1
|
+
from asyncio import Task, create_task, wait_for
|
|
2
2
|
from datetime import date, datetime
|
|
3
3
|
from json import dumps, loads
|
|
4
4
|
from logging import getLogger
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, AsyncIterable
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
8
|
from asyncio_multisubscriber_queue import MultisubscriberQueue
|
|
9
|
-
from
|
|
9
|
+
from httpx import AsyncClient
|
|
10
10
|
from sjcl import SJCL
|
|
11
|
-
from websockets import
|
|
11
|
+
from websockets.asyncio.client import ClientConnection, connect
|
|
12
12
|
|
|
13
13
|
from pybluecurrent._version import __version__
|
|
14
14
|
from pybluecurrent.exceptions import AuthenticationFailed, BlueCurrentException
|
|
@@ -24,17 +24,18 @@ class BlueCurrentClient:
|
|
|
24
24
|
self.consumer: Task | None = None
|
|
25
25
|
self.credentials: tuple[str, str] = (username, password)
|
|
26
26
|
self.logger = getLogger("BlueCurrentClient")
|
|
27
|
+
self.httpx_client: AsyncClient | None = None
|
|
27
28
|
self.queue = MultisubscriberQueue()
|
|
28
|
-
self.socket:
|
|
29
|
+
self.socket: ClientConnection | None = None
|
|
29
30
|
self.token: str | None = None
|
|
30
31
|
|
|
31
|
-
## Asynchronous API ##
|
|
32
|
-
|
|
33
32
|
async def __aenter__(self) -> "BlueCurrentClient":
|
|
34
33
|
self.logger.debug("Creating BlueCurrent websocket connection")
|
|
35
34
|
self.connection = connect(self.socket_url, user_agent_header=self._user_agent)
|
|
36
35
|
self.socket = await self.connection.__aenter__()
|
|
37
36
|
self.consumer = create_task(self._handler())
|
|
37
|
+
self.httpx_client = AsyncClient()
|
|
38
|
+
await self.httpx_client.__aenter__()
|
|
38
39
|
if self.token is None:
|
|
39
40
|
await self._login()
|
|
40
41
|
await self._hello()
|
|
@@ -44,7 +45,8 @@ class BlueCurrentClient:
|
|
|
44
45
|
self.logger.debug("Closing BlueCurrent connection")
|
|
45
46
|
self.consumer.cancel()
|
|
46
47
|
await self.connection.__aexit__(exc_type, exc_val, exc_tb)
|
|
47
|
-
self.
|
|
48
|
+
await self.httpx_client.__aexit__(exc_type, exc_val, exc_tb)
|
|
49
|
+
self.consumer, self.socket, self.httpx_client = None, None, None
|
|
48
50
|
|
|
49
51
|
async def get_account(self) -> dict[str, bool | date | str]:
|
|
50
52
|
"""
|
|
@@ -263,25 +265,7 @@ class BlueCurrentClient:
|
|
|
263
265
|
await self._receive("RECEIVED_SOFT_RESET")
|
|
264
266
|
return await self._receive("STATUS_SOFT_RESET", timeout=30)
|
|
265
267
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def login(self) -> None:
|
|
269
|
-
"""
|
|
270
|
-
Retrieves a connection token.
|
|
271
|
-
|
|
272
|
-
This method does not do anything if the client is already logged in.
|
|
273
|
-
Connection to the websocket api (async with client) automatically logs in the client,
|
|
274
|
-
so this endpoint is not needed when using the async API.
|
|
275
|
-
"""
|
|
276
|
-
|
|
277
|
-
async def _login(client: BlueCurrentClient) -> None:
|
|
278
|
-
async with client:
|
|
279
|
-
pass
|
|
280
|
-
|
|
281
|
-
if self.token is None:
|
|
282
|
-
run(_login(self))
|
|
283
|
-
|
|
284
|
-
def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]:
|
|
268
|
+
async def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]:
|
|
285
269
|
"""
|
|
286
270
|
Get the status of a charge point.
|
|
287
271
|
|
|
@@ -307,7 +291,9 @@ class BlueCurrentClient:
|
|
|
307
291
|
"evse_id": "BCU123456",
|
|
308
292
|
}
|
|
309
293
|
"""
|
|
310
|
-
|
|
294
|
+
if self.httpx_client is None:
|
|
295
|
+
raise RuntimeError(f"{self.__class__.__name__} is not connected.")
|
|
296
|
+
response = await self.httpx_client.get(
|
|
311
297
|
f"{self.api_url}/chargepointstatus?evse_id={evse_id}",
|
|
312
298
|
headers={"Authorization": f"Token {self.token}", "User-Agent": self._user_agent},
|
|
313
299
|
)
|
|
@@ -321,7 +307,7 @@ class BlueCurrentClient:
|
|
|
321
307
|
},
|
|
322
308
|
)
|
|
323
309
|
|
|
324
|
-
def get_contracts(self) -> list[dict[str, str]]:
|
|
310
|
+
async def get_contracts(self) -> list[dict[str, str]]:
|
|
325
311
|
"""
|
|
326
312
|
Get your contracts.
|
|
327
313
|
|
|
@@ -337,14 +323,16 @@ class BlueCurrentClient:
|
|
|
337
323
|
}
|
|
338
324
|
]
|
|
339
325
|
"""
|
|
340
|
-
|
|
326
|
+
if self.httpx_client is None:
|
|
327
|
+
raise RuntimeError(f"{self.__class__.__name__} is not connected.")
|
|
328
|
+
response = await self.httpx_client.get(
|
|
341
329
|
f"{self.api_url}/getcontracts",
|
|
342
330
|
headers={"Authorization": f"Token {self.token}", "User-Agent": self._user_agent},
|
|
343
331
|
)
|
|
344
332
|
response.raise_for_status()
|
|
345
333
|
return response.json()["contracts"]
|
|
346
334
|
|
|
347
|
-
def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]:
|
|
335
|
+
async def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]:
|
|
348
336
|
"""
|
|
349
337
|
Get your grid connections.
|
|
350
338
|
|
|
@@ -359,14 +347,16 @@ class BlueCurrentClient:
|
|
|
359
347
|
}
|
|
360
348
|
]
|
|
361
349
|
"""
|
|
362
|
-
|
|
350
|
+
if self.httpx_client is None:
|
|
351
|
+
raise RuntimeError(f"{self.__class__.__name__} is not connected.")
|
|
352
|
+
response = await self.httpx_client.get(
|
|
363
353
|
f"{self.api_url}/getgrids",
|
|
364
354
|
headers={"Authorization": f"Token {self.token}", "User-Agent": self._user_agent},
|
|
365
355
|
)
|
|
366
356
|
response.raise_for_status()
|
|
367
357
|
return response.json()["grids"]
|
|
368
358
|
|
|
369
|
-
def get_transactions(
|
|
359
|
+
async def get_transactions(
|
|
370
360
|
self, evse_id: str, newest_first: bool = True, page: int = 1
|
|
371
361
|
) -> dict[str, int | list[dict[str, Any]]]:
|
|
372
362
|
"""
|
|
@@ -405,13 +395,15 @@ class BlueCurrentClient:
|
|
|
405
395
|
}
|
|
406
396
|
|
|
407
397
|
"""
|
|
408
|
-
|
|
398
|
+
if self.httpx_client is None:
|
|
399
|
+
raise RuntimeError(f"{self.__class__.__name__} is not connected.")
|
|
400
|
+
response = await self.httpx_client.post(
|
|
409
401
|
f"{self.api_url}/gettransactions?"
|
|
410
402
|
f"page={page}&"
|
|
411
403
|
f"sort_field_order={'DESC' if newest_first else 'ASC'}&"
|
|
412
404
|
f"sort_field=stoppedtimestamp",
|
|
413
405
|
headers={"Authorization": f"Token {self.token}", "User-Agent": self._user_agent},
|
|
414
|
-
data=dumps({"chargepoints": [{"chargepoint_id": evse_id}]})
|
|
406
|
+
data=dumps({"chargepoints": [{"chargepoint_id": evse_id}]}),
|
|
415
407
|
)
|
|
416
408
|
response.raise_for_status()
|
|
417
409
|
result = response.json()["data"]
|
|
@@ -421,7 +413,7 @@ class BlueCurrentClient:
|
|
|
421
413
|
)
|
|
422
414
|
return result
|
|
423
415
|
|
|
424
|
-
def iterate_transactions(self, evse_id: str, newest_first: bool = True) ->
|
|
416
|
+
async def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> AsyncIterable[dict[str, Any]]:
|
|
425
417
|
"""
|
|
426
418
|
Iterate through your transactions.
|
|
427
419
|
|
|
@@ -450,8 +442,9 @@ class BlueCurrentClient:
|
|
|
450
442
|
"""
|
|
451
443
|
next_page = 1
|
|
452
444
|
while next_page is not None:
|
|
453
|
-
transactions = self.get_transactions(evse_id=evse_id, newest_first=newest_first, page=next_page)
|
|
454
|
-
|
|
445
|
+
transactions = await self.get_transactions(evse_id=evse_id, newest_first=newest_first, page=next_page)
|
|
446
|
+
for tx in transactions["transactions"]: # type: ignore
|
|
447
|
+
yield tx
|
|
455
448
|
next_page = transactions["next_page"] # type: ignore
|
|
456
449
|
|
|
457
450
|
async def _login(self) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybluecurrent
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Python client for BlueCurrent charge points.
|
|
5
5
|
Author-email: Rogier van der Geer <rogier@vander-geer.nl>
|
|
6
6
|
License: MIT
|
|
@@ -15,9 +15,9 @@ Requires-Python: >=3.10
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: asyncio-multisubscriber-queue>=0.4.1
|
|
18
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: httpx>=0.28
|
|
19
19
|
Requires-Dist: sjcl>=0.2.1
|
|
20
|
-
Requires-Dist: websockets>=
|
|
20
|
+
Requires-Dist: websockets>=14.0
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: black==23.3.0; extra == "dev"
|
|
23
23
|
Requires-Dist: pre-commit>=3.3.3; extra == "dev"
|
|
@@ -43,36 +43,32 @@ from pybluecurrent import BlueCurrentClient
|
|
|
43
43
|
client = BlueCurrentClient("your_username", "your_secret_password")
|
|
44
44
|
|
|
45
45
|
async with client:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
transactions = client.get_transactions(charge_points[0]["evse_id"])
|
|
46
|
+
charge_points = await client.get_charge_points()
|
|
47
|
+
transactions = await client.get_transactions(charge_points[0]["evse_id"])
|
|
49
48
|
```
|
|
50
49
|
|
|
51
50
|
## Methods
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
As a result, the `BlueCurrentClient` also exposes synchronous as well as asynchronous methods:
|
|
52
|
+
The `BlueCurrentClient` exposes the following methods:
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
The async methods can only be used when the websocket client is connected. For example:
|
|
54
|
+
- [`get_account`](#getaccount---get-your-account-information)
|
|
55
|
+
- [`get_charge_cards`](#getchargecards---get-your-charge-cards)
|
|
56
|
+
- [`get_charge_points`](#getchargepoints---get-your-charge-points)
|
|
57
|
+
- [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
|
|
58
|
+
- [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
|
|
59
|
+
- [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
|
|
60
|
+
- [`set_plug_and_charge_charge_card`](#setplugandchargechargecard---set-the-charge-card-for-plug-and-charge)
|
|
61
|
+
- [`set_status`](#setstatus---enable-or-disable-a-charge-point)
|
|
62
|
+
- [`login`](#login---log-in)
|
|
63
|
+
- [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
|
|
64
|
+
- [`get_contracts`](#getcontracts---get-your-contracts)
|
|
65
|
+
- [`get_grids`](#getgrids---get-your-grid-connections)
|
|
66
|
+
- [`get_transactions`](#gettransactions---get-a-list-of-transactions)
|
|
67
|
+
- [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
|
|
68
|
+
|
|
69
|
+
### Connection
|
|
70
|
+
|
|
71
|
+
The client can only be used when the websocket client is connected. For example:
|
|
76
72
|
```python
|
|
77
73
|
client = BlueCurrentClient("your_username", "your_secret_password")
|
|
78
74
|
async with client:
|
|
@@ -248,22 +244,10 @@ async def set_status(self, evse_id: str, enabled: bool) -> None
|
|
|
248
244
|
- `evse_id`: The ID of the charge point.
|
|
249
245
|
- `enabled`: Boolean that indicates the desired status.
|
|
250
246
|
|
|
251
|
-
### Synchronous
|
|
252
|
-
|
|
253
|
-
#### `login` - Log in
|
|
254
|
-
```python
|
|
255
|
-
def login(self) -> None
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
This method does not do anything if the client is already logged in.
|
|
259
|
-
Connection to the websocket api (async with client) automatically logs in the client,
|
|
260
|
-
so this endpoint is not needed when using the async API.
|
|
261
|
-
|
|
262
|
-
|
|
263
247
|
#### `get_charge_point_status` - Get the status of a charge point.
|
|
264
248
|
|
|
265
249
|
```python
|
|
266
|
-
def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
|
|
250
|
+
async def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
|
|
267
251
|
```
|
|
268
252
|
|
|
269
253
|
##### Arguments
|
|
@@ -296,7 +280,7 @@ A dictionary with the chargepoint status:
|
|
|
296
280
|
#### `get_contracts` - Get your contracts.
|
|
297
281
|
|
|
298
282
|
```python
|
|
299
|
-
def get_contracts(self) -> list[dict[str, str]]
|
|
283
|
+
async def get_contracts(self) -> list[dict[str, str]]
|
|
300
284
|
```
|
|
301
285
|
|
|
302
286
|
##### Returns
|
|
@@ -316,7 +300,7 @@ A list of dictionaries, each representing a contract:
|
|
|
316
300
|
#### `get_grids` - Get your grid connections.
|
|
317
301
|
|
|
318
302
|
```python
|
|
319
|
-
def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
|
|
303
|
+
async def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
|
|
320
304
|
```
|
|
321
305
|
|
|
322
306
|
##### Returns
|
|
@@ -335,7 +319,7 @@ A list of dictionaries, each representing a grid:
|
|
|
335
319
|
#### `get_transactions` - Get a list of transactions.
|
|
336
320
|
|
|
337
321
|
```python
|
|
338
|
-
def get_transactions(
|
|
322
|
+
async def get_transactions(
|
|
339
323
|
self, evse_id: str, newest_first: bool = True, page: int = 1
|
|
340
324
|
) -> dict[str, int | list[dict[str, Any]]]
|
|
341
325
|
```
|
|
@@ -377,7 +361,7 @@ A dictionary like this:
|
|
|
377
361
|
#### `iterate_transactions` - Iterate through your transactions
|
|
378
362
|
|
|
379
363
|
```python
|
|
380
|
-
def iterate_transactions(self, evse_id: str, newest_first: bool = True) ->
|
|
364
|
+
async def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> AsyncIterable[dict[str, Any]]
|
|
381
365
|
```
|
|
382
366
|
|
|
383
367
|
##### Arguments
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from datetime import date
|
|
2
|
-
from itertools import islice
|
|
3
2
|
from os import environ
|
|
4
3
|
|
|
5
|
-
from pytest import
|
|
4
|
+
from pytest import mark, raises, skip
|
|
6
5
|
|
|
7
6
|
from pybluecurrent import BlueCurrentClient
|
|
8
7
|
from pybluecurrent.exceptions import AuthenticationFailed, BlueCurrentException
|
|
@@ -26,10 +25,6 @@ class TestAuthentication:
|
|
|
26
25
|
async with client:
|
|
27
26
|
pass
|
|
28
27
|
|
|
29
|
-
def test_login(self, client_with_auth: BlueCurrentClient):
|
|
30
|
-
client_with_auth.login()
|
|
31
|
-
assert client_with_auth.token is not None
|
|
32
|
-
|
|
33
28
|
|
|
34
29
|
class TestSocketApi:
|
|
35
30
|
async def test_get_account(self, connected_client: BlueCurrentClient):
|
|
@@ -122,13 +117,13 @@ class TestSocketApi:
|
|
|
122
117
|
|
|
123
118
|
@mark.skipif(environ.get("BLUECURRENT_READ_ONLY", "TRUE") != "FALSE", reason="Running read-only tests.")
|
|
124
119
|
async def test_set_status(self, connected_client: BlueCurrentClient, evse_id: str):
|
|
125
|
-
before_status = connected_client.get_charge_point_status(evse_id=evse_id)
|
|
120
|
+
before_status = await connected_client.get_charge_point_status(evse_id=evse_id)
|
|
126
121
|
if before_status["activity"] != "available":
|
|
127
122
|
skip(reason="Only perform this test if the charge point is available.")
|
|
128
123
|
await connected_client.set_status(evse_id=evse_id, enabled=False)
|
|
129
|
-
assert connected_client.get_charge_point_status(evse_id=evse_id)["activity"] == "unavailable"
|
|
124
|
+
assert (await connected_client.get_charge_point_status(evse_id=evse_id))["activity"] == "unavailable"
|
|
130
125
|
await connected_client.set_status(evse_id=evse_id, enabled=True)
|
|
131
|
-
assert connected_client.get_charge_point_status(evse_id=evse_id)["activity"] == "available"
|
|
126
|
+
assert (await connected_client.get_charge_point_status(evse_id=evse_id))["activity"] == "available"
|
|
132
127
|
|
|
133
128
|
async def test_error(self, connected_client: BlueCurrentClient):
|
|
134
129
|
with raises(BlueCurrentException) as e:
|
|
@@ -137,33 +132,36 @@ class TestSocketApi:
|
|
|
137
132
|
|
|
138
133
|
|
|
139
134
|
class TestRestApi:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
client_with_auth.login()
|
|
143
|
-
return client_with_auth
|
|
144
|
-
|
|
145
|
-
def test_get_contracts(self, authenticated_client: BlueCurrentClient):
|
|
146
|
-
contracts = authenticated_client.get_contracts()
|
|
135
|
+
async def test_get_contracts(self, connected_client: BlueCurrentClient):
|
|
136
|
+
contracts = await connected_client.get_contracts()
|
|
147
137
|
assert len(contracts) > 0
|
|
148
138
|
assert "contract_id" in contracts[0]
|
|
149
139
|
|
|
150
|
-
def test_get_charge_point_status(self,
|
|
151
|
-
status =
|
|
140
|
+
async def test_get_charge_point_status(self, connected_client: BlueCurrentClient, evse_id: str):
|
|
141
|
+
status = await connected_client.get_charge_point_status(evse_id)
|
|
152
142
|
assert status["evse_id"] == evse_id
|
|
153
143
|
assert "activity" in status
|
|
154
144
|
|
|
155
|
-
def test_get_grids(self,
|
|
156
|
-
grids =
|
|
145
|
+
async def test_get_grids(self, connected_client: BlueCurrentClient):
|
|
146
|
+
grids = await connected_client.get_grids()
|
|
157
147
|
assert len(grids) > 0
|
|
158
148
|
assert "id" in grids[0]
|
|
159
149
|
|
|
160
|
-
def test_get_transactions(self,
|
|
161
|
-
transactions =
|
|
150
|
+
async def test_get_transactions(self, connected_client: BlueCurrentClient, evse_id: str):
|
|
151
|
+
transactions = await connected_client.get_transactions(evse_id)
|
|
162
152
|
assert "transactions" in transactions
|
|
163
153
|
|
|
164
|
-
def test_iterate_transactions(self,
|
|
165
|
-
transactions =
|
|
166
|
-
# If there are less than three pages, there might be
|
|
154
|
+
async def test_iterate_transactions(self, connected_client: BlueCurrentClient, evse_id: str):
|
|
155
|
+
transactions = await connected_client.get_transactions(evse_id)
|
|
156
|
+
# If there are less than three pages, there might be fewer than 30.
|
|
167
157
|
if transactions["total_pages"] < 3: # type: ignore
|
|
168
158
|
skip("Not enough transactions.")
|
|
169
|
-
|
|
159
|
+
n_transactions = 0
|
|
160
|
+
# Verify pagination works correctly - we get 30 unique transactions from multiple pages.
|
|
161
|
+
unique_transactions = set()
|
|
162
|
+
async for transaction in connected_client.iterate_transactions(evse_id):
|
|
163
|
+
n_transactions += 1
|
|
164
|
+
unique_transactions.add(transaction["transaction_id"])
|
|
165
|
+
if n_transactions >= 30:
|
|
166
|
+
break
|
|
167
|
+
assert len(unique_transactions) == 30
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|