python-openevse-http 0.2.1__tar.gz → 0.2.3__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 (19) hide show
  1. {python_openevse_http-0.2.1/python_openevse_http.egg-info → python_openevse_http-0.2.3}/PKG-INFO +1 -1
  2. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/openevsehttp/__main__.py +107 -136
  3. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3/python_openevse_http.egg-info}/PKG-INFO +1 -1
  4. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/setup.py +1 -1
  5. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/tests/test_main.py +61 -27
  6. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/tests/test_main_edge_cases.py +6 -4
  7. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/tests/test_websocket.py +4 -3
  8. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/LICENSE +0 -0
  9. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/README.md +0 -0
  10. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/openevsehttp/__init__.py +0 -0
  11. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/openevsehttp/const.py +0 -0
  12. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/openevsehttp/exceptions.py +0 -0
  13. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/openevsehttp/websocket.py +0 -0
  14. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/python_openevse_http.egg-info/SOURCES.txt +0 -0
  15. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/python_openevse_http.egg-info/dependency_links.txt +0 -0
  16. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/python_openevse_http.egg-info/not-zip-safe +0 -0
  17. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/python_openevse_http.egg-info/requires.txt +0 -0
  18. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/python_openevse_http.egg-info/top_level.txt +0 -0
  19. {python_openevse_http-0.2.1 → python_openevse_http-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 0.2.1
3
+ Version: 0.2.3
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,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
@@ -896,37 +896,32 @@ class OpenEVSE:
896
896
  raise UnknownError
897
897
 
898
898
  @property
899
- def led_brightness(self) -> str:
899
+ def led_brightness(self) -> int | None:
900
900
  """Return charger led_brightness."""
901
901
  if not self._version_check("4.1.0"):
902
902
  _LOGGER.debug("Feature not supported for older firmware.")
903
903
  raise UnsupportedFeature
904
- assert self._config is not None
905
- return self._config["led_brightness"]
904
+ return self._config.get("led_brightness")
906
905
 
907
906
  @property
908
- def hostname(self) -> str:
907
+ def hostname(self) -> str | None:
909
908
  """Return charger hostname."""
910
- assert self._config is not None
911
- return self._config["hostname"]
909
+ return self._config.get("hostname")
912
910
 
913
911
  @property
914
- def wifi_ssid(self) -> str:
912
+ def wifi_ssid(self) -> str | None:
915
913
  """Return charger connected SSID."""
916
- assert self._config is not None
917
- return self._config["ssid"]
914
+ return self._config.get("ssid")
918
915
 
919
916
  @property
920
- def ammeter_offset(self) -> int:
917
+ def ammeter_offset(self) -> int | None:
921
918
  """Return ammeter's current offset."""
922
- assert self._config is not None
923
- return self._config["offset"]
919
+ return self._config.get("offset")
924
920
 
925
921
  @property
926
- def ammeter_scale_factor(self) -> int:
922
+ def ammeter_scale_factor(self) -> int | None:
927
923
  """Return ammeter's current scale factor."""
928
- assert self._config is not None
929
- return self._config["scale"]
924
+ return self._config.get("scale")
930
925
 
931
926
  @property
932
927
  def temp_check_enabled(self) -> bool:
@@ -954,23 +949,21 @@ class OpenEVSE:
954
949
  return bool(self._config.get("relayt", False))
955
950
 
956
951
  @property
957
- def service_level(self) -> str:
952
+ def service_level(self) -> str | None:
958
953
  """Return the service level."""
959
- assert self._config is not None
960
- return self._config["service"]
954
+ return self._config.get("service")
961
955
 
962
956
  @property
963
- def openevse_firmware(self) -> str:
957
+ def openevse_firmware(self) -> str | None:
964
958
  """Return the firmware version."""
965
- assert self._config is not None
966
- return self._config["firmware"]
959
+ return self._config.get("firmware")
967
960
 
968
961
  @property
969
962
  def max_current_soft(self) -> int | None:
970
963
  """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"]
964
+ if "max_current_soft" in self._config:
965
+ return self._config.get("max_current_soft")
966
+ return self._status.get("pilot")
974
967
 
975
968
  @property
976
969
  async def async_charge_current(self) -> int | None:
@@ -984,9 +977,9 @@ class OpenEVSE:
984
977
  return min(
985
978
  claims["properties"]["charge_current"], self._config["max_current_hard"]
986
979
  )
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"]
980
+ if "max_current_soft" in self._config:
981
+ return self._config.get("max_current_soft")
982
+ return self._status.get("pilot")
990
983
 
991
984
  @property
992
985
  def max_current(self) -> int | None:
@@ -994,33 +987,29 @@ class OpenEVSE:
994
987
  return self._status.get("max_current", None)
995
988
 
996
989
  @property
997
- def wifi_firmware(self) -> str:
990
+ def wifi_firmware(self) -> str | None:
998
991
  """Return the ESP firmware version."""
999
- assert self._config is not None
1000
- value = self._config["version"]
1001
- if "dev" in value:
992
+ value = self._config.get("version")
993
+ if value is not None and "dev" in value:
1002
994
  _LOGGER.debug("Stripping 'dev' from version.")
1003
995
  value = value.split(".")
1004
996
  value = ".".join(value[0:3])
1005
997
  return value
1006
998
 
1007
999
  @property
1008
- def ip_address(self) -> str:
1000
+ def ip_address(self) -> str | None:
1009
1001
  """Return the ip address."""
1010
- assert self._status is not None
1011
- return self._status["ipaddress"]
1002
+ return self._status.get("ipaddress")
1012
1003
 
1013
1004
  @property
1014
- def charging_voltage(self) -> int:
1005
+ def charging_voltage(self) -> int | None:
1015
1006
  """Return the charging voltage."""
1016
- assert self._status is not None
1017
- return self._status["voltage"]
1007
+ return self._status.get("voltage")
1018
1008
 
1019
1009
  @property
1020
- def mode(self) -> str:
1010
+ def mode(self) -> str | None:
1021
1011
  """Return the mode."""
1022
- assert self._status is not None
1023
- return self._status["mode"]
1012
+ return self._status.get("mode")
1024
1013
 
1025
1014
  @property
1026
1015
  def using_ethernet(self) -> bool:
@@ -1028,98 +1017,80 @@ class OpenEVSE:
1028
1017
  return bool(self._status.get("eth_connected", False))
1029
1018
 
1030
1019
  @property
1031
- def stuck_relay_trip_count(self) -> int:
1020
+ def stuck_relay_trip_count(self) -> int | None:
1032
1021
  """Return the stuck relay count."""
1033
- assert self._status is not None
1034
- return self._status["stuckcount"]
1022
+ return self._status.get("stuckcount")
1035
1023
 
1036
1024
  @property
1037
- def no_gnd_trip_count(self) -> int:
1025
+ def no_gnd_trip_count(self) -> int | None:
1038
1026
  """Return the no ground count."""
1039
- assert self._status is not None
1040
- return self._status["nogndcount"]
1027
+ return self._status.get("nogndcount")
1041
1028
 
1042
1029
  @property
1043
- def gfi_trip_count(self) -> int:
1030
+ def gfi_trip_count(self) -> int | None:
1044
1031
  """Return the GFCI count."""
1045
- assert self._status is not None
1046
- return self._status["gfcicount"]
1032
+ return self._status.get("gfcicount")
1047
1033
 
1048
1034
  @property
1049
1035
  def status(self) -> str:
1050
1036
  """Return charger's state."""
1051
- state = self._status.get("status", states[int(self._status.get("state", 0))])
1052
- return state
1037
+ return self._status.get("status", states[int(self._status.get("state", 0))])
1053
1038
 
1054
1039
  @property
1055
1040
  def state(self) -> str:
1056
1041
  """Return charger's state."""
1057
- assert self._status is not None
1058
- return states[int(self._status["state"])]
1042
+ return states[int(self._status.get("state", 0))]
1059
1043
 
1060
1044
  @property
1061
- def state_raw(self) -> int:
1045
+ def state_raw(self) -> int | None:
1062
1046
  """Return charger's state int form."""
1063
- assert self._status is not None
1064
- return self._status["state"]
1047
+ return self._status.get("state")
1065
1048
 
1066
1049
  @property
1067
- def charge_time_elapsed(self) -> int:
1050
+ def charge_time_elapsed(self) -> int | None:
1068
1051
  """Return elapsed charging time."""
1069
- assert self._status is not None
1070
- return self._status["elapsed"]
1052
+ return self._status.get("elapsed")
1071
1053
 
1072
1054
  @property
1073
- def wifi_signal(self) -> str:
1055
+ def wifi_signal(self) -> str | None:
1074
1056
  """Return charger's wifi signal."""
1075
- assert self._status is not None
1076
- return self._status["srssi"]
1057
+ return self._status.get("srssi")
1077
1058
 
1078
1059
  @property
1079
- def charging_current(self) -> float:
1060
+ def charging_current(self) -> float | None:
1080
1061
  """Return the charge current.
1081
1062
 
1082
1063
  0 if is not currently charging.
1083
1064
  """
1084
- assert self._status is not None
1085
- return self._status["amp"]
1065
+ return self._status.get("amp")
1086
1066
 
1087
1067
  @property
1088
- def current_capacity(self) -> int:
1068
+ def current_capacity(self) -> int | None:
1089
1069
  """Return the current capacity."""
1090
- assert self._status is not None
1091
- return self._status["pilot"]
1070
+ return self._status.get("pilot")
1092
1071
 
1093
1072
  @property
1094
- def usage_total(self) -> float:
1073
+ def usage_total(self) -> float | None:
1095
1074
  """Return the total energy usage in Wh."""
1096
- assert self._status is not None
1097
1075
  if "total_energy" in self._status:
1098
- return self._status["total_energy"]
1099
- return self._status["watthour"]
1076
+ return self._status.get("total_energy")
1077
+ return self._status.get("watthour")
1100
1078
 
1101
1079
  @property
1102
1080
  def ambient_temperature(self) -> float | None:
1103
1081
  """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
1082
+ temp = self._status.get("temp")
1083
+ if temp:
1084
+ return temp / 10
1085
+ return self._status.get("temp1", 0) / 10
1111
1086
 
1112
1087
  @property
1113
1088
  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
1089
+ """Return the temperature of the real time clock sensor."""
1090
+ temp = self._status.get("temp2")
1091
+ if temp is None or isinstance(temp, bool):
1092
+ return None
1093
+ return float(temp) / 10
1123
1094
 
1124
1095
  @property
1125
1096
  def ir_temperature(self) -> float | None:
@@ -1127,37 +1098,42 @@ class OpenEVSE:
1127
1098
 
1128
1099
  In degrees Celsius.
1129
1100
  """
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
1101
+ temp = self._status.get("temp3")
1102
+ if temp is None or isinstance(temp, bool):
1103
+ return None
1104
+ return float(temp) / 10
1135
1105
 
1136
1106
  @property
1137
1107
  def esp_temperature(self) -> float | None:
1138
1108
  """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
1109
+ temp = self._status.get("temp4")
1110
+ if temp is None or isinstance(temp, bool):
1111
+ return None
1112
+ return float(temp) / 10
1145
1113
 
1146
1114
  @property
1147
- def time(self) -> datetime.datetime | None:
1115
+ def time(self) -> datetime | None:
1148
1116
  """Get the RTC time."""
1149
- return self._status.get("time", None)
1117
+ value = self._status.get("time")
1118
+ if value:
1119
+ try:
1120
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
1121
+ except (ValueError, AttributeError):
1122
+ return None
1123
+ return None
1150
1124
 
1151
1125
  @property
1152
- def usage_session(self) -> float:
1126
+ def usage_session(self) -> float | None:
1153
1127
  """Get the energy usage for the current charging session.
1154
1128
 
1155
1129
  Return the energy usage in Wh.
1156
1130
  """
1157
- assert self._status is not None
1158
1131
  if "session_energy" in self._status:
1159
- return self._status["session_energy"]
1160
- return float(round(self._status["wattsec"] / 3600, 2))
1132
+ return self._status.get("session_energy")
1133
+ wattsec = self._status.get("wattsec")
1134
+ if wattsec is not None:
1135
+ return float(round(wattsec / 3600, 2))
1136
+ return None
1161
1137
 
1162
1138
  @property
1163
1139
  def total_day(self) -> float | None:
@@ -1187,61 +1163,53 @@ class OpenEVSE:
1187
1163
  @property
1188
1164
  def protocol_version(self) -> str | None:
1189
1165
  """Return the protocol version."""
1190
- assert self._config is not None
1191
- if self._config["protocol"] == "-":
1166
+ protocol = self._config.get("protocol")
1167
+ if protocol == "-":
1192
1168
  return None
1193
- return self._config["protocol"]
1169
+ return protocol
1194
1170
 
1195
1171
  @property
1196
- def vehicle(self) -> str:
1172
+ def vehicle(self) -> bool:
1197
1173
  """Return if a vehicle is connected dto the EVSE."""
1198
- assert self._status is not None
1199
- return self._status["vehicle"]
1174
+ return self._status.get("vehicle", False)
1200
1175
 
1201
1176
  @property
1202
- def ota_update(self) -> str:
1177
+ def ota_update(self) -> bool:
1203
1178
  """Return if an OTA update is active."""
1204
- assert self._status is not None
1205
- return self._status["ota_update"]
1179
+ return self._status.get("ota_update", False)
1206
1180
 
1207
1181
  @property
1208
- def manual_override(self) -> str:
1182
+ def manual_override(self) -> bool:
1209
1183
  """Return if Manual Override is set."""
1210
- assert self._status is not None
1211
- return self._status["manual_override"]
1184
+ return self._status.get("manual_override", False)
1212
1185
 
1213
1186
  @property
1214
1187
  def divertmode(self) -> str:
1215
1188
  """Return the divert mode."""
1216
- assert self._status is not None
1217
- mode = self._status["divertmode"]
1189
+ mode = self._status.get("divertmode", 1)
1218
1190
  if mode == 1:
1219
1191
  return "fast"
1220
1192
  return "eco"
1221
1193
 
1222
1194
  @property
1223
- def charge_mode(self) -> str:
1195
+ def charge_mode(self) -> str | None:
1224
1196
  """Return the charge mode."""
1225
- assert self._config is not None
1226
- return self._config["charge_mode"]
1197
+ return self._config.get("charge_mode")
1227
1198
 
1228
1199
  @property
1229
- def available_current(self) -> float:
1200
+ def available_current(self) -> float | None:
1230
1201
  """Return the computed available current for divert."""
1231
- assert self._status is not None
1232
- return self._status["available_current"]
1202
+ return self._status.get("available_current")
1233
1203
 
1234
1204
  @property
1235
- def smoothed_available_current(self) -> float:
1205
+ def smoothed_available_current(self) -> float | None:
1236
1206
  """Return the computed smoothed available current for divert."""
1237
- assert self._status is not None
1238
- return self._status["smoothed_available_current"]
1207
+ return self._status.get("smoothed_available_current")
1239
1208
 
1240
1209
  @property
1241
- def charge_rate(self) -> float:
1210
+ def charge_rate(self) -> float | None:
1242
1211
  """Return the divert charge rate."""
1243
- assert self._status is not None
1244
- return self._status["charge_rate"]
1212
+ return self._status.get("charge_rate")
1245
1213
 
1246
1214
  @property
1247
1215
  def divert_active(self) -> bool:
@@ -1259,7 +1227,7 @@ class OpenEVSE:
1259
1227
 
1260
1228
  Calculate Watts base on V*I
1261
1229
  """
1262
- if self._status is not None and any(
1230
+ if self._status is not None and all(
1263
1231
  key in self._status for key in ["voltage", "amp"]
1264
1232
  ):
1265
1233
  return round(self._status["voltage"] * self._status["amp"], 2)
@@ -1308,11 +1276,14 @@ class OpenEVSE:
1308
1276
  )
1309
1277
 
1310
1278
  @property
1311
- def vehicle_eta(self) -> int | None:
1279
+ def vehicle_eta(self) -> datetime | None:
1312
1280
  """Return time to full charge."""
1313
- return self._status.get(
1314
- "vehicle_eta", self._status.get("time_to_full_charge", None)
1281
+ value = self._status.get(
1282
+ "time_to_full_charge", self._status.get("vehicle_eta", None)
1315
1283
  )
1284
+ if value is not None:
1285
+ return datetime.now(timezone.utc) + timedelta(seconds=value)
1286
+ return value
1316
1287
 
1317
1288
  # There is currently no min/max amps JSON data
1318
1289
  # available via HTTP API methods
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -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.1"
9
+ VERSION = "0.2.3"
10
10
 
11
11
  setup(
12
12
  name="python_openevse_http",
@@ -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
  [
@@ -661,29 +687,27 @@ async def test_get_charge_rate(fixture, expected, request):
661
687
 
662
688
 
663
689
  @pytest.mark.parametrize(
664
- "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)]
690
+ "fixture, expected", [("test_charger", None), ("test_charger_v2", None)]
665
691
  )
666
692
  async def test_get_available_current(fixture, expected, request):
667
693
  """Test v4 Status reply."""
668
694
  charger = request.getfixturevalue(fixture)
669
695
  await charger.update()
670
- with pytest.raises(KeyError):
671
- status = charger.available_current
672
- # assert status == expected
673
- await charger.ws_disconnect()
696
+ status = charger.available_current
697
+ assert status == expected
698
+ await charger.ws_disconnect()
674
699
 
675
700
 
676
701
  @pytest.mark.parametrize(
677
- "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)]
702
+ "fixture, expected", [("test_charger", None), ("test_charger_v2", None)]
678
703
  )
679
704
  async def test_get_smoothed_available_current(fixture, expected, request):
680
705
  """Test v4 Status reply."""
681
706
  charger = request.getfixturevalue(fixture)
682
707
  await charger.update()
683
- with pytest.raises(KeyError):
684
- status = charger.smoothed_available_current
685
- # assert status == expected
686
- await charger.ws_disconnect()
708
+ status = charger.smoothed_available_current
709
+ assert status == expected
710
+ await charger.ws_disconnect()
687
711
 
688
712
 
689
713
  @pytest.mark.parametrize(
@@ -700,16 +724,15 @@ async def test_get_divert_active(fixture, expected, request):
700
724
 
701
725
 
702
726
  @pytest.mark.parametrize(
703
- "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)]
727
+ "fixture, expected", [("test_charger", False), ("test_charger_v2", False)]
704
728
  )
705
729
  async def test_get_manual_override(fixture, expected, request):
706
730
  """Test v4 Status reply."""
707
731
  charger = request.getfixturevalue(fixture)
708
732
  await charger.update()
709
- with pytest.raises(KeyError):
710
- status = charger.manual_override
711
- # assert status == expected
712
- await charger.ws_disconnect()
733
+ status = charger.manual_override
734
+ assert status == expected
735
+ await charger.ws_disconnect()
713
736
 
714
737
 
715
738
  async def test_toggle_override(
@@ -1266,14 +1289,25 @@ async def test_vehicle_range(fixture, expected, request):
1266
1289
 
1267
1290
 
1268
1291
  @pytest.mark.parametrize(
1269
- "fixture, expected", [("test_charger", 18000), ("test_charger_v2", None)]
1292
+ "fixture, expected_seconds", [("test_charger", 18000), ("test_charger_v2", None)]
1270
1293
  )
1271
- async def test_vehicle_eta(fixture, expected, request):
1294
+ @freeze_time("2026-01-09 12:00:00+00:00")
1295
+ async def test_vehicle_eta(fixture, expected_seconds, request):
1272
1296
  """Test vehicle_eta reply."""
1273
1297
  charger = request.getfixturevalue(fixture)
1274
1298
  await charger.update()
1275
- status = charger.vehicle_eta
1276
- assert status == expected
1299
+
1300
+ result = charger.vehicle_eta
1301
+
1302
+ if expected_seconds is not None:
1303
+ # Calculate what the expected datetime should be based on our frozen time
1304
+ expected_datetime = datetime(
1305
+ 2026, 1, 9, 12, 0, 0, tzinfo=timezone.utc
1306
+ ) + timedelta(seconds=expected_seconds)
1307
+ assert result == expected_datetime
1308
+ else:
1309
+ assert result is None
1310
+
1277
1311
  await charger.ws_disconnect()
1278
1312
 
1279
1313
 
@@ -2235,10 +2269,10 @@ async def test_main_auth_instantiation():
2235
2269
  # Ensure session.get() returns the request context
2236
2270
  mock_session.get.return_value = mock_request_ctx
2237
2271
 
2238
- with patch("aiohttp.ClientSession", return_value=mock_session), patch(
2239
- "aiohttp.BasicAuth"
2240
- ) as mock_basic_auth:
2241
-
2272
+ with (
2273
+ patch("aiohttp.ClientSession", return_value=mock_session),
2274
+ patch("aiohttp.BasicAuth") as mock_basic_auth,
2275
+ ):
2242
2276
  await charger.update()
2243
2277
 
2244
2278
  # 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