python-openevse-http 0.2.4__tar.gz → 0.2.6__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.4/python_openevse_http.egg-info → python_openevse_http-0.2.6}/PKG-INFO +3 -5
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/openevsehttp/__main__.py +132 -172
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/openevsehttp/websocket.py +1 -1
- python_openevse_http-0.2.6/pyproject.toml +32 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6/python_openevse_http.egg-info}/PKG-INFO +3 -5
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/python_openevse_http.egg-info/SOURCES.txt +1 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/setup.py +3 -5
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/tests/test_external_session.py +1 -2
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/tests/test_main.py +53 -4
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/tests/test_websocket.py +0 -1
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/LICENSE +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/README.md +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/openevsehttp/__init__.py +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/openevsehttp/const.py +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/openevsehttp/exceptions.py +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/python_openevse_http.egg-info/dependency_links.txt +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/python_openevse_http.egg-info/not-zip-safe +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/python_openevse_http.egg-info/requires.txt +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/python_openevse_http.egg-info/top_level.txt +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/setup.cfg +0 -0
- {python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/tests/test_main_edge_cases.py +0 -0
{python_openevse_http-0.2.4/python_openevse_http.egg-info → python_openevse_http-0.2.6}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python_openevse_http
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
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
|
|
@@ -11,12 +11,10 @@ Classifier: Development Status :: 4 - Beta
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Natural Language :: English
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
16
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.13
|
|
20
18
|
Description-Content-Type: text/markdown
|
|
21
19
|
License-File: LICENSE
|
|
22
20
|
Requires-Dist: aiohttp
|
|
@@ -6,8 +6,9 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
import re
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from datetime import datetime, timedelta, timezone
|
|
10
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
11
12
|
|
|
12
13
|
import aiohttp # type: ignore
|
|
13
14
|
from aiohttp.client_exceptions import ContentTypeError, ServerTimeoutError
|
|
@@ -122,112 +123,79 @@ class OpenEVSE:
|
|
|
122
123
|
auth = aiohttp.BasicAuth(self._user, self._pwd)
|
|
123
124
|
|
|
124
125
|
# Use provided session or create a temporary one
|
|
125
|
-
if self._session is
|
|
126
|
-
session = self._session
|
|
127
|
-
http_method = getattr(session, method)
|
|
128
|
-
_LOGGER.debug(
|
|
129
|
-
"Connecting to %s with data: %s rapi: %s using method %s",
|
|
130
|
-
url,
|
|
131
|
-
data,
|
|
132
|
-
rapi,
|
|
133
|
-
method,
|
|
134
|
-
)
|
|
135
|
-
try:
|
|
136
|
-
async with http_method(
|
|
137
|
-
url,
|
|
138
|
-
data=rapi,
|
|
139
|
-
json=data,
|
|
140
|
-
auth=auth,
|
|
141
|
-
) as resp:
|
|
142
|
-
try:
|
|
143
|
-
message = await resp.text()
|
|
144
|
-
except UnicodeDecodeError:
|
|
145
|
-
_LOGGER.debug("Decoding error")
|
|
146
|
-
message = await resp.read()
|
|
147
|
-
message = message.decode(errors="replace")
|
|
148
|
-
|
|
149
|
-
try:
|
|
150
|
-
message = json.loads(message)
|
|
151
|
-
except ValueError:
|
|
152
|
-
_LOGGER.warning("Non JSON response: %s", message)
|
|
153
|
-
|
|
154
|
-
if resp.status == 400:
|
|
155
|
-
index = ""
|
|
156
|
-
if "msg" in message.keys():
|
|
157
|
-
index = "msg"
|
|
158
|
-
elif "error" in message.keys():
|
|
159
|
-
index = "error"
|
|
160
|
-
_LOGGER.error("Error 400: %s", message[index])
|
|
161
|
-
raise ParseJSONError
|
|
162
|
-
if resp.status == 401:
|
|
163
|
-
_LOGGER.error("Authentication error: %s", message)
|
|
164
|
-
raise AuthenticationError
|
|
165
|
-
if resp.status in [404, 405, 500]:
|
|
166
|
-
_LOGGER.warning("%s", message)
|
|
167
|
-
|
|
168
|
-
if method == "post" and "config_version" in message:
|
|
169
|
-
await self.update()
|
|
170
|
-
return message
|
|
171
|
-
|
|
172
|
-
except (TimeoutError, ServerTimeoutError) as err:
|
|
173
|
-
_LOGGER.error("%s: %s", ERROR_TIMEOUT, url)
|
|
174
|
-
raise err
|
|
175
|
-
except ContentTypeError as err:
|
|
176
|
-
_LOGGER.error("Content error: %s", err.message)
|
|
177
|
-
raise err
|
|
178
|
-
else:
|
|
126
|
+
if (session := self._session) is None:
|
|
179
127
|
async with aiohttp.ClientSession() as session:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"Connecting to %s with data: %s rapi: %s using method %s",
|
|
183
|
-
url,
|
|
184
|
-
data,
|
|
185
|
-
rapi,
|
|
186
|
-
method,
|
|
128
|
+
return await self._process_request_with_session(
|
|
129
|
+
session, url, method, data, rapi, auth
|
|
187
130
|
)
|
|
131
|
+
else:
|
|
132
|
+
return await self._process_request_with_session(
|
|
133
|
+
session, url, method, data, rapi, auth
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async def _process_request_with_session(
|
|
137
|
+
self,
|
|
138
|
+
session: aiohttp.ClientSession,
|
|
139
|
+
url: str,
|
|
140
|
+
method: str,
|
|
141
|
+
data: Any,
|
|
142
|
+
rapi: Any,
|
|
143
|
+
auth: Any,
|
|
144
|
+
) -> dict[str, str] | dict[str, Any]:
|
|
145
|
+
"""Process a request with a given session."""
|
|
146
|
+
http_method = getattr(session, method)
|
|
147
|
+
_LOGGER.debug(
|
|
148
|
+
"Connecting to %s with data: %s rapi: %s using method %s",
|
|
149
|
+
url,
|
|
150
|
+
data,
|
|
151
|
+
rapi,
|
|
152
|
+
method,
|
|
153
|
+
)
|
|
154
|
+
try:
|
|
155
|
+
kwargs = {"data": rapi, "auth": auth}
|
|
156
|
+
if data is not None:
|
|
157
|
+
kwargs["json"] = data
|
|
158
|
+
async with http_method(url, **kwargs) as resp:
|
|
188
159
|
try:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
except ContentTypeError as err:
|
|
229
|
-
_LOGGER.error("Content error: %s", err.message)
|
|
230
|
-
raise err
|
|
160
|
+
message = await resp.text()
|
|
161
|
+
except UnicodeDecodeError:
|
|
162
|
+
_LOGGER.debug("Decoding error")
|
|
163
|
+
message = await resp.read()
|
|
164
|
+
message = message.decode(errors="replace")
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
message = json.loads(message)
|
|
168
|
+
except ValueError:
|
|
169
|
+
_LOGGER.warning("Non JSON response: %s", message)
|
|
170
|
+
|
|
171
|
+
if resp.status == 400:
|
|
172
|
+
if isinstance(message, dict) and "msg" in message:
|
|
173
|
+
_LOGGER.error("Error 400: %s", message["msg"])
|
|
174
|
+
elif isinstance(message, dict) and "error" in message:
|
|
175
|
+
_LOGGER.error("Error 400: %s", message["error"])
|
|
176
|
+
else:
|
|
177
|
+
_LOGGER.error("Error 400: %s", message)
|
|
178
|
+
raise ParseJSONError
|
|
179
|
+
if resp.status == 401:
|
|
180
|
+
_LOGGER.error("Authentication error: %s", message)
|
|
181
|
+
raise AuthenticationError
|
|
182
|
+
if resp.status in [404, 405, 500]:
|
|
183
|
+
_LOGGER.warning("%s", message)
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
method == "post"
|
|
187
|
+
and isinstance(message, dict)
|
|
188
|
+
and "config_version" in message
|
|
189
|
+
):
|
|
190
|
+
await self.update()
|
|
191
|
+
return message
|
|
192
|
+
|
|
193
|
+
except (TimeoutError, ServerTimeoutError):
|
|
194
|
+
_LOGGER.error("%s: %s", ERROR_TIMEOUT, url)
|
|
195
|
+
raise
|
|
196
|
+
except ContentTypeError as err:
|
|
197
|
+
_LOGGER.error("Content error: %s", err.message)
|
|
198
|
+
raise
|
|
231
199
|
|
|
232
200
|
async def send_command(self, command: str) -> tuple:
|
|
233
201
|
"""Send a RAPI command to the charger and parses the response."""
|
|
@@ -387,7 +355,7 @@ class OpenEVSE:
|
|
|
387
355
|
await asyncio.sleep(interval)
|
|
388
356
|
await func(*args, **kwargs)
|
|
389
357
|
|
|
390
|
-
async def get_schedule(self) ->
|
|
358
|
+
async def get_schedule(self) -> dict[str, str] | dict[str, Any]:
|
|
391
359
|
"""Return the current schedule."""
|
|
392
360
|
url = f"{self.url}schedule"
|
|
393
361
|
|
|
@@ -406,9 +374,7 @@ class OpenEVSE:
|
|
|
406
374
|
data = {"charge_mode": mode}
|
|
407
375
|
|
|
408
376
|
_LOGGER.debug("Setting charge mode to %s", mode)
|
|
409
|
-
response = await self.process_request(
|
|
410
|
-
url=url, method="post", data=data
|
|
411
|
-
) # noqa: E501
|
|
377
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
412
378
|
result = response["msg"]
|
|
413
379
|
if result not in ["done", "no change"]:
|
|
414
380
|
_LOGGER.error("Problem issuing command: %s", response["msg"])
|
|
@@ -433,13 +399,11 @@ class OpenEVSE:
|
|
|
433
399
|
data = {"divert_enabled": mode}
|
|
434
400
|
|
|
435
401
|
_LOGGER.debug("Toggling divert: %s", mode)
|
|
436
|
-
response = await self.process_request(
|
|
437
|
-
url=url, method="post", data=data
|
|
438
|
-
) # noqa: E501
|
|
402
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
439
403
|
_LOGGER.debug("divert_mode response: %s", response)
|
|
440
404
|
return response
|
|
441
405
|
|
|
442
|
-
async def get_override(self) ->
|
|
406
|
+
async def get_override(self) -> dict[str, str] | dict[str, Any]:
|
|
443
407
|
"""Get the manual override status."""
|
|
444
408
|
if not self._version_check("4.0.1"):
|
|
445
409
|
_LOGGER.debug("Feature not supported for older firmware.")
|
|
@@ -486,9 +450,7 @@ class OpenEVSE:
|
|
|
486
450
|
|
|
487
451
|
_LOGGER.debug("Override data: %s", data)
|
|
488
452
|
_LOGGER.debug("Setting override config on %s", url)
|
|
489
|
-
response = await self.process_request(
|
|
490
|
-
url=url, method="post", data=data
|
|
491
|
-
) # noqa: E501
|
|
453
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
492
454
|
return response
|
|
493
455
|
|
|
494
456
|
async def toggle_override(self) -> None:
|
|
@@ -559,9 +521,7 @@ class OpenEVSE:
|
|
|
559
521
|
data = {"service": level}
|
|
560
522
|
|
|
561
523
|
_LOGGER.debug("Set service level to: %s", level)
|
|
562
|
-
response = await self.process_request(
|
|
563
|
-
url=url, method="post", data=data
|
|
564
|
-
) # noqa: E501
|
|
524
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
565
525
|
_LOGGER.debug("service response: %s", response)
|
|
566
526
|
result = response["msg"]
|
|
567
527
|
if result not in ["done", "no change"]:
|
|
@@ -633,42 +593,11 @@ class OpenEVSE:
|
|
|
633
593
|
return None
|
|
634
594
|
|
|
635
595
|
try:
|
|
636
|
-
if self._session:
|
|
637
|
-
session = self._session
|
|
638
|
-
http_method = getattr(session, method)
|
|
639
|
-
_LOGGER.debug(
|
|
640
|
-
"Connecting to %s using method %s",
|
|
641
|
-
url,
|
|
642
|
-
method,
|
|
643
|
-
)
|
|
644
|
-
async with http_method(url) as resp:
|
|
645
|
-
if resp.status != 200:
|
|
646
|
-
return None
|
|
647
|
-
message = await resp.text()
|
|
648
|
-
message = json.loads(message)
|
|
649
|
-
response = {}
|
|
650
|
-
response["latest_version"] = message["tag_name"]
|
|
651
|
-
response["release_notes"] = message["body"]
|
|
652
|
-
response["release_url"] = message["html_url"]
|
|
653
|
-
return response
|
|
654
|
-
else:
|
|
596
|
+
if (session := self._session) is None:
|
|
655
597
|
async with aiohttp.ClientSession() as session:
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
|
598
|
+
return await self._firmware_check_with_session(session, url, method)
|
|
599
|
+
else:
|
|
600
|
+
return await self._firmware_check_with_session(session, url, method)
|
|
672
601
|
|
|
673
602
|
except (TimeoutError, ServerTimeoutError):
|
|
674
603
|
_LOGGER.error("%s: %s", ERROR_TIMEOUT, url)
|
|
@@ -679,6 +608,33 @@ class OpenEVSE:
|
|
|
679
608
|
|
|
680
609
|
return None
|
|
681
610
|
|
|
611
|
+
async def _firmware_check_with_session(
|
|
612
|
+
self, session: aiohttp.ClientSession, url: str, method: str
|
|
613
|
+
) -> dict | None:
|
|
614
|
+
"""Process a firmware check request with a given session."""
|
|
615
|
+
http_method = getattr(session, method)
|
|
616
|
+
_LOGGER.debug(
|
|
617
|
+
"Connecting to %s using method %s",
|
|
618
|
+
url,
|
|
619
|
+
method,
|
|
620
|
+
)
|
|
621
|
+
async with http_method(url) as resp:
|
|
622
|
+
if resp.status != 200:
|
|
623
|
+
return None
|
|
624
|
+
message = await resp.text()
|
|
625
|
+
try:
|
|
626
|
+
message = json.loads(message)
|
|
627
|
+
except json.JSONDecodeError:
|
|
628
|
+
_LOGGER.error("Failed to parse JSON response: %s", message)
|
|
629
|
+
return None
|
|
630
|
+
|
|
631
|
+
response = {}
|
|
632
|
+
if isinstance(message, dict):
|
|
633
|
+
response["latest_version"] = message.get("tag_name")
|
|
634
|
+
response["release_notes"] = message.get("body")
|
|
635
|
+
response["release_url"] = message.get("html_url")
|
|
636
|
+
return response
|
|
637
|
+
|
|
682
638
|
def _version_check(self, min_version: str, max_version: str = "") -> bool:
|
|
683
639
|
"""Return bool if minimum version is met."""
|
|
684
640
|
if "version" not in self._config:
|
|
@@ -831,7 +787,7 @@ class OpenEVSE:
|
|
|
831
787
|
raise UnsupportedFeature
|
|
832
788
|
|
|
833
789
|
url = f"{self.url}limit"
|
|
834
|
-
data:
|
|
790
|
+
data: dict[str, Any] = await self.get_limit()
|
|
835
791
|
valid_types = ["time", "energy", "soc", "range"]
|
|
836
792
|
|
|
837
793
|
if limit_type not in valid_types:
|
|
@@ -844,9 +800,7 @@ class OpenEVSE:
|
|
|
844
800
|
|
|
845
801
|
_LOGGER.debug("Limit data: %s", data)
|
|
846
802
|
_LOGGER.debug("Setting limit config on %s", url)
|
|
847
|
-
response = await self.process_request(
|
|
848
|
-
url=url, method="post", data=data
|
|
849
|
-
) # noqa: E501
|
|
803
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
850
804
|
return response
|
|
851
805
|
|
|
852
806
|
async def clear_limit(self) -> Any:
|
|
@@ -856,12 +810,9 @@ class OpenEVSE:
|
|
|
856
810
|
raise UnsupportedFeature
|
|
857
811
|
|
|
858
812
|
url = f"{self.url}limit"
|
|
859
|
-
data: Dict[str, Any] = {}
|
|
860
813
|
|
|
861
814
|
_LOGGER.debug("Clearing limit config on %s", url)
|
|
862
|
-
response = await self.process_request(
|
|
863
|
-
url=url, method="delete", data=data
|
|
864
|
-
) # noqa: E501
|
|
815
|
+
response = await self.process_request(url=url, method="delete")
|
|
865
816
|
return response
|
|
866
817
|
|
|
867
818
|
async def get_limit(self) -> Any:
|
|
@@ -871,12 +822,9 @@ class OpenEVSE:
|
|
|
871
822
|
raise UnsupportedFeature
|
|
872
823
|
|
|
873
824
|
url = f"{self.url}limit"
|
|
874
|
-
data: Dict[str, Any] = {}
|
|
875
825
|
|
|
876
826
|
_LOGGER.debug("Getting limit config on %s", url)
|
|
877
|
-
response = await self.process_request(
|
|
878
|
-
url=url, method="get", data=data
|
|
879
|
-
) # noqa: E501
|
|
827
|
+
response = await self.process_request(url=url, method="get")
|
|
880
828
|
return response
|
|
881
829
|
|
|
882
830
|
async def make_claim(
|
|
@@ -911,9 +859,7 @@ class OpenEVSE:
|
|
|
911
859
|
|
|
912
860
|
_LOGGER.debug("Claim data: %s", data)
|
|
913
861
|
_LOGGER.debug("Setting up claim on %s", url)
|
|
914
|
-
response = await self.process_request(
|
|
915
|
-
url=url, method="post", data=data
|
|
916
|
-
) # noqa: E501
|
|
862
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
917
863
|
return response
|
|
918
864
|
|
|
919
865
|
async def release_claim(self, client: int = CLIENT) -> Any:
|
|
@@ -925,7 +871,7 @@ class OpenEVSE:
|
|
|
925
871
|
url = f"{self.url}claims/{client}"
|
|
926
872
|
|
|
927
873
|
_LOGGER.debug("Releasing claim on %s", url)
|
|
928
|
-
response = await self.process_request(url=url, method="delete")
|
|
874
|
+
response = await self.process_request(url=url, method="delete")
|
|
929
875
|
return response
|
|
930
876
|
|
|
931
877
|
async def list_claims(self, target: bool | None = None) -> Any:
|
|
@@ -941,7 +887,7 @@ class OpenEVSE:
|
|
|
941
887
|
url = f"{self.url}claims{target_check}"
|
|
942
888
|
|
|
943
889
|
_LOGGER.debug("Getting claims on %s", url)
|
|
944
|
-
response = await self.process_request(url=url, method="get")
|
|
890
|
+
response = await self.process_request(url=url, method="get")
|
|
945
891
|
return response
|
|
946
892
|
|
|
947
893
|
async def set_led_brightness(self, level: int) -> None:
|
|
@@ -955,7 +901,7 @@ class OpenEVSE:
|
|
|
955
901
|
|
|
956
902
|
data["led_brightness"] = level
|
|
957
903
|
_LOGGER.debug("Setting LED brightness to %s", level)
|
|
958
|
-
await self.process_request(url=url, method="post", data=data)
|
|
904
|
+
await self.process_request(url=url, method="post", data=data)
|
|
959
905
|
|
|
960
906
|
async def set_divert_mode(self, mode: str = "fast") -> None:
|
|
961
907
|
"""Set the divert mode."""
|
|
@@ -1312,6 +1258,20 @@ class OpenEVSE:
|
|
|
1312
1258
|
return round(self._status["voltage"] * self._status["amp"], 2)
|
|
1313
1259
|
return None
|
|
1314
1260
|
|
|
1261
|
+
# Shaper HTTP Posting
|
|
1262
|
+
async def set_shaper_live_pwr(self, power: int) -> None:
|
|
1263
|
+
"""Send pushed sensor data to shaper."""
|
|
1264
|
+
if not self._version_check("4.0.0"):
|
|
1265
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
1266
|
+
raise UnsupportedFeature
|
|
1267
|
+
|
|
1268
|
+
url = f"{self.url}status"
|
|
1269
|
+
data = {"shaper_live_pwr": power}
|
|
1270
|
+
|
|
1271
|
+
_LOGGER.debug("Posting shaper data: %s", data)
|
|
1272
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
1273
|
+
_LOGGER.debug("Shaper response: %s", response)
|
|
1274
|
+
|
|
1315
1275
|
# Shaper values
|
|
1316
1276
|
@property
|
|
1317
1277
|
def shaper_active(self) -> bool | None:
|
|
@@ -1404,7 +1364,7 @@ class OpenEVSE:
|
|
|
1404
1364
|
# Safety counts
|
|
1405
1365
|
@property
|
|
1406
1366
|
def checks_count(self) -> dict:
|
|
1407
|
-
"""Return the
|
|
1367
|
+
"""Return the safety checks counts."""
|
|
1408
1368
|
attributes = ("gfcicount", "nogndcount", "stuckcount")
|
|
1409
1369
|
counts = {}
|
|
1410
1370
|
if self._status is not None and set(attributes).issubset(self._status.keys()):
|
|
@@ -170,7 +170,7 @@ class OpenEVSEWebsocket:
|
|
|
170
170
|
if self._ping and self._pong:
|
|
171
171
|
time_delta = self._pong - self._ping
|
|
172
172
|
if time_delta < datetime.timedelta(0):
|
|
173
|
-
#
|
|
173
|
+
# Negative time should indicate no pong reply so consider the
|
|
174
174
|
# websocket disconnected.
|
|
175
175
|
self._error_reason = ERROR_PING_TIMEOUT
|
|
176
176
|
await self._set_state(STATE_DISCONNECTED)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[tool.ruff]
|
|
2
|
+
target-version = "py310"
|
|
3
|
+
line-length = 88
|
|
4
|
+
|
|
5
|
+
[tool.ruff.lint]
|
|
6
|
+
select = [
|
|
7
|
+
"E", # pycodestyle errors
|
|
8
|
+
"W", # pycodestyle warnings
|
|
9
|
+
"F", # pyflakes
|
|
10
|
+
"I", # isort
|
|
11
|
+
"C", # flake8-comprehensions
|
|
12
|
+
"B", # flake8-bugbear
|
|
13
|
+
"UP", # pyupgrade
|
|
14
|
+
"D", # pydocstyle
|
|
15
|
+
]
|
|
16
|
+
ignore = [
|
|
17
|
+
"D100", # missing docstring in public module
|
|
18
|
+
"D101", # missing docstring in public class
|
|
19
|
+
"D102", # missing docstring in public method
|
|
20
|
+
"D103", # missing docstring in public function
|
|
21
|
+
"D104", # missing docstring in public package
|
|
22
|
+
"D105", # missing docstring in magic method
|
|
23
|
+
"D106", # missing docstring in public nested class
|
|
24
|
+
"D107", # missing docstring in __init__
|
|
25
|
+
"D202", # No blank lines allowed after function docstring
|
|
26
|
+
"D203", # 1 blank line required before class docstring
|
|
27
|
+
"D213", # Multi-line docstring summary should start at the second line
|
|
28
|
+
"E501", # line too long
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint.mccabe]
|
|
32
|
+
max-complexity = 18
|
{python_openevse_http-0.2.4 → python_openevse_http-0.2.6/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.2.
|
|
3
|
+
Version: 0.2.6
|
|
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
|
|
@@ -11,12 +11,10 @@ Classifier: Development Status :: 4 - Beta
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Natural Language :: English
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
16
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.13
|
|
20
18
|
Description-Content-Type: text/markdown
|
|
21
19
|
License-File: LICENSE
|
|
22
20
|
Requires-Dist: aiohttp
|
|
@@ -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.
|
|
9
|
+
VERSION = "0.2.6"
|
|
10
10
|
|
|
11
11
|
setup(
|
|
12
12
|
name="python_openevse_http",
|
|
@@ -19,7 +19,7 @@ setup(
|
|
|
19
19
|
long_description=README_FILE.read_text(encoding="utf-8"),
|
|
20
20
|
long_description_content_type="text/markdown",
|
|
21
21
|
packages=find_packages(exclude=["test.*", "tests"]),
|
|
22
|
-
python_requires=">=3.
|
|
22
|
+
python_requires=">=3.13",
|
|
23
23
|
install_requires=["aiohttp"],
|
|
24
24
|
license="Apache-2.0",
|
|
25
25
|
entry_points={},
|
|
@@ -30,10 +30,8 @@ setup(
|
|
|
30
30
|
"Intended Audience :: Developers",
|
|
31
31
|
"Natural Language :: English",
|
|
32
32
|
"Programming Language :: Python :: 3",
|
|
33
|
-
"Programming Language :: Python :: 3.10",
|
|
34
|
-
"Programming Language :: Python :: 3.11",
|
|
35
|
-
"Programming Language :: Python :: 3.12",
|
|
36
33
|
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Programming Language :: Python :: 3.14",
|
|
37
35
|
"License :: OSI Approved :: Apache Software License",
|
|
38
36
|
],
|
|
39
37
|
)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"""Test external session management."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from unittest.mock import AsyncMock, MagicMock
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
5
5
|
|
|
6
6
|
import aiohttp
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
import openevsehttp.__main__ as main
|
|
10
9
|
from openevsehttp.__main__ import OpenEVSE
|
|
11
10
|
from tests.common import load_fixture
|
|
12
11
|
|
|
@@ -11,7 +11,6 @@ import aiohttp
|
|
|
11
11
|
import pytest
|
|
12
12
|
from aiohttp.client_exceptions import ContentTypeError, ServerTimeoutError
|
|
13
13
|
from aiohttp.client_reqrep import ConnectionKey
|
|
14
|
-
from awesomeversion import AwesomeVersion
|
|
15
14
|
from awesomeversion.exceptions import AwesomeVersionCompareException
|
|
16
15
|
from freezegun import freeze_time
|
|
17
16
|
|
|
@@ -151,6 +150,16 @@ async def test_send_command_parse_err(test_charger_auth, mock_aioclient):
|
|
|
151
150
|
status = await test_charger_auth.send_command("test")
|
|
152
151
|
assert status is None
|
|
153
152
|
|
|
153
|
+
mock_aioclient.post(TEST_URL_RAPI, status=400, body='{"other": "Something else"}')
|
|
154
|
+
with pytest.raises(main.ParseJSONError):
|
|
155
|
+
status = await test_charger_auth.send_command("test")
|
|
156
|
+
assert status is None
|
|
157
|
+
|
|
158
|
+
mock_aioclient.post(TEST_URL_RAPI, status=400, body='"Just a string response"')
|
|
159
|
+
with pytest.raises(main.ParseJSONError):
|
|
160
|
+
status = await test_charger_auth.send_command("test")
|
|
161
|
+
assert status is None
|
|
162
|
+
|
|
154
163
|
|
|
155
164
|
async def test_send_command_auth_err(test_charger_auth, mock_aioclient):
|
|
156
165
|
"""Test v4 Status reply."""
|
|
@@ -855,7 +864,7 @@ async def test_toggle_override_v2_err(test_charger_v2, mock_aioclient, caplog):
|
|
|
855
864
|
"""Test v4 Status reply."""
|
|
856
865
|
await test_charger_v2.update()
|
|
857
866
|
content_error = mock.Mock()
|
|
858
|
-
|
|
867
|
+
content_error.real_url = f"{TEST_URL_RAPI}"
|
|
859
868
|
mock_aioclient.post(
|
|
860
869
|
TEST_URL_RAPI,
|
|
861
870
|
exception=ContentTypeError(
|
|
@@ -1070,7 +1079,7 @@ async def test_set_divertmode(
|
|
|
1070
1079
|
|
|
1071
1080
|
|
|
1072
1081
|
async def test_test_and_get(test_charger, test_charger_v2, mock_aioclient, caplog):
|
|
1073
|
-
"""Test v4 Status reply"""
|
|
1082
|
+
"""Test v4 Status reply."""
|
|
1074
1083
|
data = await test_charger.test_and_get()
|
|
1075
1084
|
mock_aioclient.get(
|
|
1076
1085
|
TEST_URL_CONFIG,
|
|
@@ -1109,7 +1118,7 @@ async def test_firmware_check(
|
|
|
1109
1118
|
mock_aioclient,
|
|
1110
1119
|
caplog,
|
|
1111
1120
|
):
|
|
1112
|
-
"""Test v4 Status reply"""
|
|
1121
|
+
"""Test v4 Status reply."""
|
|
1113
1122
|
await test_charger.update()
|
|
1114
1123
|
mock_aioclient.get(
|
|
1115
1124
|
TEST_URL_GITHUB_v4,
|
|
@@ -1271,6 +1280,38 @@ async def test_shaper_max_power(fixture, expected, request):
|
|
|
1271
1280
|
await charger.ws_disconnect()
|
|
1272
1281
|
|
|
1273
1282
|
|
|
1283
|
+
async def test_set_shaper_live_power(
|
|
1284
|
+
test_charger, test_charger_v2, mock_aioclient, caplog
|
|
1285
|
+
):
|
|
1286
|
+
"""Test setting shaper live power."""
|
|
1287
|
+
await test_charger.update()
|
|
1288
|
+
mock_aioclient.post(
|
|
1289
|
+
TEST_URL_STATUS,
|
|
1290
|
+
status=200,
|
|
1291
|
+
body='{"shaper_live_pwr": 210}',
|
|
1292
|
+
)
|
|
1293
|
+
with caplog.at_level(logging.DEBUG):
|
|
1294
|
+
await test_charger.set_shaper_live_pwr(210)
|
|
1295
|
+
assert "Posting shaper data: {'shaper_live_pwr': 210}" in caplog.text
|
|
1296
|
+
assert "Shaper response: {'shaper_live_pwr': 210}" in caplog.text
|
|
1297
|
+
|
|
1298
|
+
mock_aioclient.post(
|
|
1299
|
+
TEST_URL_STATUS,
|
|
1300
|
+
status=200,
|
|
1301
|
+
body='{"shaper_live_pwr": 0}',
|
|
1302
|
+
)
|
|
1303
|
+
with caplog.at_level(logging.DEBUG):
|
|
1304
|
+
await test_charger.set_shaper_live_pwr(0)
|
|
1305
|
+
assert "Posting shaper data: {'shaper_live_pwr': 0}" in caplog.text
|
|
1306
|
+
|
|
1307
|
+
await test_charger_v2.update()
|
|
1308
|
+
with pytest.raises(UnsupportedFeature):
|
|
1309
|
+
with caplog.at_level(logging.DEBUG):
|
|
1310
|
+
await test_charger_v2.set_shaper_live_pwr(210)
|
|
1311
|
+
assert "Feature not supported for older firmware." in caplog.text
|
|
1312
|
+
await test_charger_v2.ws_disconnect()
|
|
1313
|
+
|
|
1314
|
+
|
|
1274
1315
|
@pytest.mark.parametrize(
|
|
1275
1316
|
"fixture, expected", [("test_charger", 75), ("test_charger_v2", None)]
|
|
1276
1317
|
)
|
|
@@ -3133,6 +3174,14 @@ async def test_firmware_check_errors(mock_aioclient):
|
|
|
3133
3174
|
)
|
|
3134
3175
|
assert await charger.firmware_check() is None
|
|
3135
3176
|
|
|
3177
|
+
# JSONDecodeError from github
|
|
3178
|
+
mock_aioclient.get(url, status=200, body="not json")
|
|
3179
|
+
assert await charger.firmware_check() is None
|
|
3180
|
+
|
|
3181
|
+
# Non-dict JSON from github
|
|
3182
|
+
mock_aioclient.get(url, status=200, body='"just a string"')
|
|
3183
|
+
assert await charger.firmware_check() == {}
|
|
3184
|
+
|
|
3136
3185
|
|
|
3137
3186
|
async def test_websocket_pong():
|
|
3138
3187
|
"""Test websocket handles pong message."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/python_openevse_http.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{python_openevse_http-0.2.4 → python_openevse_http-0.2.6}/python_openevse_http.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|