python-openevse-http 0.4.4__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
- openevsehttp/client.py +39 -32
- openevsehttp/commands.py +7 -7
- openevsehttp/const.py +7 -0
- openevsehttp/websocket.py +15 -8
- {python_openevse_http-0.4.4.dist-info → python_openevse_http-1.0.0.dist-info}/METADATA +13 -17
- {python_openevse_http-0.4.4.dist-info → python_openevse_http-1.0.0.dist-info}/RECORD +9 -9
- {python_openevse_http-0.4.4.dist-info → python_openevse_http-1.0.0.dist-info}/WHEEL +0 -0
- {python_openevse_http-0.4.4.dist-info → python_openevse_http-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {python_openevse_http-0.4.4.dist-info → python_openevse_http-1.0.0.dist-info}/top_level.txt +0 -0
openevsehttp/client.py
CHANGED
|
@@ -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
|
)
|
|
@@ -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:
|
openevsehttp/commands.py
CHANGED
|
@@ -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:
|
openevsehttp/const.py
CHANGED
|
@@ -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
|
+
)
|
openevsehttp/websocket.py
CHANGED
|
@@ -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."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python_openevse_http
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
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())
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
openevsehttp/__init__.py,sha256=I6a1mjOZHYiWb_qfCuDuFLOOncrkkB_7uwybtOIujfY,1165
|
|
2
2
|
openevsehttp/__main__.py,sha256=EHmSdT7GjAVvHQxvLBTjZXsj_V5SB6B2_kpgUAT7mPM,146
|
|
3
|
-
openevsehttp/client.py,sha256=
|
|
4
|
-
openevsehttp/commands.py,sha256=
|
|
5
|
-
openevsehttp/const.py,sha256=
|
|
3
|
+
openevsehttp/client.py,sha256=tw8MGwraUVn5ERbQCR6JRWEvMRc7KnUgJvf_0viOQHo,18983
|
|
4
|
+
openevsehttp/commands.py,sha256=JGxjmGvE2-eUJ1gD76y_701Qk3Kzab-6XtGeaXXDSb0,27243
|
|
5
|
+
openevsehttp/const.py,sha256=9jVzW4CZz7uP7VKbGQT4v0jNeP5PPC55tR-jnG11ODA,1646
|
|
6
6
|
openevsehttp/exceptions.py,sha256=bqz-tHTW1AYJMKcm0s5M6z5tA6XZgjnCiBLW1XrZ_70,672
|
|
7
7
|
openevsehttp/managers.py,sha256=EtQMQziwhoZeqKe2zWY-0yS7zedhuYjYtL5j9xBBCZ0,5380
|
|
8
8
|
openevsehttp/properties.py,sha256=psGGiRacHYs1YYmagiWSlpfua-SnpU2nCviEYDZX0V8,17892
|
|
9
9
|
openevsehttp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
openevsehttp/sensors.py,sha256=yO4Q1sgJkvmfOi5dpoTwEoldsrpqgGz0k9fC7mFWass,4680
|
|
11
11
|
openevsehttp/utils.py,sha256=e3HH_jwZgb1iBWJgIoMOM0JPrQNwXyVdOx5vTWOh4T0,858
|
|
12
|
-
openevsehttp/websocket.py,sha256=
|
|
13
|
-
python_openevse_http-0.
|
|
14
|
-
python_openevse_http-0.
|
|
15
|
-
python_openevse_http-0.
|
|
16
|
-
python_openevse_http-0.
|
|
17
|
-
python_openevse_http-0.
|
|
12
|
+
openevsehttp/websocket.py,sha256=dOSemvm7CM5pFb9RaoJSTIM6Tf5xtWMLEBEccKlBUKI,10517
|
|
13
|
+
python_openevse_http-1.0.0.dist-info/licenses/LICENSE,sha256=hSB6TOQ7rmwSGb6XzqRjDGMvmUj5_GlacqQin3tegtA,11341
|
|
14
|
+
python_openevse_http-1.0.0.dist-info/METADATA,sha256=fYgJfqxD6zmc8b540T6jStiE1AKNUmkqSC2vmHHHRjo,4417
|
|
15
|
+
python_openevse_http-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
+
python_openevse_http-1.0.0.dist-info/top_level.txt,sha256=u8RUkoEIE33Cjn6gmqiEoVpZ0VZ59WJ3FXBwwOg0CPE,13
|
|
17
|
+
python_openevse_http-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
{python_openevse_http-0.4.4.dist-info → python_openevse_http-1.0.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|