python-openevse-http 0.2.2__tar.gz → 0.2.4__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.2/python_openevse_http.egg-info → python_openevse_http-0.2.4}/PKG-INFO +1 -1
  2. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/openevsehttp/__main__.py +179 -139
  3. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/openevsehttp/websocket.py +37 -15
  4. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4/python_openevse_http.egg-info}/PKG-INFO +1 -1
  5. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/python_openevse_http.egg-info/SOURCES.txt +1 -0
  6. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/setup.py +1 -1
  7. python_openevse_http-0.2.4/tests/test_external_session.py +200 -0
  8. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/tests/test_main.py +897 -20
  9. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/tests/test_websocket.py +26 -2
  10. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/LICENSE +0 -0
  11. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/README.md +0 -0
  12. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/openevsehttp/__init__.py +0 -0
  13. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/openevsehttp/const.py +0 -0
  14. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/openevsehttp/exceptions.py +0 -0
  15. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/python_openevse_http.egg-info/dependency_links.txt +0 -0
  16. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/python_openevse_http.egg-info/not-zip-safe +0 -0
  17. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/python_openevse_http.egg-info/requires.txt +0 -0
  18. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/python_openevse_http.egg-info/top_level.txt +0 -0
  19. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/setup.cfg +0 -0
  20. {python_openevse_http-0.2.2 → python_openevse_http-0.2.4}/tests/test_main_edge_cases.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -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__(self, host: str, user: str = "", pwd: str = "") -> None:
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
- async with aiohttp.ClientSession() as session:
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
- await session.close()
170
- return message
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
- async with aiohttp.ClientSession() as session:
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)
@@ -896,37 +975,32 @@ class OpenEVSE:
896
975
  raise UnknownError
897
976
 
898
977
  @property
899
- def led_brightness(self) -> str:
978
+ def led_brightness(self) -> int | None:
900
979
  """Return charger led_brightness."""
901
980
  if not self._version_check("4.1.0"):
902
981
  _LOGGER.debug("Feature not supported for older firmware.")
903
982
  raise UnsupportedFeature
904
- assert self._config is not None
905
- return self._config["led_brightness"]
983
+ return self._config.get("led_brightness")
906
984
 
907
985
  @property
908
- def hostname(self) -> str:
986
+ def hostname(self) -> str | None:
909
987
  """Return charger hostname."""
910
- assert self._config is not None
911
- return self._config["hostname"]
988
+ return self._config.get("hostname")
912
989
 
913
990
  @property
914
- def wifi_ssid(self) -> str:
991
+ def wifi_ssid(self) -> str | None:
915
992
  """Return charger connected SSID."""
916
- assert self._config is not None
917
- return self._config["ssid"]
993
+ return self._config.get("ssid")
918
994
 
919
995
  @property
920
- def ammeter_offset(self) -> int:
996
+ def ammeter_offset(self) -> int | None:
921
997
  """Return ammeter's current offset."""
922
- assert self._config is not None
923
- return self._config["offset"]
998
+ return self._config.get("offset")
924
999
 
925
1000
  @property
926
- def ammeter_scale_factor(self) -> int:
1001
+ def ammeter_scale_factor(self) -> int | None:
927
1002
  """Return ammeter's current scale factor."""
928
- assert self._config is not None
929
- return self._config["scale"]
1003
+ return self._config.get("scale")
930
1004
 
931
1005
  @property
932
1006
  def temp_check_enabled(self) -> bool:
@@ -954,23 +1028,21 @@ class OpenEVSE:
954
1028
  return bool(self._config.get("relayt", False))
955
1029
 
956
1030
  @property
957
- def service_level(self) -> str:
1031
+ def service_level(self) -> str | None:
958
1032
  """Return the service level."""
959
- assert self._config is not None
960
- return self._config["service"]
1033
+ return self._config.get("service")
961
1034
 
962
1035
  @property
963
- def openevse_firmware(self) -> str:
1036
+ def openevse_firmware(self) -> str | None:
964
1037
  """Return the firmware version."""
965
- assert self._config is not None
966
- return self._config["firmware"]
1038
+ return self._config.get("firmware")
967
1039
 
968
1040
  @property
969
1041
  def max_current_soft(self) -> int | None:
970
1042
  """Return the max current soft."""
971
- if self._config is not None and "max_current_soft" in self._config:
972
- return self._config["max_current_soft"]
973
- return self._status["pilot"]
1043
+ if "max_current_soft" in self._config:
1044
+ return self._config.get("max_current_soft")
1045
+ return self._status.get("pilot")
974
1046
 
