python-openevse-http 0.2.0__tar.gz → 0.2.2__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.
Files changed (20) hide show
  1. {python_openevse_http-0.2.0/python_openevse_http.egg-info → python_openevse_http-0.2.2}/PKG-INFO +4 -2
  2. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/openevsehttp/__main__.py +16 -6
  3. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2/python_openevse_http.egg-info}/PKG-INFO +4 -2
  4. python_openevse_http-0.2.2/python_openevse_http.egg-info/requires.txt +1 -0
  5. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/setup.py +4 -2
  6. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/tests/test_main.py +49 -12
  7. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/tests/test_main_edge_cases.py +6 -4
  8. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/tests/test_websocket.py +4 -3
  9. python_openevse_http-0.2.0/python_openevse_http.egg-info/requires.txt +0 -2
  10. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/LICENSE +0 -0
  11. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/README.md +0 -0
  12. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/openevsehttp/__init__.py +0 -0
  13. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/openevsehttp/const.py +0 -0
  14. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/openevsehttp/exceptions.py +0 -0
  15. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/openevsehttp/websocket.py +0 -0
  16. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/python_openevse_http.egg-info/SOURCES.txt +0 -0
  17. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/python_openevse_http.egg-info/dependency_links.txt +0 -0
  18. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/python_openevse_http.egg-info/not-zip-safe +0 -0
  19. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/python_openevse_http.egg-info/top_level.txt +0 -0
  20. {python_openevse_http-0.2.0 → python_openevse_http-0.2.2}/setup.cfg +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
7
7
  Author: firstof9
8
8
  Author-email: firstof9@gmail.com
9
+ License: Apache-2.0
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Intended Audience :: Developers
11
12
  Classifier: Natural Language :: English
@@ -14,11 +15,11 @@ Classifier: Programming Language :: Python :: 3.10
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
16
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: License :: OSI Approved :: Apache Software License
17
19
  Requires-Python: >=3.10
18
20
  Description-Content-Type: text/markdown
19
21
  License-File: LICENSE
20
22
  Requires-Dist: aiohttp
21
- Requires-Dist: requests
22
23
  Dynamic: author
23
24
  Dynamic: author-email
24
25
  Dynamic: classifier
@@ -26,6 +27,7 @@ Dynamic: description
26
27
  Dynamic: description-content-type
27
28
  Dynamic: download-url
28
29
  Dynamic: home-page
30
+ Dynamic: license
29
31
  Dynamic: license-file
30
32
  Dynamic: requires-dist
31
33
  Dynamic: requires-python
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import datetime
6
+ from datetime import datetime, timedelta, timezone
7
7
  import json
8
8
  import logging
9
9
  import re
@@ -1144,9 +1144,16 @@ class OpenEVSE:
1144
1144
  return None
1145
1145
 
1146
1146
  @property
1147
- def time(self) -> datetime.datetime | None:
1147
+ def time(self) -> datetime | None:
1148
1148
  """Get the RTC time."""
1149
- return self._status.get("time", None)
1149
+ value = self._status.get("time")
1150
+
1151
+ if value:
1152
+ try:
1153
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
1154
+ except (ValueError, AttributeError):
1155
+ return None
1156
+ return None
1150
1157
 
1151
1158
  @property
1152
1159
  def usage_session(self) -> float:
@@ -1308,11 +1315,14 @@ class OpenEVSE:
1308
1315
  )
1309
1316
 
1310
1317
  @property
1311
- def vehicle_eta(self) -> int | None:
1318
+ def vehicle_eta(self) -> datetime | None:
1312
1319
  """Return time to full charge."""
