python-openevse-http 0.4.4__tar.gz → 1.0.1__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.
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.github/workflows/autolabeler.yml +2 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.github/workflows/links.yml +1 -1
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.github/workflows/publish-to-pypi.yml +2 -2
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.github/workflows/test.yml +3 -3
- python_openevse_http-1.0.1/EXTERNAL_SESSION.md +91 -0
- {python_openevse_http-0.4.4/python_openevse_http.egg-info → python_openevse_http-1.0.1}/PKG-INFO +13 -17
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/README.md +12 -16
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/example_external_session.py +0 -13
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/client.py +43 -36
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/commands.py +7 -7
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/const.py +7 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/properties.py +6 -3
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/websocket.py +15 -8
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1/python_openevse_http.egg-info}/PKG-INFO +13 -17
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/requirements_test.txt +1 -1
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/conftest.py +140 -143
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/test_client.py +197 -143
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/test_commands.py +4 -1
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/test_external_session.py +12 -26
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/test_main_edge_cases.py +6 -3
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/test_mixins.py +2 -0
- python_openevse_http-1.0.1/tests/test_properties.py +492 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/test_shaper.py +2 -1
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/test_websocket.py +36 -24
- python_openevse_http-0.4.4/EXTERNAL_SESSION.md +0 -141
- python_openevse_http-0.4.4/tests/test_properties.py +0 -1236
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.github/dependabot.yml +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.github/release-drafter.yml +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.github/workflows/release-drafter.yml +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.gitignore +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.pre-commit-config.yaml +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.yamllint +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/LICENSE +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/codecov.yml +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/__init__.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/__main__.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/exceptions.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/managers.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/py.typed +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/sensors.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/openevsehttp/utils.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/pyproject.toml +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/python_openevse_http.egg-info/SOURCES.txt +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/python_openevse_http.egg-info/dependency_links.txt +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/python_openevse_http.egg-info/not-zip-safe +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/python_openevse_http.egg-info/requires.txt +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/python_openevse_http.egg-info/top_level.txt +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/requirements.txt +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/requirements_lint.txt +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/setup.cfg +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/setup.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/__init__.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/common.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/github_v2.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/github_v4.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v2_json/config.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v2_json/status.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/config-broken-semver.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/config-broken.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/config-dev.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/config-extra-version.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/config-new.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/config-unknown-semver.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/config.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/schedule.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/status-broken.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/status-new.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/v4_json/status.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/fixtures/websocket.json +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/test_managers.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tests/test_sensors.py +0 -0
- {python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/tox.ini +0 -0
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
with:
|
|
20
20
|
egress-policy: audit
|
|
21
21
|
|
|
22
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
23
23
|
|
|
24
24
|
- name: Link Checker
|
|
25
25
|
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
|
{python_openevse_http-0.4.4 → python_openevse_http-1.0.1}/.github/workflows/publish-to-pypi.yml
RENAMED
|
@@ -25,13 +25,13 @@ jobs:
|
|
|
25
25
|
with:
|
|
26
26
|
egress-policy: audit
|
|
27
27
|
|
|
28
|
-
- uses: actions/checkout@
|
|
28
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
29
29
|
with:
|
|
30
30
|
ref: ${{ inputs.tag || github.ref }}
|
|
31
31
|
fetch-depth: 0
|
|
32
32
|
|
|
33
33
|
- name: Install uv
|
|
34
|
-
uses: astral-sh/setup-uv@
|
|
34
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
35
35
|
with:
|
|
36
36
|
enable-cache: true
|
|
37
37
|
version: "0.10.9"
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
with:
|
|
23
23
|
egress-policy: audit
|
|
24
24
|
|
|
25
|
-
- uses: actions/checkout@
|
|
25
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
26
26
|
- name: Set up Python
|
|
27
27
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
28
28
|
with:
|
|
@@ -45,7 +45,7 @@ jobs:
|
|
|
45
45
|
with:
|
|
46
46
|
egress-policy: audit
|
|
47
47
|
|
|
48
|
-
- uses: actions/checkout@
|
|
48
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
49
49
|
with:
|
|
50
50
|
fetch-depth: 2
|
|
51
51
|
- name: Set up Python ${{ matrix.python-version }}
|
|
@@ -74,7 +74,7 @@ jobs:
|
|
|
74
74
|
egress-policy: audit
|
|
75
75
|
|
|
76
76
|
- name: Check out the repository
|
|
77
|
-
uses: actions/checkout@
|
|
77
|
+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
78
78
|
with:
|
|
79
79
|
fetch-depth: 2
|
|
80
80
|
- name: Download coverage data
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# HTTP Session Management
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `python-openevse-http` library requires you to pass an external `aiohttp.ClientSession` to `OpenEVSE`. Session ownership stays with the caller, so the library no longer constructs temporary HTTP clients internally.
|
|
6
|
+
|
|
7
|
+
## Benefits
|
|
8
|
+
|
|
9
|
+
- **Session Reuse**: Share a single session across multiple OpenEVSE instances or other `aiohttp` clients
|
|
10
|
+
- **Custom Configuration**: Configure timeouts, connectors, proxies, and SSL behavior yourself
|
|
11
|
+
- **Resource Management**: Keep connection pooling and cleanup in one place
|
|
12
|
+
- **Predictable Lifecycle**: Avoid hidden session creation inside request and websocket code paths
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Basic Usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import aiohttp
|
|
20
|
+
from openevsehttp import OpenEVSE
|
|
21
|
+
|
|
22
|
+
async def main():
|
|
23
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
24
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
25
|
+
charger = OpenEVSE("openevse.local", session=session)
|
|
26
|
+
await charger.update()
|
|
27
|
+
print(f"Status: {charger.status}")
|
|
28
|
+
await charger.ws_disconnect()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Sharing a Session
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import aiohttp
|
|
35
|
+
from openevsehttp import OpenEVSE
|
|
36
|
+
|
|
37
|
+
async def main():
|
|
38
|
+
async with aiohttp.ClientSession() as session:
|
|
39
|
+
charger1 = OpenEVSE("charger1.local", session=session)
|
|
40
|
+
charger2 = OpenEVSE("charger2.local", session=session)
|
|
41
|
+
|
|
42
|
+
await charger1.update()
|
|
43
|
+
await charger2.update()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Websocket Startup
|
|
47
|
+
|
|
48
|
+
Start websocket listening from the same event loop that owns the
|
|
49
|
+
`aiohttp.ClientSession`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import aiohttp
|
|
53
|
+
from openevsehttp import OpenEVSE
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
async with aiohttp.ClientSession() as session:
|
|
57
|
+
charger = OpenEVSE("openevse.local", session=session)
|
|
58
|
+
await charger.ws_start()
|
|
59
|
+
await charger.ws_disconnect()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`ws_start()` is async so websocket tasks are created on the event loop that owns
|
|
63
|
+
the configured `aiohttp.ClientSession`. This prevents using a session from a
|
|
64
|
+
private background loop it was not created on.
|
|
65
|
+
|
|
66
|
+
## API Notes
|
|
67
|
+
|
|
68
|
+
- `OpenEVSE(..., session=session)` uses the provided session for HTTP requests.
|
|
69
|
+
- `OpenEVSEWebsocket(..., session=session)` uses the provided session for websocket connections.
|
|
70
|
+
- If no session is configured, HTTP requests and websocket startup raise `RuntimeError`.
|
|
71
|
+
- Call `await charger.ws_start()` from the event loop that owns the session.
|
|
72
|
+
- Externally provided sessions are never closed by the library.
|
|
73
|
+
|
|
74
|
+
## Migration
|
|
75
|
+
|
|
76
|
+
Before:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
charger = OpenEVSE("openevse.local")
|
|
80
|
+
await charger.update()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
After:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import aiohttp
|
|
87
|
+
|
|
88
|
+
async with aiohttp.ClientSession() as session:
|
|
89
|
+
charger = OpenEVSE("openevse.local", session=session)
|
|
90
|
+
await charger.update()
|
|
91
|
+
```
|
{python_openevse_http-0.4.4/python_openevse_http.egg-info → python_openevse_http-1.0.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python_openevse_http
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Python wrapper for OpenEVSE HTTP API
|
|
5
5
|
Home-page: https://github.com/firstof9/python-openevse-http
|
|
6
6
|
Download-URL: https://github.com/firstof9/python-openevse-http
|
|
@@ -62,28 +62,24 @@ pip install python_openevse_http
|
|
|
62
62
|
|
|
63
63
|
```python
|
|
64
64
|
import asyncio
|
|
65
|
+
import aiohttp
|
|
65
66
|
from openevsehttp import OpenEVSE
|
|
66
67
|
|
|
67
68
|
async def main():
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
async with aiohttp.ClientSession() as session:
|
|
70
|
+
charger = OpenEVSE("192.168.1.30", session=session)
|
|
71
|
+
await charger.update()
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
print(f"Charger State: {charger.status}")
|
|
74
|
+
print(f"Current Charge: {charger.charge_current}A")
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
if charger.shaper_active:
|
|
77
|
+
print("Shaper is active, disabling...")
|
|
78
|
+
else:
|
|
79
|
+
print("Shaper is inactive, enabling...")
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
print("Shaper is active, disabling...")
|
|
80
|
-
else:
|
|
81
|
-
print("Shaper is inactive, enabling...")
|
|
82
|
-
|
|
83
|
-
await charger.toggle_shaper()
|
|
84
|
-
|
|
85
|
-
# Clean up
|
|
86
|
-
await charger.close()
|
|
81
|
+
await charger.toggle_shaper()
|
|
82
|
+
await charger.ws_disconnect()
|
|
87
83
|
|
|
88
84
|
if __name__ == "__main__":
|
|
89
85
|
asyncio.run(main())
|
|
@@ -29,28 +29,24 @@ pip install python_openevse_http
|
|
|
29
29
|
|
|
30
30
|
```python
|
|
31
31
|
import asyncio
|
|
32
|
+
import aiohttp
|
|
32
33
|
from openevsehttp import OpenEVSE
|
|
33
34
|
|
|
34
35
|
async def main():
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
async with aiohttp.ClientSession() as session:
|
|
37
|
+
charger = OpenEVSE("192.168.1.30", session=session)
|
|
38
|
+
await charger.update()
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
print(f"Charger State: {charger.status}")
|
|
41
|
+
print(f"Current Charge: {charger.charge_current}A")
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
if charger.shaper_active:
|
|
44
|
+
print("Shaper is active, disabling...")
|
|
45
|
+
else:
|
|
46
|
+
print("Shaper is inactive, enabling...")
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
print("Shaper is active, disabling...")
|
|
47
|
-
else:
|
|
48
|
-
print("Shaper is inactive, enabling...")
|
|
49
|
-
|
|
50
|
-
await charger.toggle_shaper()
|
|
51
|
-
|
|
52
|
-
# Clean up
|
|
53
|
-
await charger.close()
|
|
48
|
+
await charger.toggle_shaper()
|
|
49
|
+
await charger.ws_disconnect()
|
|
54
50
|
|
|
55
51
|
if __name__ == "__main__":
|
|
56
52
|
asyncio.run(main())
|
|
@@ -31,19 +31,6 @@ async def example_with_external_session():
|
|
|
31
31
|
await charger.ws_disconnect()
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
async def example_without_external_session():
|
|
35
|
-
"""Demonstrate without external session (backward compatible)."""
|
|
36
|
-
# The library will create and manage its own sessions
|
|
37
|
-
charger = OpenEVSE("openevse.local")
|
|
38
|
-
|
|
39
|
-
# Use the charger normally
|
|
40
|
-
await charger.update()
|
|
41
|
-
print(f"Status: {charger.status}")
|
|
42
|
-
print(f"Current: {charger.charging_current}A")
|
|
43
|
-
|
|
44
|
-
await charger.ws_disconnect()
|
|
45
|
-
|
|
46
|
-
|
|
47
34
|
async def example_shared_session():
|
|
48
35
|
"""Demonstrate sharing a session between multiple clients."""
|
|
49
36
|
async with aiohttp.ClientSession() as session:
|
|
@@ -17,6 +17,8 @@ from awesomeversion.exceptions import AwesomeVersionCompareException
|
|
|
17
17
|
|
|
18
18
|
from .commands import CommandsMixin
|
|
19
19
|
from .const import (
|
|
20
|
+
ERROR_SESSION_LOOP_MISMATCH,
|
|
21
|
+
ERROR_SESSION_REQUIRED,
|
|
20
22
|
ERROR_TIMEOUT,
|
|
21
23
|
UPDATE_TRIGGERS,
|
|
22
24
|
)
|
|
@@ -48,13 +50,13 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
|
|
|
48
50
|
def __init__(
|
|
49
51
|
self,
|
|
50
52
|
host: str,
|
|
51
|
-
user: str =
|
|
52
|
-
pwd: str =
|
|
53
|
+
user: str | None = None,
|
|
54
|
+
pwd: str | None = None,
|
|
53
55
|
session: aiohttp.ClientSession | None = None,
|
|
54
56
|
) -> None:
|
|
55
57
|
"""Connect to an OpenEVSE charger equipped with wifi or ethernet."""
|
|
56
|
-
self._user = user
|
|
57
|
-
self._pwd = pwd
|
|
58
|
+
self._user = user or ""
|
|
59
|
+
self._pwd = pwd or ""
|
|
58
60
|
self.url = f"http://{host}/"
|
|
59
61
|
self._status: dict[str, Any] = {}
|
|
60
62
|
self._config: dict[str, Any] = {}
|
|
@@ -68,7 +70,17 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
|
|
|
68
70
|
self._owns_loop = False
|
|
69
71
|
self._loop_thread: threading.Thread | None = None
|
|
70
72
|
self._session = session
|
|
71
|
-
|
|
73
|
+
|
|
74
|
+
def _get_session(self) -> aiohttp.ClientSession:
|
|
75
|
+
"""Return the configured HTTP session or fail fast."""
|
|
76
|
+
if self._session is None:
|
|
77
|
+
raise RuntimeError(ERROR_SESSION_REQUIRED)
|
|
78
|
+
try:
|
|
79
|
+
loop = asyncio.get_running_loop()
|
|
80
|
+
except RuntimeError:
|
|
81
|
+
return self._session
|
|
82
|
+
self._validate_session_loop(loop)
|
|
83
|
+
return self._session
|
|
72
84
|
|
|
73
85
|
async def process_request(
|
|
74
86
|
self,
|
|
@@ -86,16 +98,10 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
|
|
|
86
98
|
if self._user and self._pwd:
|
|
87
99
|
auth = aiohttp.BasicAuth(self._user, self._pwd)
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
session, url, method, data, rapi, auth
|
|
94
|
-
)
|
|
95
|
-
else:
|
|
96
|
-
return await self._process_request_with_session(
|
|
97
|
-
session, url, method, data, rapi, auth
|
|
98
|
-
)
|
|
101
|
+
session = self._get_session()
|
|
102
|
+
return await self._process_request_with_session(
|
|
103
|
+
session, url, method, data, rapi, auth
|
|
104
|
+
)
|
|
99
105
|
|
|
100
106
|
def _normalize_response(self, response: Any) -> dict[str, Any] | list[Any]:
|
|
101
107
|
"""Normalize response to a dict or list."""
|
|
@@ -259,36 +265,37 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
|
|
|
259
265
|
data = {"serial": serial, "model": model}
|
|
260
266
|
return data
|
|
261
267
|
|
|
262
|
-
def ws_start(self) -> None:
|
|
268
|
+
async def ws_start(self) -> None:
|
|
263
269
|
"""Start the websocket listener."""
|
|
264
270
|
if self.websocket and self.websocket.state != STATE_STOPPED:
|
|
265
271
|
raise AlreadyListening
|
|
266
272
|
|
|
267
|
-
|
|
268
|
-
use_session = self._session
|
|
269
|
-
try:
|
|
270
|
-
asyncio.get_running_loop()
|
|
271
|
-
except RuntimeError:
|
|
272
|
-
# We are about to create a private loop in _start_listening
|
|
273
|
-
# If we have a session, it's likely bound to another loop
|
|
274
|
-
if self._session:
|
|
275
|
-
_LOGGER.warning(
|
|
276
|
-
"Caller-provided session may not work on private event loop. "
|
|
277
|
-
"Creating a loop-local session."
|
|
278
|
-
)
|
|
279
|
-
use_session = None
|
|
280
|
-
# Clear self._session so subsequent await self.update() uses
|
|
281
|
-
# a loop-local session as well.
|
|
282
|
-
self._session = None
|
|
283
|
-
self._session_external = False
|
|
273
|
+
self._get_session()
|
|
284
274
|
|
|
285
275
|
if not self.websocket or self.websocket.state == STATE_STOPPED:
|
|
286
|
-
self.
|
|
287
|
-
self.url, self._update_status, self._user, self._pwd, use_session
|
|
288
|
-
)
|
|
276
|
+
self._create_websocket()
|
|
289
277
|
|
|
290
278
|
self._start_listening()
|
|
291
279
|
|
|
280
|
+
def _create_websocket(self) -> None:
|
|
281
|
+
"""Create a websocket using the configured session."""
|
|
282
|
+
self.websocket = OpenEVSEWebsocket(
|
|
283
|
+
self.url,
|
|
284
|
+
self._update_status,
|
|
285
|
+
self._user,
|
|
286
|
+
self._pwd,
|
|
287
|
+
self._session,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def _validate_session_loop(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
291
|
+
"""Ensure the configured session belongs to the active event loop."""
|
|
292
|
+
session_loop = getattr(self._session, "_loop", None)
|
|
293
|
+
if (
|
|
294
|
+
isinstance(session_loop, asyncio.AbstractEventLoop)
|
|
295
|
+
and session_loop is not loop
|
|
296
|
+
):
|
|
297
|
+
raise RuntimeError(ERROR_SESSION_LOOP_MISMATCH)
|
|
298
|
+
|
|
292
299
|
def _start_listening(self) -> None:
|
|
293
300
|
"""Start the websocket listener."""
|
|
294
301
|
if not self._loop:
|
|
@@ -25,7 +25,7 @@ class CommandsMixin:
|
|
|
25
25
|
url: str
|
|
26
26
|
_status: dict[str, Any]
|
|
27
27
|
_config: dict[str, Any]
|
|
28
|
-
_session:
|
|
28
|
+
_session: aiohttp.ClientSession | None
|
|
29
29
|
|
|
30
30
|
# These are defined in client.py
|
|
31
31
|
def _version_check(self, min_version: str, max_version: str = "") -> bool:
|
|
@@ -46,6 +46,10 @@ class CommandsMixin:
|
|
|
46
46
|
"""Normalize response to a dict or list."""
|
|
47
47
|
raise NotImplementedError
|
|
48
48
|
|
|
49
|
+
def _get_session(self) -> aiohttp.ClientSession:
|
|
50
|
+
"""Return the configured HTTP session."""
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
49
53
|
def _flag_ota_if_started(self, response: Any) -> None:
|
|
50
54
|
"""Flag OTA as active if response indicates firmware update has started."""
|
|
51
55
|
normalized = self._normalize_response(response)
|
|
@@ -409,12 +413,8 @@ class CommandsMixin:
|
|
|
409
413
|
return None
|
|
410
414
|
|
|
411
415
|
try:
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return await self._firmware_check_with_session(session, url, method)
|
|
415
|
-
else:
|
|
416
|
-
return await self._firmware_check_with_session(session, url, method)
|
|
417
|
-
|
|
416
|
+
session = self._get_session()
|
|
417
|
+
return await self._firmware_check_with_session(session, url, method)
|
|
418
418
|
except (TimeoutError, ServerTimeoutError):
|
|
419
419
|
_LOGGER.error("%s: %s", "Timeout while updating", url)
|
|
420
420
|
except ContentTypeError as err:
|
|
@@ -62,3 +62,10 @@ RAPI_ERRORS = [
|
|
|
62
62
|
]
|
|
63
63
|
|
|
64
64
|
SUCCESS_ANSWERS = ["OK", "done", "no change", "Created", "Updated", "Deleted"]
|
|
65
|
+
|
|
66
|
+
ERROR_SESSION_REQUIRED = (
|
|
67
|
+
"An aiohttp.ClientSession must be provided via the session argument."
|
|
68
|
+
)
|
|
69
|
+
ERROR_SESSION_LOOP_MISMATCH = (
|
|
70
|
+
"The aiohttp.ClientSession is bound to a different event loop."
|
|
71
|
+
)
|
|
@@ -84,9 +84,12 @@ class PropertiesMixin:
|
|
|
84
84
|
return bool(self._config.get("relayt", False))
|
|
85
85
|
|
|
86
86
|
@property
|
|
87
|
-
def service_level(self) ->
|
|
88
|
-
"""Return the service level (1, 2, or 'A')."""
|
|
89
|
-
|
|
87
|
+
def service_level(self) -> str | None:
|
|
88
|
+
"""Return the service level ('1', '2', or 'A')."""
|
|
89
|
+
value = self._config.get("service")
|
|
90
|
+
if value is None:
|
|
91
|
+
return value
|
|
92
|
+
return str(value)
|
|
90
93
|
|
|
91
94
|
@property
|
|
92
95
|
def openevse_firmware(self) -> str | None:
|
|
@@ -11,6 +11,11 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
import aiohttp
|
|
13
13
|
|
|
14
|
+
from .const import (
|
|
15
|
+
ERROR_SESSION_LOOP_MISMATCH,
|
|
16
|
+
ERROR_SESSION_REQUIRED,
|
|
17
|
+
)
|
|
18
|
+
|
|
14
19
|
_LOGGER = logging.getLogger(__name__)
|
|
15
20
|
|
|
16
21
|
MAX_FAILED_ATTEMPTS = 5
|
|
@@ -40,7 +45,6 @@ class OpenEVSEWebsocket:
|
|
|
40
45
|
) -> None:
|
|
41
46
|
"""Initialize a OpenEVSEWebsocket instance."""
|
|
42
47
|
self.session = session
|
|
43
|
-
self._session_external = session is not None
|
|
44
48
|
self.uri = self._get_uri(server)
|
|
45
49
|
self._user = user
|
|
46
50
|
self._password = password
|
|
@@ -224,10 +228,17 @@ class OpenEVSEWebsocket:
|
|
|
224
228
|
self._listener_loop = None
|
|
225
229
|
|
|
226
230
|
async def _ensure_session(self) -> None:
|
|
227
|
-
"""Ensure aiohttp.ClientSession exists."""
|
|
231
|
+
"""Ensure an external aiohttp.ClientSession exists."""
|
|
228
232
|
if self.session is None:
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
raise RuntimeError(ERROR_SESSION_REQUIRED)
|
|
234
|
+
|
|
235
|
+
loop = asyncio.get_running_loop()
|
|
236
|
+
session_loop = getattr(self.session, "_loop", None)
|
|
237
|
+
if (
|
|
238
|
+
isinstance(session_loop, asyncio.AbstractEventLoop)
|
|
239
|
+
and session_loop is not loop
|
|
240
|
+
):
|
|
241
|
+
raise RuntimeError(ERROR_SESSION_LOOP_MISMATCH)
|
|
231
242
|
|
|
232
243
|
async def close(self) -> None:
|
|
233
244
|
"""Close the listening websocket."""
|
|
@@ -242,10 +253,6 @@ class OpenEVSEWebsocket:
|
|
|
242
253
|
if self._client is not None:
|
|
243
254
|
await self._client.close()
|
|
244
255
|
self._client = None
|
|
245
|
-
# Only close the session if we created it
|
|
246
|
-
if not self._session_external and self.session is not None:
|
|
247
|
-
await self.session.close()
|
|
248
|
-
self.session = None
|
|
249
256
|
|
|
250
257
|
async def keepalive(self) -> None:
|
|
251
258
|
"""Send ping requests to websocket."""
|
{python_openevse_http-0.4.4 → python_openevse_http-1.0.1/python_openevse_http.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python_openevse_http
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Python wrapper for OpenEVSE HTTP API
|
|
5
5
|
Home-page: https://github.com/firstof9/python-openevse-http
|
|
6
6
|
Download-URL: https://github.com/firstof9/python-openevse-http
|
|
@@ -62,28 +62,24 @@ pip install python_openevse_http
|
|
|
62
62
|
|
|
63
63
|
```python
|
|
64
64
|
import asyncio
|
|
65
|
+
import aiohttp
|
|
65
66
|
from openevsehttp import OpenEVSE
|
|
66
67
|
|
|
67
68
|
async def main():
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
async with aiohttp.ClientSession() as session:
|
|
70
|
+
charger = OpenEVSE("192.168.1.30", session=session)
|
|
71
|
+
await charger.update()
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
print(f"Charger State: {charger.status}")
|
|
74
|
+
print(f"Current Charge: {charger.charge_current}A")
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
if charger.shaper_active:
|
|
77
|
+
print("Shaper is active, disabling...")
|
|
78
|
+
else:
|
|
79
|
+
print("Shaper is inactive, enabling...")
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
print("Shaper is active, disabling...")
|
|
80
|
-
else:
|
|
81
|
-
print("Shaper is inactive, enabling...")
|
|
82
|
-
|
|
83
|
-
await charger.toggle_shaper()
|
|
84
|
-
|
|
85
|
-
# Clean up
|
|
86
|
-
await charger.close()
|
|
81
|
+
await charger.toggle_shaper()
|
|
82
|
+
await charger.ws_disconnect()
|
|
87
83
|
|
|
88
84
|
if __name__ == "__main__":
|
|
89
85
|
asyncio.run(main())
|