975
1047
  @property
976
1048
  async def async_charge_current(self) -> int | None:
@@ -984,9 +1056,9 @@ class OpenEVSE:
984
1056
  return min(
985
1057
  claims["properties"]["charge_current"], self._config["max_current_hard"]
986
1058
  )
987
- if self._config is not None and "max_current_soft" in self._config:
988
- return self._config["max_current_soft"]
989
- return self._status["pilot"]
1059
+ if "max_current_soft" in self._config:
1060
+ return self._config.get("max_current_soft")
1061
+ return self._status.get("pilot")
990
1062
 
991
1063
  @property
992
1064
  def max_current(self) -> int | None:
@@ -994,33 +1066,29 @@ class OpenEVSE:
994
1066
  return self._status.get("max_current", None)
995
1067
 
996
1068
  @property
997
- def wifi_firmware(self) -> str:
1069
+ def wifi_firmware(self) -> str | None:
998
1070
  """Return the ESP firmware version."""
999
- assert self._config is not None
1000
- value = self._config["version"]
1001
- if "dev" in value:
1071
+ value = self._config.get("version")
1072
+ if value is not None and "dev" in value:
1002
1073
  _LOGGER.debug("Stripping 'dev' from version.")
1003
1074
  value = value.split(".")
1004
1075
  value = ".".join(value[0:3])
1005
1076
  return value
1006
1077
 
1007
1078
  @property
1008
- def ip_address(self) -> str:
1079
+ def ip_address(self) -> str | None:
1009
1080
  """Return the ip address."""
1010
- assert self._status is not None
1011
- return self._status["ipaddress"]
1081
+ return self._status.get("ipaddress")
1012
1082
 
1013
1083
  @property
1014
- def charging_voltage(self) -> int:
1084
+ def charging_voltage(self) -> int | None:
1015
1085
  """Return the charging voltage."""
1016
- assert self._status is not None
1017
- return self._status["voltage"]
1086
+ return self._status.get("voltage")
1018
1087
 
1019
1088
  @property
1020
- def mode(self) -> str:
1089
+ def mode(self) -> str | None:
1021
1090
  """Return the mode."""
1022
- assert self._status is not None
1023
- return self._status["mode"]
1091
+ return self._status.get("mode")
1024
1092
 
1025
1093
  @property
1026
1094
  def using_ethernet(self) -> bool:
@@ -1028,98 +1096,80 @@ class OpenEVSE:
1028
1096
  return bool(self._status.get("eth_connected", False))
1029
1097
 
1030
1098
  @property
1031
- def stuck_relay_trip_count(self) -> int:
1099
+ def stuck_relay_trip_count(self) -> int | None:
1032
1100
  """Return the stuck relay count."""
1033
- assert self._status is not None
1034
- return self._status["stuckcount"]
1101
+ return self._status.get("stuckcount")
1035
1102
 
1036
1103
  @property
1037
- def no_gnd_trip_count(self) -> int:
1104
+ def no_gnd_trip_count(self) -> int | None:
1038
1105
  """Return the no ground count."""
1039
- assert self._status is not None
1040
- return self._status["nogndcount"]
1106
+ return self._status.get("nogndcount")
1041
1107
 
1042
1108
  @property
1043
- def gfi_trip_count(self) -> int:
1109
+ def gfi_trip_count(self) -> int | None:
1044
1110
  """Return the GFCI count."""
1045
- assert self._status is not None
1046
- return self._status["gfcicount"]
1111
+ return self._status.get("gfcicount")
1047
1112
 
1048
1113
  @property
1049
1114
  def status(self) -> str:
1050
1115
  """Return charger's state."""
1051
- state = self._status.get("status", states[int(self._status.get("state", 0))])
1052
- return state
1116
+ return self._status.get("status", states[int(self._status.get("state", 0))])
1053
1117
 
1054
1118
  @property
1055
1119
  def state(self) -> str:
1056
1120
  """Return charger's state."""
1057
- assert self._status is not None
1058
- return states[int(self._status["state"])]
1121
+ return states[int(self._status.get("state", 0))]
1059
1122
 
1060
1123
  @property
1061
- def state_raw(self) -> int:
1124
+ def state_raw(self) -> int | None:
1062
1125
  """Return charger's state int form."""
1063
- assert self._status is not None
1064
- return self._status["state"]
1126
+ return self._status.get("state")
1065
1127
 
1066
1128
  @property
