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 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
- self._session_external = session is not None
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
- # Use provided session or create a temporary one
90
- if (session := self._session) is None:
91
- async with aiohttp.ClientSession() as session:
92
- return await self._process_request_with_session(
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
- # Detect loop mismatch
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.websocket = OpenEVSEWebsocket(
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: Any
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
- if (session := self._session) is None:
413
- async with aiohttp.ClientSession() as session:
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
- self.session = aiohttp.ClientSession()
230
- self._session_external = False
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.4.4
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
- # Initialize the charger
69
- charger = OpenEVSE("192.168.1.30")
69
+ async with aiohttp.ClientSession() as session:
70
+ charger = OpenEVSE("192.168.1.30", session=session)
71
+ await charger.update()
70
72
 
71
- # Update state
72
- await charger.update()
73
+ print(f"Charger State: {charger.status}")
74
+ print(f"Current Charge: {charger.charge_current}A")
73
75
 
74
- print(f"Charger State: {charger.status}")
75
- print(f"Current Charge: {charger.charge_current}A")
76
+ if charger.shaper_active:
77
+ print("Shaper is active, disabling...")
78
+ else:
79
+ print("Shaper is inactive, enabling...")
76
80
 
77
- # Toggle the Shaper feature
78
- if charger.shaper_active:
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=8wd56AXEkEanRFWUPkmSDPUY8Zr7oHuNpg3_89m9iAM,18998
4
- openevsehttp/commands.py,sha256=X40x1UaXVfPEiquw7mJGSVA3AbO9gQ4Ay0TLm9i1Iqk,27264
5
- openevsehttp/const.py,sha256=y-2hGv_PCal_-VCSGC7IIyzQYtfeVdq3MjOhBWIdZvc,1440
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=TIMWzkRDs_9EUm3HAOaLWmZEX72BqHPhfDb9aIkz3uY,10409
13
- python_openevse_http-0.4.4.dist-info/licenses/LICENSE,sha256=hSB6TOQ7rmwSGb6XzqRjDGMvmUj5_GlacqQin3tegtA,11341
14
- python_openevse_http-0.4.4.dist-info/METADATA,sha256=RNnswPvgDpWIY5ehHia-XDxl4EGhZe1yIRquF0ZQP8M,4383
15
- python_openevse_http-0.4.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
- python_openevse_http-0.4.4.dist-info/top_level.txt,sha256=u8RUkoEIE33Cjn6gmqiEoVpZ0VZ59WJ3FXBwwOg0CPE,13
17
- python_openevse_http-0.4.4.dist-info/RECORD,,
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,,