1313
- return self._status.get(
1314
- "vehicle_eta", self._status.get("time_to_full_charge", None)
1320
+ value = self._status.get(
1321
+ "time_to_full_charge", self._status.get("vehicle_eta", None)
1315
1322
  )
1323
+ if value is not None:
1324
+ return datetime.now(timezone.utc) + timedelta(seconds=value)
1325
+ return value
1316
1326
 
1317
1327
  # There is currently no min/max amps JSON data
1318
1328
  # available via HTTP API methods
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
7
7
  Author: firstof9
8
8
  Author-email: firstof9@gmail.com
9
+ License: Apache-2.0
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Intended Audience :: Developers
11
12
  Classifier: Natural Language :: English
@@ -14,11 +15,11 @@ Classifier: Programming Language :: Python :: 3.10
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
16
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: License :: OSI Approved :: Apache Software License
17
19
  Requires-Python: >=3.10
18
20
  Description-Content-Type: text/markdown
19
21
  License-File: LICENSE
20
22
  Requires-Dist: aiohttp
21
- Requires-Dist: requests
22
23
  Dynamic: author
23
24
  Dynamic: author-email
24
25
  Dynamic: classifier
@@ -26,6 +27,7 @@ Dynamic: description
26
27
  Dynamic: description-content-type
27
28
  Dynamic: download-url
28
29
  Dynamic: home-page
30
+ Dynamic: license
29
31
  Dynamic: license-file
30
32
  Dynamic: requires-dist
31
33
  Dynamic: requires-python
@@ -6,7 +6,7 @@ from setuptools import find_packages, setup
6
6
 
7
7
  PROJECT_DIR = Path(__file__).parent.resolve()
8
8
  README_FILE = PROJECT_DIR / "README.md"
9
- VERSION = "0.2.0"
9
+ VERSION = "0.2.2"
10
10
 
11
11
  setup(
12
12
  name="python_openevse_http",
@@ -20,7 +20,8 @@ setup(
20
20
  long_description_content_type="text/markdown",
21
21
  packages=find_packages(exclude=["test.*", "tests"]),
22
22
  python_requires=">=3.10",
23
- install_requires=["aiohttp", "requests"],
23
+ install_requires=["aiohttp"],
24
+ license="Apache-2.0",
24
25
  entry_points={},
25
26
  include_package_data=True,
26
27
  zip_safe=False,
@@ -33,5 +34,6 @@ setup(
33
34
  "Programming Language :: Python :: 3.11",
34
35
  "Programming Language :: Python :: 3.12",
35
36
  "Programming Language :: Python :: 3.13",
37
+ "License :: OSI Approved :: Apache Software License",
36
38
  ],
37
39
  )
@@ -8,6 +8,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
8
8
 
9
9
  import aiohttp
10
10
  import pytest
11
+ from datetime import datetime, timezone, timedelta
12
+ from freezegun import freeze_time
11
13
  from aiohttp.client_exceptions import ContentTypeError, ServerTimeoutError
12
14
  from aiohttp.client_reqrep import ConnectionKey
13
15
  from awesomeversion.exceptions import AwesomeVersionCompareException
@@ -488,18 +490,42 @@ async def test_get_esp_temperature(fixture, expected, request):
488
490
 
489
491
 
490
492
  @pytest.mark.parametrize(
491
- "fixture, expected",
493
+ "fixture, expected_str",
492
494
  [("test_charger", "2021-08-10T23:00:11Z"), ("test_charger_v2", None)],
493
495
  )
494
- async def test_get_time(fixture, expected, request):
496
+ async def test_get_time(fixture, expected_str, request):
495
497
  """Test v4 Status reply."""
496
498
  charger = request.getfixturevalue(fixture)
497
499
  await charger.update()
498
- status = charger.time
499
- assert status == expected
500
+
501
+ result = charger.time
502
+
503
+ if expected_str:
504
+ expected_dt = datetime(2021, 8, 10, 23, 0, 11, tzinfo=timezone.utc)
505
+ assert result == expected_dt
506
+ assert isinstance(result, datetime)
507
+ else:
508
+ assert result is None
509
+
500
510
  await charger.ws_disconnect()
501
511
 
502
512
 
513
+ @pytest.mark.parametrize(
514
+ "bad_value",
515
+ [
516
+ "not-a-timestamp",
517
+ 123456789,
518
+ True,
519
+ {"some": "dict"},
520
+ ],
521
+ )
522
+ async def test_time_parsing_errors(test_charger, bad_value):
523
+ """Test that ValueError and AttributeError are caught and return None."""
524
+ test_charger._status["time"] = bad_value
525
+ result = test_charger.time
526
+ assert result is None
527
+
528
+
503
529
  @pytest.mark.parametrize(
504
530
  "fixture, expected",
505
531
  [
@@ -1266,14 +1292,25 @@ async def test_vehicle_range(fixture, expected, request):
1266
1292
 
1267
1293
 
1268
1294
  @pytest.mark.parametrize(
1269
- "fixture, expected", [("test_charger", 18000), ("test_charger_v2", None)]
1295
+ "fixture, expected_seconds", [("test_charger", 18000), ("test_charger_v2", None)]
1270
1296
  )
1271
- async def test_vehicle_eta(fixture, expected, request):
1297
+ @freeze_time("2026-01-09 12:00:00+00:00")
1298
+ async def test_vehicle_eta(fixture, expected_seconds, request):
1272
1299
  """Test vehicle_eta reply."""
1273
1300
  charger = request.getfixturevalue(fixture)
1274
1301
  await charger.update()
1275
- status = charger.vehicle_eta
1276
- assert status == expected
1302
+
1303
+ result = charger.vehicle_eta
1304
+
1305
+ if expected_seconds is not None:
1306
+ # Calculate what the expected datetime should be based on our frozen time
1307
+ expected_datetime = datetime(
1308
+ 2026, 1, 9, 12, 0, 0, tzinfo=timezone.utc
1309
+ ) + timedelta(seconds=expected_seconds)
1310
+ assert result == expected_datetime
1311
+ else:
1312
+ assert result is None
1313
+
1277
1314
  await charger.ws_disconnect()
1278
1315
 
1279
1316
 
@@ -2235,10 +2272,10 @@ async def test_main_auth_instantiation():
2235
2272
  # Ensure session.get() returns the request context
2236
2273
  mock_session.get.return_value = mock_request_ctx
2237
2274
 
2238
- with patch("aiohttp.ClientSession", return_value=mock_session), patch(
2239
- "aiohttp.BasicAuth"
2240
- ) as mock_basic_auth:
2241
-
2275
+ with (
2276
+ patch("aiohttp.ClientSession", return_value=mock_session),
2277
+ patch("aiohttp.BasicAuth") as mock_basic_auth,
2278
+ ):
2242
2279
  await charger.update()
2243
2280
 
2244
2281
  # Verify BasicAuth was instantiated
@@ -33,8 +33,9 @@ async def test_process_request_decode_error(charger, caplog):
33
33
  mock_resp.__aenter__.return_value = mock_resp
34
34
  mock_resp.__aexit__.return_value = None
35
35
 
36
- with patch("aiohttp.ClientSession.get", return_value=mock_resp), caplog.at_level(
37
- logging.DEBUG
36
+ with (
37
+ patch("aiohttp.ClientSession.get", return_value=mock_resp),
38
+ caplog.at_level(logging.DEBUG),
38
39
  ):
39
40
  data = await charger.process_request("http://url", method="get")
40
41
 
@@ -53,8 +54,9 @@ async def test_process_request_http_warnings(charger, caplog):
53
54
  mock_resp.__aenter__.return_value = mock_resp
54
55
  mock_resp.__aexit__.return_value = None
55
56
 
56
- with patch("aiohttp.ClientSession.get", return_value=mock_resp), caplog.at_level(
57
- logging.WARNING
57
+ with (
58
+ patch("aiohttp.ClientSession.get", return_value=mock_resp),
59
+ caplog.at_level(logging.WARNING),
58
60
  ):
59
61
  await charger.process_request("http://url", method="get")
60
62
  # Verify the 404 response body was logged as a warning
@@ -97,9 +97,10 @@ async def test_connection_error_retry(ws_client, mock_callback):
97
97
  """Test connection retry logic."""
98
98
  error = aiohttp.ClientConnectionError("Connection lost")
99
99
 
100
- with patch("aiohttp.ClientSession.ws_connect", side_effect=error), patch(
101
- "asyncio.sleep", new_callable=AsyncMock
102
- ) as mock_sleep:
100
+ with (
101
+ patch("aiohttp.ClientSession.ws_connect", side_effect=error),
102
+ patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
103
+ ):
103
104
  # Simulate one run of 'running' which catches the error and triggers sleep
104
105
  await ws_client.running()
105
106
 
@@ -1,2 +0,0 @@
1
- aiohttp
2
- requests