1067
- def charge_time_elapsed(self) -> int:
1129
+ def charge_time_elapsed(self) -> int | None:
1068
1130
  """Return elapsed charging time."""
1069
- assert self._status is not None
1070
- return self._status["elapsed"]
1131
+ return self._status.get("elapsed")
1071
1132
 
1072
1133
  @property
1073
- def wifi_signal(self) -> str:
1134
+ def wifi_signal(self) -> str | None:
1074
1135
  """Return charger's wifi signal."""
1075
- assert self._status is not None
1076
- return self._status["srssi"]
1136
+ return self._status.get("srssi")
1077
1137
 
1078
1138
  @property
1079
- def charging_current(self) -> float:
1139
+ def charging_current(self) -> float | None:
1080
1140
  """Return the charge current.
1081
1141
 
1082
1142
  0 if is not currently charging.
1083
1143
  """
1084
- assert self._status is not None
1085
- return self._status["amp"]
1144
+ return self._status.get("amp")
1086
1145
 
1087
1146
  @property
1088
- def current_capacity(self) -> int:
1147
+ def current_capacity(self) -> int | None:
1089
1148
  """Return the current capacity."""
1090
- assert self._status is not None
1091
- return self._status["pilot"]
1149
+ return self._status.get("pilot")
1092
1150
 
1093
1151
  @property
1094
- def usage_total(self) -> float:
1152
+ def usage_total(self) -> float | None:
1095
1153
  """Return the total energy usage in Wh."""
1096
- assert self._status is not None
1097
1154
  if "total_energy" in self._status:
1098
- return self._status["total_energy"]
1099
- return self._status["watthour"]
1155
+ return self._status.get("total_energy")
1156
+ return self._status.get("watthour")
1100
1157
 
1101
1158
  @property
1102
1159
  def ambient_temperature(self) -> float | None:
1103
1160
  """Return the temperature of the ambient sensor, in degrees Celsius."""
1104
- assert self._status is not None
1105
- temp = None
1106
- if "temp" in self._status and self._status["temp"]:
1107
- temp = self._status["temp"] / 10
1108
- else:
1109
- temp = self._status["temp1"] / 10
1110
- return temp
1161
+ temp = self._status.get("temp")
1162
+ if temp:
1163
+ return temp / 10
1164
+ return self._status.get("temp1", 0) / 10
1111
1165
 
1112
1166
  @property
1113
1167
  def rtc_temperature(self) -> float | None:
1114
- """Return the temperature of the real time clock sensor.
1115
-
1116
- In degrees Celsius.
1117
- """
1118
- assert self._status is not None
1119
- temp = self._status["temp2"] if self._status["temp2"] else None
1120
- if temp is not None:
1121
- return temp / 10
1122
- return None
1168
+ """Return the temperature of the real time clock sensor."""
1169
+ temp = self._status.get("temp2")
1170
+ if temp is None or isinstance(temp, bool):
1171
+ return None
1172
+ return float(temp) / 10
1123
1173
 
1124
1174
  @property
1125
1175
  def ir_temperature(self) -> float | None:
@@ -1127,27 +1177,23 @@ class OpenEVSE:
1127
1177
 
1128
1178
  In degrees Celsius.
1129
1179
  """
1130
- assert self._status is not None
1131
- temp = self._status["temp3"] if self._status["temp3"] else None
1132
- if temp is not None:
1133
- return temp / 10
1134
- return None
1180
+ temp = self._status.get("temp3")
1181
+ if temp is None or isinstance(temp, bool):
1182
+ return None
1183
+ return float(temp) / 10
1135
1184
 
1136
1185
  @property
1137
1186
  def esp_temperature(self) -> float | None:
1138
1187
  """Return the temperature of the ESP sensor, in degrees Celsius."""
1139
- assert self._status is not None
1140
- if "temp4" in self._status:
1141
- temp = self._status["temp4"] if self._status["temp4"] else None
1142
- if temp is not None:
1143
- return temp / 10
1144
- return None
1188
+ temp = self._status.get("temp4")
1189
+ if temp is None or isinstance(temp, bool):
1190
+ return None
1191
+ return float(temp) / 10
1145
1192
 
1146
1193
  @property
1147
1194
  def time(self) -> datetime | None:
1148
1195
  """Get the RTC time."""
1149
1196
  value = self._status.get("time")
1150
-
1151
1197
  if value:
1152
1198
  try:
1153
1199
  return datetime.fromisoformat(value.replace("Z", "+00:00"))
@@ -1156,15 +1202,17 @@ class OpenEVSE:
1156
1202
  return None
1157
1203
 
1158
1204
  @property
1159
- def usage_session(self) -> float:
1205
+ def usage_session(self) -> float | None:
1160
1206
  """Get the energy usage for the current charging session.
