python-openevse-http 0.2.3__tar.gz → 0.2.5__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.2.3/python_openevse_http.egg-info → python_openevse_http-0.2.5}/PKG-INFO +1 -1
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/openevsehttp/__main__.py +101 -8
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/openevsehttp/websocket.py +37 -15
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5/python_openevse_http.egg-info}/PKG-INFO +1 -1
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/python_openevse_http.egg-info/SOURCES.txt +1 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/setup.py +1 -1
- python_openevse_http-0.2.5/tests/test_external_session.py +199 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/tests/test_main.py +916 -5
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/tests/test_websocket.py +26 -2
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/LICENSE +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/README.md +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/openevsehttp/__init__.py +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/openevsehttp/const.py +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/openevsehttp/exceptions.py +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/python_openevse_http.egg-info/dependency_links.txt +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/python_openevse_http.egg-info/not-zip-safe +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/python_openevse_http.egg-info/requires.txt +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/python_openevse_http.egg-info/top_level.txt +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/setup.cfg +0 -0
- {python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/tests/test_main_edge_cases.py +0 -0
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
from datetime import datetime, timedelta, timezone
|
|
7
6
|
import json
|
|
8
7
|
import logging
|
|
9
8
|
import re
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
10
|
from typing import Any, Callable, Dict, Union
|
|
11
11
|
|
|
12
12
|
import aiohttp # type: ignore
|
|
@@ -84,7 +84,13 @@ UPDATE_TRIGGERS = [
|
|
|
84
84
|
class OpenEVSE:
|
|
85
85
|
"""Represent an OpenEVSE charger."""
|
|
86
86
|
|
|
87
|
-
def __init__(
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
host: str,
|
|
90
|
+
user: str = "",
|
|
91
|
+
pwd: str = "",
|
|
92
|
+
session: aiohttp.ClientSession | None = None,
|
|
93
|
+
) -> None:
|
|
88
94
|
"""Connect to an OpenEVSE charger equipped with wifi or ethernet."""
|
|
89
95
|
self._user = user
|
|
90
96
|
self._pwd = pwd
|
|
@@ -97,6 +103,8 @@ class OpenEVSE:
|
|
|
97
103
|
self.callback: Callable | None = None
|
|
98
104
|
self._loop = None
|
|
99
105
|
self.tasks = None
|
|
106
|
+
self._session = session
|
|
107
|
+
self._session_external = session is not None
|
|
100
108
|
|
|
101
109
|
async def process_request(
|
|
102
110
|
self,
|
|
@@ -113,7 +121,9 @@ class OpenEVSE:
|
|
|
113
121
|
if self._user and self._pwd:
|
|
114
122
|
auth = aiohttp.BasicAuth(self._user, self._pwd)
|
|
115
123
|
|
|
116
|
-
|
|
124
|
+
# Use provided session or create a temporary one
|
|
125
|
+
if self._session is not None:
|
|
126
|
+
session = self._session
|
|
117
127
|
http_method = getattr(session, method)
|
|
118
128
|
_LOGGER.debug(
|
|
119
129
|
"Connecting to %s with data: %s rapi: %s using method %s",
|
|
@@ -165,9 +175,59 @@ class OpenEVSE:
|
|
|
165
175
|
except ContentTypeError as err:
|
|
166
176
|
_LOGGER.error("Content error: %s", err.message)
|
|
167
177
|
raise err
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
else:
|
|
179
|
+
async with aiohttp.ClientSession() as session:
|
|
180
|
+
http_method = getattr(session, method)
|
|
181
|
+
_LOGGER.debug(
|
|
182
|
+
"Connecting to %s with data: %s rapi: %s using method %s",
|
|
183
|
+
url,
|
|
184
|
+
data,
|
|
185
|
+
rapi,
|
|
186
|
+
method,
|
|
187
|
+
)
|
|
188
|
+
try:
|
|
189
|
+
async with http_method(
|
|
190
|
+
url,
|
|
191
|
+
data=rapi,
|
|
192
|
+
json=data,
|
|
193
|
+
auth=auth,
|
|
194
|
+
) as resp:
|
|
195
|
+
try:
|
|
196
|
+
message = await resp.text()
|
|
197
|
+
except UnicodeDecodeError:
|
|
198
|
+
_LOGGER.debug("Decoding error")
|
|
199
|
+
message = await resp.read()
|
|
200
|
+
message = message.decode(errors="replace")
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
message = json.loads(message)
|
|
204
|
+
except ValueError:
|
|
205
|
+
_LOGGER.warning("Non JSON response: %s", message)
|
|
206
|
+
|
|
207
|
+
if resp.status == 400:
|
|
208
|
+
index = ""
|
|
209
|
+
if "msg" in message.keys():
|
|
210
|
+
index = "msg"
|
|
211
|
+
elif "error" in message.keys():
|
|
212
|
+
index = "error"
|
|
213
|
+
_LOGGER.error("Error 400: %s", message[index])
|
|
214
|
+
raise ParseJSONError
|
|
215
|
+
if resp.status == 401:
|
|
216
|
+
_LOGGER.error("Authentication error: %s", message)
|
|
217
|
+
raise AuthenticationError
|
|
218
|
+
if resp.status in [404, 405, 500]:
|
|
219
|
+
_LOGGER.warning("%s", message)
|
|
220
|
+
|
|
221
|
+
if method == "post" and "config_version" in message:
|
|
222
|
+
await self.update()
|
|
223
|
+
return message
|
|
224
|
+
|
|
225
|
+
except (TimeoutError, ServerTimeoutError) as err:
|
|
226
|
+
_LOGGER.error("%s: %s", ERROR_TIMEOUT, url)
|
|
227
|
+
raise err
|
|
228
|
+
except ContentTypeError as err:
|
|
229
|
+
_LOGGER.error("Content error: %s", err.message)
|
|
230
|
+
raise err
|
|
171
231
|
|
|
172
232
|
async def send_command(self, command: str) -> tuple:
|
|
173
233
|
"""Send a RAPI command to the charger and parses the response."""
|
|
@@ -204,7 +264,7 @@ class OpenEVSE:
|
|
|
204
264
|
if not self.websocket:
|
|
205
265
|
# Start Websocket listening
|
|
206
266
|
self.websocket = OpenEVSEWebsocket(
|
|
207
|
-
self.url, self._update_status, self._user, self._pwd
|
|
267
|
+
self.url, self._update_status, self._user, self._pwd, self._session
|
|
208
268
|
)
|
|
209
269
|
|
|
210
270
|
async def test_and_get(self) -> dict:
|
|
@@ -573,7 +633,8 @@ class OpenEVSE:
|
|
|
573
633
|
return None
|
|
574
634
|
|
|
575
635
|
try:
|
|
576
|
-
|
|
636
|
+
if self._session:
|
|
637
|
+
session = self._session
|
|
577
638
|
http_method = getattr(session, method)
|
|
578
639
|
_LOGGER.debug(
|
|
579
640
|
"Connecting to %s using method %s",
|
|
@@ -590,6 +651,24 @@ class OpenEVSE:
|
|
|
590
651
|
response["release_notes"] = message["body"]
|
|
591
652
|
response["release_url"] = message["html_url"]
|
|
592
653
|
return response
|
|
654
|
+
else:
|
|
655
|
+
async with aiohttp.ClientSession() as session:
|
|
656
|
+
http_method = getattr(session, method)
|
|
657
|
+
_LOGGER.debug(
|
|
658
|
+
"Connecting to %s using method %s",
|
|
659
|
+
url,
|
|
660
|
+
method,
|
|
661
|
+
)
|
|
662
|
+
async with http_method(url) as resp:
|
|
663
|
+
if resp.status != 200:
|
|
664
|
+
return None
|
|
665
|
+
message = await resp.text()
|
|
666
|
+
message = json.loads(message)
|
|
667
|
+
response = {}
|
|
668
|
+
response["latest_version"] = message["tag_name"]
|
|
669
|
+
response["release_notes"] = message["body"]
|
|
670
|
+
response["release_url"] = message["html_url"]
|
|
671
|
+
return response
|
|
593
672
|
|
|
594
673
|
except (TimeoutError, ServerTimeoutError):
|
|
595
674
|
_LOGGER.error("%s: %s", ERROR_TIMEOUT, url)
|
|
@@ -1233,6 +1312,20 @@ class OpenEVSE:
|
|
|
1233
1312
|
return round(self._status["voltage"] * self._status["amp"], 2)
|
|
1234
1313
|
return None
|
|
1235
1314
|
|
|
1315
|
+
# Shaper HTTP Posting
|
|
1316
|
+
async def set_shaper_live_pwr(self, power: int) -> None:
|
|
1317
|
+
"""Send pushed sensor data to shaper."""
|
|
1318
|
+
if not self._version_check("4.0.0"):
|
|
1319
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
1320
|
+
raise UnsupportedFeature
|
|
1321
|
+
|
|
1322
|
+
url = f"{self.url}status"
|
|
1323
|
+
data = {"shaper_live_pwr": power}
|
|
1324
|
+
|
|
1325
|
+
_LOGGER.debug("Posting shaper data: %s", data)
|
|
1326
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
1327
|
+
_LOGGER.debug("Shaper response: %s", response)
|
|
1328
|
+
|
|
1236
1329
|
# Shaper values
|
|
1237
1330
|
@property
|
|
1238
1331
|
def shaper_active(self) -> bool | None:
|
|
@@ -31,9 +31,11 @@ class OpenEVSEWebsocket:
|
|
|
31
31
|
callback,
|
|
32
32
|
user=None,
|
|
33
33
|
password=None,
|
|
34
|
+
session: aiohttp.ClientSession | None = None,
|
|
34
35
|
):
|
|
35
36
|
"""Initialize a OpenEVSEWebsocket instance."""
|
|
36
|
-
self.session = aiohttp.ClientSession()
|
|
37
|
+
self.session = session if session is not None else aiohttp.ClientSession()
|
|
38
|
+
self._session_external = session is not None
|
|
37
39
|
self.uri = self._get_uri(server)
|
|
38
40
|
self._user = user
|
|
39
41
|
self._password = password
|
|
@@ -51,8 +53,26 @@ class OpenEVSEWebsocket:
|
|
|
51
53
|
return self._state
|
|
52
54
|
|
|
53
55
|
@state.setter
|
|
54
|
-
|
|
55
|
-
"""
|
|
56
|
+
def state(self, value):
|
|
57
|
+
"""Setter that schedules the callback."""
|
|
58
|
+
self._state = value
|
|
59
|
+
_LOGGER.debug("Websocket %s", value)
|
|
60
|
+
# Schedule the callback asynchronously without awaiting here.
|
|
61
|
+
try:
|
|
62
|
+
asyncio.create_task(
|
|
63
|
+
self.callback(SIGNAL_CONNECTION_STATE, value, self._error_reason)
|
|
64
|
+
)
|
|
65
|
+
except RuntimeError:
|
|
66
|
+
# If there's no running loop, schedule safely on the event loop.
|
|
67
|
+
loop = asyncio.get_event_loop()
|
|
68
|
+
loop.call_soon_threadsafe(
|
|
69
|
+
asyncio.create_task,
|
|
70
|
+
self.callback(SIGNAL_CONNECTION_STATE, value, self._error_reason),
|
|
71
|
+
)
|
|
72
|
+
self._error_reason = None
|
|
73
|
+
|
|
74
|
+
async def _set_state(self, value):
|
|
75
|
+
"""Async helper to set the state and await the callback."""
|
|
56
76
|
self._state = value
|
|
57
77
|
_LOGGER.debug("Websocket %s", value)
|
|
58
78
|
await self.callback(SIGNAL_CONNECTION_STATE, value, self._error_reason)
|
|
@@ -65,7 +85,7 @@ class OpenEVSEWebsocket:
|
|
|
65
85
|
|
|
66
86
|
async def running(self):
|
|
67
87
|
"""Open a persistent websocket connection and act on events."""
|
|
68
|
-
await
|
|
88
|
+
await self._set_state(STATE_STARTING)
|
|
69
89
|
auth = None
|
|
70
90
|
|
|
71
91
|
if self._user and self._password:
|
|
@@ -77,7 +97,7 @@ class OpenEVSEWebsocket:
|
|
|
77
97
|
heartbeat=15,
|
|
78
98
|
auth=auth,
|
|
79
99
|
) as ws_client:
|
|
80
|
-
await
|
|
100
|
+
await self._set_state(STATE_CONNECTED)
|
|
81
101
|
self.failed_attempts = 0
|
|
82
102
|
self._client = ws_client
|
|
83
103
|
|
|
@@ -107,11 +127,11 @@ class OpenEVSEWebsocket:
|
|
|
107
127
|
else:
|
|
108
128
|
_LOGGER.error("Unexpected response received: %s", error)
|
|
109
129
|
self._error_reason = error
|
|
110
|
-
await
|
|
130
|
+
await self._set_state(STATE_STOPPED)
|
|
111
131
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as error:
|
|
112
132
|
if self.failed_attempts > MAX_FAILED_ATTEMPTS:
|
|
113
133
|
self._error_reason = ERROR_TOO_MANY_RETRIES
|
|
114
|
-
await
|
|
134
|
+
await self._set_state(STATE_STOPPED)
|
|
115
135
|
elif self.state != STATE_STOPPED:
|
|
116
136
|
retry_delay = min(2 ** (self.failed_attempts - 1) * 30, 300)
|
|
117
137
|
self.failed_attempts += 1
|
|
@@ -120,16 +140,16 @@ class OpenEVSEWebsocket:
|
|
|
120
140
|
retry_delay,
|
|
121
141
|
error,
|
|
122
142
|
)
|
|
123
|
-
await
|
|
143
|
+
await self._set_state(STATE_DISCONNECTED)
|
|
124
144
|
await asyncio.sleep(retry_delay)
|
|
125
145
|
except Exception as error: # pylint: disable=broad-except
|
|
126
146
|
if self.state != STATE_STOPPED:
|
|
127
147
|
_LOGGER.exception("Unexpected exception occurred: %s", error)
|
|
128
148
|
self._error_reason = error
|
|
129
|
-
await
|
|
149
|
+
await self._set_state(STATE_STOPPED)
|
|
130
150
|
else:
|
|
131
151
|
if self.state != STATE_STOPPED:
|
|
132
|
-
await
|
|
152
|
+
await self._set_state(STATE_DISCONNECTED)
|
|
133
153
|
await asyncio.sleep(5)
|
|
134
154
|
|
|
135
155
|
async def listen(self):
|
|
@@ -140,8 +160,10 @@ class OpenEVSEWebsocket:
|
|
|
140
160
|
|
|
141
161
|
async def close(self):
|
|
142
162
|
"""Close the listening websocket."""
|
|
143
|
-
await
|
|
144
|
-
|
|
163
|
+
await self._set_state(STATE_STOPPED)
|
|
164
|
+
# Only close the session if we created it
|
|
165
|
+
if not self._session_external:
|
|
166
|
+
await self.session.close()
|
|
145
167
|
|
|
146
168
|
async def keepalive(self):
|
|
147
169
|
"""Send ping requests to websocket."""
|
|
@@ -151,7 +173,7 @@ class OpenEVSEWebsocket:
|
|
|
151
173
|
# Negitive time should indicate no pong reply so consider the
|
|
152
174
|
# websocket disconnected.
|
|
153
175
|
self._error_reason = ERROR_PING_TIMEOUT
|
|
154
|
-
await
|
|
176
|
+
await self._set_state(STATE_DISCONNECTED)
|
|
155
177
|
|
|
156
178
|
data = {"ping": 1}
|
|
157
179
|
_LOGGER.debug("Sending message: %s to websocket.", data)
|
|
@@ -168,7 +190,7 @@ class OpenEVSEWebsocket:
|
|
|
168
190
|
_LOGGER.error("Error parsing data: %s", err)
|
|
169
191
|
except RuntimeError as err:
|
|
170
192
|
_LOGGER.debug("Websocket connection issue: %s", err)
|
|
171
|
-
await
|
|
193
|
+
await self._set_state(STATE_DISCONNECTED)
|
|
172
194
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
173
195
|
_LOGGER.debug("Problem sending ping request: %s", err)
|
|
174
|
-
await
|
|
196
|
+
await self._set_state(STATE_DISCONNECTED)
|
{python_openevse_http-0.2.3 → python_openevse_http-0.2.5}/python_openevse_http.egg-info/SOURCES.txt
RENAMED
|
@@ -12,6 +12,7 @@ python_openevse_http.egg-info/dependency_links.txt
|
|
|
12
12
|
python_openevse_http.egg-info/not-zip-safe
|
|
13
13
|
python_openevse_http.egg-info/requires.txt
|
|
14
14
|
python_openevse_http.egg-info/top_level.txt
|
|
15
|
+
tests/test_external_session.py
|
|
15
16
|
tests/test_main.py
|
|
16
17
|
tests/test_main_edge_cases.py
|
|
17
18
|
tests/test_websocket.py
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Test external session management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from openevsehttp.__main__ import OpenEVSE
|
|
10
|
+
from tests.common import load_fixture
|
|
11
|
+
|
|
12
|
+
pytestmark = pytest.mark.asyncio
|
|
13
|
+
|
|
14
|
+
TEST_URL_STATUS = "http://openevse.test.tld/status"
|
|
15
|
+
TEST_URL_CONFIG = "http://openevse.test.tld/config"
|
|
16
|
+
TEST_TLD = "openevse.test.tld"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def test_external_session_provided():
|
|
20
|
+
"""Test that an external session is used when provided."""
|
|
21
|
+
# Create a mock session
|
|
22
|
+
mock_session = MagicMock(spec=aiohttp.ClientSession)
|
|
23
|
+
mock_session.closed = False
|
|
24
|
+
|
|
25
|
+
# Mock the response
|
|
26
|
+
mock_response = AsyncMock()
|
|
27
|
+
mock_response.status = 200
|
|
28
|
+
mock_response.text = AsyncMock(return_value=load_fixture("v4_json/status.json"))
|
|
29
|
+
|
|
30
|
+
# Mock the get method to return the response
|
|
31
|
+
mock_get = AsyncMock(return_value=mock_response)
|
|
32
|
+
mock_get.__aenter__ = AsyncMock(return_value=mock_response)
|
|
33
|
+
mock_get.__aexit__ = AsyncMock(return_value=None)
|
|
34
|
+
mock_session.get = MagicMock(return_value=mock_get)
|
|
35
|
+
|
|
36
|
+
# Create OpenEVSE instance with external session
|
|
37
|
+
charger = OpenEVSE(TEST_TLD, session=mock_session)
|
|
38
|
+
|
|
39
|
+
# Verify the session is stored
|
|
40
|
+
assert charger._session is mock_session
|
|
41
|
+
assert charger._session_external is True
|
|
42
|
+
|
|
43
|
+
# Make a request
|
|
44
|
+
await charger.process_request(TEST_URL_STATUS, method="get")
|
|
45
|
+
|
|
46
|
+
# Verify the external session was used
|
|
47
|
+
mock_session.get.assert_called_once()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def test_no_external_session(mock_aioclient):
|
|
51
|
+
"""Test that a temporary session is created when none is provided."""
|
|
52
|
+
mock_aioclient.get(
|
|
53
|
+
TEST_URL_STATUS,
|
|
54
|
+
status=200,
|
|
55
|
+
body=load_fixture("v4_json/status.json"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Create OpenEVSE instance without external session
|
|
59
|
+
charger = OpenEVSE(TEST_TLD)
|
|
60
|
+
|
|
61
|
+
# Verify no session is stored
|
|
62
|
+
assert charger._session is None
|
|
63
|
+
assert charger._session_external is False
|
|
64
|
+
|
|
65
|
+
# Make a request - should create a temporary session
|
|
66
|
+
await charger.process_request(TEST_URL_STATUS, method="get")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def test_external_session_with_update(mock_aioclient):
|
|
70
|
+
"""Test that external session is used during update."""
|
|
71
|
+
mock_aioclient.get(
|
|
72
|
+
TEST_URL_STATUS,
|
|
73
|
+
status=200,
|
|
74
|
+
body=load_fixture("v4_json/status.json"),
|
|
75
|
+
)
|
|
76
|
+
mock_aioclient.get(
|
|
77
|
+
TEST_URL_CONFIG,
|
|
78
|
+
status=200,
|
|
79
|
+
body=load_fixture("v4_json/config.json"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Create a real session for testing
|
|
83
|
+
async with aiohttp.ClientSession() as session:
|
|
84
|
+
# Create OpenEVSE instance with external session
|
|
85
|
+
charger = OpenEVSE(TEST_TLD, session=session)
|
|
86
|
+
|
|
87
|
+
# Verify the session is stored
|
|
88
|
+
assert charger._session is session
|
|
89
|
+
assert charger._session_external is True
|
|
90
|
+
|
|
91
|
+
# Update should use the external session
|
|
92
|
+
await charger.update()
|
|
93
|
+
|
|
94
|
+
# Verify status was updated
|
|
95
|
+
assert charger._status is not None
|
|
96
|
+
assert charger._config is not None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def test_websocket_uses_external_session(mock_aioclient):
|
|
100
|
+
"""Test that websocket uses the external session."""
|
|
101
|
+
mock_aioclient.get(
|
|
102
|
+
TEST_URL_STATUS,
|
|
103
|
+
status=200,
|
|
104
|
+
body=load_fixture("v4_json/status.json"),
|
|
105
|
+
)
|
|
106
|
+
mock_aioclient.get(
|
|
107
|
+
TEST_URL_CONFIG,
|
|
108
|
+
status=200,
|
|
109
|
+
body=load_fixture("v4_json/config.json"),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Create a real session for testing
|
|
113
|
+
async with aiohttp.ClientSession() as session:
|
|
114
|
+
# Create OpenEVSE instance with external session
|
|
115
|
+
charger = OpenEVSE(TEST_TLD, session=session)
|
|
116
|
+
|
|
117
|
+
# Update to initialize websocket
|
|
118
|
+
await charger.update()
|
|
119
|
+
|
|
120
|
+
# Verify websocket was created with the session
|
|
121
|
+
assert charger.websocket is not None
|
|
122
|
+
assert charger.websocket.session is session
|
|
123
|
+
assert charger.websocket._session_external is True
|
|
124
|
+
|
|
125
|
+
# Cleanup
|
|
126
|
+
await charger.ws_disconnect()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def test_firmware_check_with_external_session(mock_aioclient):
|
|
130
|
+
"""Test that firmware_check uses external session."""
|
|
131
|
+
mock_aioclient.get(
|
|
132
|
+
TEST_URL_STATUS,
|
|
133
|
+
status=200,
|
|
134
|
+
body=load_fixture("v4_json/status.json"),
|
|
135
|
+
)
|
|
136
|
+
mock_aioclient.get(
|
|
137
|
+
TEST_URL_CONFIG,
|
|
138
|
+
status=200,
|
|
139
|
+
body=load_fixture("v4_json/config.json"),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
github_response = {
|
|
143
|
+
"tag_name": "v4.2.0",
|
|
144
|
+
"body": "Release notes",
|
|
145
|
+
"html_url": "https://github.com/OpenEVSE/ESP32_WiFi_V4.x/releases/tag/v4.2.0",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
mock_aioclient.get(
|
|
149
|
+
"https://api.github.com/repos/OpenEVSE/ESP32_WiFi_V4.x/releases/latest",
|
|
150
|
+
status=200,
|
|
151
|
+
body=json.dumps(github_response),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Create OpenEVSE instance without external session (use mocked responses)
|
|
155
|
+
charger = OpenEVSE(TEST_TLD)
|
|
156
|
+
|
|
157
|
+
# Load config first
|
|
158
|
+
await charger.update()
|
|
159
|
+
|
|
160
|
+
# Check firmware - should use mocked session
|
|
161
|
+
result = await charger.firmware_check()
|
|
162
|
+
|
|
163
|
+
# Verify result
|
|
164
|
+
assert result is not None
|
|
165
|
+
assert result["latest_version"] == "v4.2.0"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def test_session_not_closed_when_external(mock_aioclient):
|
|
169
|
+
"""Test that external session is not closed by the library."""
|
|
170
|
+
mock_aioclient.get(
|
|
171
|
+
TEST_URL_STATUS,
|
|
172
|
+
status=200,
|
|
173
|
+
body=load_fixture("v4_json/status.json"),
|
|
174
|
+
)
|
|
175
|
+
mock_aioclient.get(
|
|
176
|
+
TEST_URL_CONFIG,
|
|
177
|
+
status=200,
|
|
178
|
+
body=load_fixture("v4_json/config.json"),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Create a real session
|
|
182
|
+
session = aiohttp.ClientSession()
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
# Create OpenEVSE instance with external session
|
|
186
|
+
charger = OpenEVSE(TEST_TLD, session=session)
|
|
187
|
+
|
|
188
|
+
# Update to initialize websocket
|
|
189
|
+
await charger.update()
|
|
190
|
+
|
|
191
|
+
# Disconnect websocket
|
|
192
|
+
await charger.ws_disconnect()
|
|
193
|
+
|
|
194
|
+
# Session should still be open (not closed by library)
|
|
195
|
+
assert not session.closed
|
|
196
|
+
|
|
197
|
+
finally:
|
|
198
|
+
# Clean up the session ourselves
|
|
199
|
+
await session.close()
|