1161
1207
 
1162
1208
  Return the energy usage in Wh.
1163
1209
  """
1164
- assert self._status is not None
1165
1210
  if "session_energy" in self._status:
1166
- return self._status["session_energy"]
1167
- return float(round(self._status["wattsec"] / 3600, 2))
1211
+ return self._status.get("session_energy")
1212
+ wattsec = self._status.get("wattsec")
1213
+ if wattsec is not None:
1214
+ return float(round(wattsec / 3600, 2))
1215
+ return None
1168
1216
 
1169
1217
  @property
1170
1218
  def total_day(self) -> float | None:
@@ -1194,61 +1242,53 @@ class OpenEVSE:
1194
1242
  @property
1195
1243
  def protocol_version(self) -> str | None:
1196
1244
  """Return the protocol version."""
1197
- assert self._config is not None
1198
- if self._config["protocol"] == "-":
1245
+ protocol = self._config.get("protocol")
1246
+ if protocol == "-":
1199
1247
  return None
1200
- return self._config["protocol"]
1248
+ return protocol
1201
1249
 
1202
1250
  @property
1203
- def vehicle(self) -> str:
1251
+ def vehicle(self) -> bool:
1204
1252
  """Return if a vehicle is connected dto the EVSE."""
1205
- assert self._status is not None
1206
- return self._status["vehicle"]
1253
+ return self._status.get("vehicle", False)
1207
1254
 
1208
1255
  @property
1209
- def ota_update(self) -> str:
1256
+ def ota_update(self) -> bool:
1210
1257
  """Return if an OTA update is active."""
1211
- assert self._status is not None
1212
- return self._status["ota_update"]
1258
+ return self._status.get("ota_update", False)
1213
1259
 
1214
1260
  @property
1215
- def manual_override(self) -> str:
1261
+ def manual_override(self) -> bool:
1216
1262
  """Return if Manual Override is set."""
1217
- assert self._status is not None
1218
- return self._status["manual_override"]
1263
+ return self._status.get("manual_override", False)
1219
1264
 
1220
1265
  @property
1221
1266
  def divertmode(self) -> str:
1222
1267
  """Return the divert mode."""
1223
- assert self._status is not None
1224
- mode = self._status["divertmode"]
1268
+ mode = self._status.get("divertmode", 1)
1225
1269
  if mode == 1:
1226
1270
  return "fast"
1227
1271
  return "eco"
1228
1272
 
1229
1273
  @property
1230
- def charge_mode(self) -> str:
1274
+ def charge_mode(self) -> str | None:
1231
1275
  """Return the charge mode."""
1232
- assert self._config is not None
1233
- return self._config["charge_mode"]
1276
+ return self._config.get("charge_mode")
1234
1277
 
1235
1278
  @property
1236
- def available_current(self) -> float:
1279
+ def available_current(self) -> float | None:
1237
1280
  """Return the computed available current for divert."""
1238
- assert self._status is not None
1239
- return self._status["available_current"]
1281
+ return self._status.get("available_current")
1240
1282
 
1241
1283
  @property
1242
- def smoothed_available_current(self) -> float:
1284
+ def smoothed_available_current(self) -> float | None:
1243
1285
  """Return the computed smoothed available current for divert."""
1244
- assert self._status is not None
1245
- return self._status["smoothed_available_current"]
1286
+ return self._status.get("smoothed_available_current")
1246
1287
 
1247
1288
  @property
1248
- def charge_rate(self) -> float:
1289
+ def charge_rate(self) -> float | None:
1249
1290
  """Return the divert charge rate."""
1250
- assert self._status is not None
1251
- return self._status["charge_rate"]
1291
+ return self._status.get("charge_rate")
1252
1292
 
1253
1293
  @property
1254
1294
  def divert_active(self) -> bool:
@@ -1266,7 +1306,7 @@ class OpenEVSE:
1266
1306
 
1267
1307
  Calculate Watts base on V*I
1268
1308
  """
1269
- if self._status is not None and any(
1309
+ if self._status is not None and all(
1270
1310
  key in self._status for key in ["voltage", "amp"]
1271
1311
  ):
1272
1312
  return round(self._status["voltage"] * self._status["amp"], 2)