python-openevse-http 1.4.0__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
openevsehttp/__init__.py CHANGED
@@ -16,11 +16,15 @@ from .const import (
16
16
  from .exceptions import (
17
17
  AlreadyListening,
18
18
  AuthenticationError,
19
+ CommandFailedError,
20
+ FirmwareResolutionError,
19
21
  InvalidType,
20
22
  MissingMethod,
21
23
  MissingSerial,
24
+ OpenEVSEError,
22
25
  ParseJSONError,
23
26
  UnknownError,
27
+ UnknownStateError,
24
28
  UnsupportedFeature,
25
29
  )
26
30
  from .websocket import (
@@ -43,15 +47,19 @@ __all__ = [
43
47
  "UPDATE_TRIGGERS",
44
48
  "AlreadyListening",
45
49
  "AuthenticationError",
50
+ "CommandFailedError",
46
51
  "ContentTypeError",
52
+ "FirmwareResolutionError",
47
53
  "InvalidType",
48
54
  "MissingMethod",
49
55
  "MissingSerial",
50
56
  "OpenEVSE",
57
+ "OpenEVSEError",
51
58
  "OpenEVSEWebsocket",
52
59
  "ParseJSONError",
53
60
  "ServerTimeoutError",
54
61
  "UnknownError",
62
+ "UnknownStateError",
55
63
  "UnsupportedFeature",
56
64
  "divert_mode",
57
65
  "states",
openevsehttp/commands.py CHANGED
@@ -13,7 +13,12 @@ from awesomeversion import AwesomeVersion
13
13
  from awesomeversion.exceptions import AwesomeVersionCompareException
14
14
 
15
15
  from .const import MAX_AMPS, MIN_AMPS, RAPI_ERRORS, SUCCESS_ANSWERS, divert_mode
16
- from .exceptions import UnknownError, UnsupportedFeature
16
+ from .exceptions import (
17
+ CommandFailedError,
18
+ FirmwareResolutionError,
19
+ UnknownStateError,
20
+ UnsupportedFeature,
21
+ )
17
22
  from .utils import get_awesome_version
18
23
 
19
24
  _LOGGER = logging.getLogger(__name__)
@@ -90,12 +95,12 @@ class CommandsMixin:
90
95
  msg = response.get("msg") if isinstance(response, Mapping) else None
91
96
  if msg not in SUCCESS_ANSWERS:
92
97
  _LOGGER.error("Problem issuing command: %s", response)
93
- raise UnknownError
98
+ raise CommandFailedError(f"Problem issuing command: {response}")
94
99
 
95
100
  async def divert_mode(self) -> Mapping[str, Any] | list[Any]:
96
101
  """Set the divert mode to either Normal or Eco modes."""
97
102
  if not self._config:
98
- raise RuntimeError("Missing configuration: self._config is required")
103
+ raise UnknownStateError("Missing configuration: self._config is required")
99
104
 
100
105
  if not self._version_check("2.9.1"):
101
106
  _LOGGER.debug("Feature not supported for older firmware.")
@@ -213,7 +218,7 @@ class CommandsMixin:
213
218
  or response.get("msg") not in SUCCESS_ANSWERS
214
219
  ):
215
220
  _LOGGER.error("Problem toggling override: %s", response)
216
- raise RuntimeError(f"Failed to toggle override: {response}")
221
+ raise CommandFailedError(f"Failed to toggle override: {response}")
217
222
  else:
218
223
  # Older firmware use RAPI commands
219
224
  _LOGGER.debug("Toggling manual override via RAPI")
@@ -222,7 +227,9 @@ class CommandsMixin:
222
227
 
223
228
  if "state" not in self._status:
224
229
  _LOGGER.error("Cannot toggle override: unknown charger state.")
225
- raise RuntimeError("Cannot toggle override: unknown charger state.")
230
+ raise UnknownStateError(
231
+ "Cannot toggle override: unknown charger state."
232
+ )
226
233
 
227
234
  command = "$FE" if self._status.get("state") == 254 else "$FS"
228
235
  response, msg = await self.send_command(command)
@@ -231,7 +238,7 @@ class CommandsMixin:
231
238
  isinstance(msg, str) and (msg.startswith("$NK") or msg in RAPI_ERRORS)
232
239
  ):
233
240
  _LOGGER.error("Problem toggling override via RAPI: %s", msg)
234
- raise RuntimeError(f"Failed to toggle override via RAPI: {msg}")
241
+ raise CommandFailedError(f"Failed to toggle override via RAPI: {msg}")
235
242
 
236
243
  async def clear_override(self) -> None:
237
244
  """Clear the manual override status."""
@@ -247,7 +254,7 @@ class CommandsMixin:
247
254
  _LOGGER.debug("Clear override response: %s", msg)
248
255
  if msg not in SUCCESS_ANSWERS:
249
256
  _LOGGER.error("Problem clearing override: %s", response)
250
- raise RuntimeError(f"Failed to clear override: {response}")
257
+ raise CommandFailedError(f"Failed to clear override: {response}")
251
258
 
252
259
  async def set_current(self, amps: int = 6) -> None:
253
260
  """Set the soft current limit."""
@@ -276,7 +283,7 @@ class CommandsMixin:
276
283
  or response.get("msg") not in SUCCESS_ANSWERS
277
284
  ):
278
285
  _LOGGER.error("Problem setting current limit: %s", response)
279
- raise UnknownError
286
+ raise CommandFailedError(f"Problem setting current limit: {response}")
280
287
 
281
288
  else:
282
289
  # RAPI commands
@@ -291,7 +298,7 @@ class CommandsMixin:
291
298
  isinstance(msg, str) and (msg.startswith("$NK") or msg in RAPI_ERRORS)
292
299
  ):
293
300
  _LOGGER.error("Problem setting current via RAPI: %s", msg)
294
- raise UnknownError
301
+ raise CommandFailedError(f"Problem setting current via RAPI: {msg}")
295
302
 
296
303
  async def set_service_level(self, level: int | str = 2) -> None:
297
304
  """Set the service level of the EVSE."""
@@ -311,7 +318,7 @@ class CommandsMixin:
311
318
  msg = response.get("msg") if isinstance(response, Mapping) else None
312
319
  if msg not in SUCCESS_ANSWERS:
313
320
  _LOGGER.error("Problem issuing command: %s", response)
314
- raise UnknownError
321
+ raise CommandFailedError(f"Problem issuing command: {response}")
315
322
 
316
323
  # Restart OpenEVSE WiFi
317
324
  async def restart_wifi(self) -> None:
@@ -346,7 +353,7 @@ class CommandsMixin:
346
353
 
347
354
  if not success:
348
355
  _LOGGER.error("Problem restarting WiFi: %s", response)
349
- raise RuntimeError(f"Failed to restart WiFi: {msg}")
356
+ raise CommandFailedError(f"Failed to restart WiFi: {msg}")
350
357
 
351
358
  # Restart EVSE module
352
359
  async def restart_evse(self) -> None:
@@ -364,7 +371,9 @@ class CommandsMixin:
364
371
  or reply.get("error")
365
372
  ):
366
373
  _LOGGER.error("Problem restarting EVSE module via HTTP: %s", reply)
367
- raise RuntimeError(f"Failed to restart EVSE module via HTTP: {reply}")
374
+ raise CommandFailedError(
375
+ f"Failed to restart EVSE module via HTTP: {reply}"
376
+ )
368
377
 
369
378
  response = (
370
379
  reply.get("msg", "Unknown error")
@@ -381,7 +390,7 @@ class CommandsMixin:
381
390
  and (response.startswith("$NK") or response in RAPI_ERRORS)
382
391
  ):
383
392
  _LOGGER.error("Problem restarting EVSE module via RAPI: %s", response)
384
- raise RuntimeError(
393
+ raise CommandFailedError(
385
394
  f"Failed to restart EVSE module via RAPI: {response}"
386
395
  )
387
396
 
@@ -563,7 +572,7 @@ class CommandsMixin:
563
572
  _LOGGER.error(
564
573
  "Could not resolve latest firmware download URL from GitHub."
565
574
  )
566
- raise RuntimeError(
575
+ raise FirmwareResolutionError(
567
576
  "Could not resolve latest firmware download URL from GitHub."
568
577
  )
569
578
  firmware_url = check_result["browser_download_url"]
@@ -605,7 +614,7 @@ class CommandsMixin:
605
614
  msg = response.get("msg") if isinstance(response, Mapping) else None
606
615
  if msg not in SUCCESS_ANSWERS:
607
616
  _LOGGER.error("Problem issuing command: %s", response)
608
- raise UnknownError
617
+ raise CommandFailedError(f"Problem issuing command: {response}")
609
618
 
610
619
  async def set_divert_mode(self, mode: str = "fast") -> None:
611
620
  """Set the divert mode."""
@@ -630,7 +639,7 @@ class CommandsMixin:
630
639
 
631
640
  if not success:
632
641
  _LOGGER.error("Problem issuing command: %s", response)
633
- raise UnknownError
642
+ raise CommandFailedError(f"Problem issuing command: {response}")
634
643
 
635
644
  self._status["divertmode"] = new_mode
636
645
 
@@ -650,7 +659,7 @@ class CommandsMixin:
650
659
  msg = response.get("msg") if isinstance(response, Mapping) else None
651
660
  if msg not in SUCCESS_ANSWERS and msg != "Current Shaper state changed":
652
661
  _LOGGER.error("Problem issuing command: %s", response)
653
- raise UnknownError
662
+ raise CommandFailedError(f"Problem issuing command: {response}")
654
663
 
655
664
  self._status["shaper"] = mode
656
665
 
@@ -663,7 +672,49 @@ class CommandsMixin:
663
672
 
664
673
  if shaper_active is None:
665
674
  _LOGGER.error("Cannot toggle shaper: unknown shaper state.")
666
- raise RuntimeError("Cannot toggle shaper: unknown shaper state.")
675
+ raise UnknownStateError("Cannot toggle shaper: unknown shaper state.")
667
676
 
668
677
  new_state = not bool(shaper_active)
669
678
  await self.set_shaper(new_state)
679
+
680
+ async def set_mqtt_vehicle_range_miles(self, enable: bool = True) -> None:
681
+ """Set mqtt_vehicle_range_miles configuration setting.
682
+
683
+ Dynamically changing this setting will affect future evaluations of
684
+ the vehicle_range_with_unit property.
685
+ """
686
+ if not isinstance(enable, bool):
687
+ raise TypeError("Value must be a boolean.")
688
+
689
+ url = f"{self.url}config"
690
+ data = {"mqtt_vehicle_range_miles": enable}
691
+
692
+ _LOGGER.debug("Setting mqtt_vehicle_range_miles to %s", enable)
693
+ response = await self.process_request(url=url, method="post", data=data)
694
+ response = self._normalize_response(response)
695
+ msg = response.get("msg") if isinstance(response, Mapping) else None
696
+ if msg not in SUCCESS_ANSWERS:
697
+ _LOGGER.error("Problem issuing command: %s", response)
698
+ raise CommandFailedError(f"Problem issuing command: {response}")
699
+
700
+ async def set_rfid_enabled(self, enable: bool = True) -> None:
701
+ """Enable or disable RFID access."""
702
+ if not self._version_check("4.1.4"):
703
+ _LOGGER.debug("Feature not supported for older firmware.")
704
+ raise UnsupportedFeature
705
+
706
+ if not isinstance(enable, bool):
707
+ raise TypeError("Value must be a boolean.")
708
+
709
+ url = f"{self.url}config"
710
+ data = {"rfid_enabled": enable}
711
+
712
+ _LOGGER.debug("Setting rfid_enabled to %s", enable)
713
+ response = await self.process_request(url=url, method="post", data=data)
714
+ response = self._normalize_response(response)
715
+ msg = response.get("msg") if isinstance(response, Mapping) else None
716
+ if msg not in SUCCESS_ANSWERS:
717
+ _LOGGER.error("Problem issuing command: %s", response)
718
+ raise CommandFailedError(f"Problem issuing command: {response}")
719
+
720
+ self._config["rfid_enabled"] = enable
@@ -1,33 +1,49 @@
1
1
  """Exceptions."""
2
2
 
3
3
 
4
- class AuthenticationError(Exception):
4
+ class OpenEVSEError(Exception):
5
+ """Base exception for python-openevse-http."""
6
+
7
+
8
+ class AuthenticationError(OpenEVSEError):
5
9
  """Exception for authentication errors."""
6
10
 
7
11
 
8
- class ParseJSONError(Exception):
12
+ class ParseJSONError(OpenEVSEError):
9
13
  """Exception for JSON parsing errors."""
10
14
 
11
15
 
12
- class UnknownError(Exception):
16
+ class UnknownError(OpenEVSEError):
13
17
  """Exception for Unknown errors."""
14
18
 
15
19
 
16
- class MissingMethod(Exception):
20
+ class MissingMethod(OpenEVSEError):
17
21
  """Exception for missing method variable."""
18
22
 
19
23
 
20
- class AlreadyListening(Exception):
24
+ class AlreadyListening(OpenEVSEError):
21
25
  """Exception for already listening websocket."""
22
26
 
23
27
 
24
- class MissingSerial(Exception):
28
+ class MissingSerial(OpenEVSEError):
25
29
  """Exception for missing serial number."""
26
30
 
27
31
 
28
- class UnsupportedFeature(Exception):
32
+ class UnsupportedFeature(OpenEVSEError):
29
33
  """Exception for firmware that is too old."""
30
34
 
31
35
 
32
- class InvalidType(Exception):
36
+ class InvalidType(OpenEVSEError):
33
37
  """Exception for invalid types."""
38
+
39
+
40
+ class CommandFailedError(OpenEVSEError):
41
+ """Exception for command rejections or failures."""
42
+
43
+
44
+ class UnknownStateError(OpenEVSEError):
45
+ """Exception when charger state cannot be determined."""
46
+
47
+
48
+ class FirmwareResolutionError(OpenEVSEError):
49
+ """Exception when firmware download URL cannot be resolved."""
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ import warnings
6
7
  from collections.abc import Mapping
7
8
  from datetime import datetime, timedelta, timezone
8
9
  from typing import Any, cast
@@ -458,11 +459,33 @@ class PropertiesMixin:
458
459
  @property
459
460
  def vehicle_range(self) -> int | None:
460
461
  """Return battery range."""
462
+ warnings.warn(
463
+ "vehicle_range is deprecated, use vehicle_range_with_unit instead",
464
+ DeprecationWarning,
465
+ stacklevel=2,
466
+ )
461
467
  return cast(
462
468
  "int | None",
463
469
  self._status.get("vehicle_range", self._status.get("battery_range", None)),
464
470
  )
465
471
 
472
+ @property
473
+ def vehicle_range_with_unit(self) -> tuple[int, str] | None:
474
+ """Return battery range and its unit."""
475
+ value = cast(
476
+ "int | None",
477
+ self._status.get("vehicle_range", self._status.get("battery_range", None)),
478
+ )
479
+ if value is None:
480
+ return None
481
+ unit = "miles" if self.mqtt_vehicle_range_miles else "km"
482
+ return (value, unit)
483
+
484
+ @property
485
+ def mqtt_vehicle_range_miles(self) -> bool:
486
+ """Return True if mqtt vehicle range is in miles, False if km."""
487
+ return bool(self._config.get("mqtt_vehicle_range_miles", False))
488
+
466
489
  @property
467
490
  def vehicle_eta(self) -> datetime | None:
468
491
  """Return time to full charge."""
@@ -545,3 +568,11 @@ class PropertiesMixin:
545
568
  _LOGGER.debug("Feature not supported for older firmware.")
546
569
  raise UnsupportedFeature
547
570
  return int(self._status.get("power", 0))
571
+
572
+ @property
573
+ def rfid_enabled(self) -> bool | None:
574
+ """Return charger RFID enabled state."""
575
+ if not self._version_check("4.1.4"):
576
+ _LOGGER.debug("Feature not supported for older firmware.")
577
+ raise UnsupportedFeature
578
+ return self._config.get("rfid_enabled")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 1.4.0
3
+ Version: 1.6.0
4
4
  Summary: Python wrapper for OpenEVSE HTTP API
5
5
  Home-page: https://github.com/firstof9/python-openevse-http
6
6
  Download-URL: https://github.com/firstof9/python-openevse-http
@@ -65,6 +65,7 @@ import asyncio
65
65
  import aiohttp
66
66
  from openevsehttp import OpenEVSE
67
67
 
68
+
68
69
  async def main():
69
70
  async with aiohttp.ClientSession() as session:
70
71
  charger = OpenEVSE("192.168.1.30", session=session)
@@ -81,6 +82,7 @@ async def main():
81
82
  await charger.toggle_shaper()
82
83
  await charger.ws_disconnect()
83
84
 
85
+
84
86
  if __name__ == "__main__":
85
87
  asyncio.run(main())
86
88
  ```
@@ -1,17 +1,17 @@
1
- openevsehttp/__init__.py,sha256=I6a1mjOZHYiWb_qfCuDuFLOOncrkkB_7uwybtOIujfY,1165
1
+ openevsehttp/__init__.py,sha256=QeXl80ozsK6J9PN-jB_83Bde31YXFGgLuUdmArpZAMM,1363
2
2
  openevsehttp/__main__.py,sha256=EHmSdT7GjAVvHQxvLBTjZXsj_V5SB6B2_kpgUAT7mPM,146
3
3
  openevsehttp/client.py,sha256=bySzhJ9SKU0IN7-QQfW7_ZrKNw-EoPV4RvnRBXtmCQ8,19326
4
- openevsehttp/commands.py,sha256=OFPreBlsXUViwvjbVsD_0bmLv9ZSWqHnwdo2lBqdrSs,27278
4
+ openevsehttp/commands.py,sha256=LzGGQbFnpLGwZ7mFgJ_xb08g6PV8-r-sqOmiMlqNlwY,29691
5
5
  openevsehttp/const.py,sha256=-frkFduu8hxEBab9FnwA3D65XHrsyH0W7P9miYsWh90,1726
6
- openevsehttp/exceptions.py,sha256=bqz-tHTW1AYJMKcm0s5M6z5tA6XZgjnCiBLW1XrZ_70,672
6
+ openevsehttp/exceptions.py,sha256=FkU2v6LaZodUmZy78bOsF16der3M0T3y6pQXzTXwFg0,1106
7
7
  openevsehttp/managers.py,sha256=EtQMQziwhoZeqKe2zWY-0yS7zedhuYjYtL5j9xBBCZ0,5380
8
- openevsehttp/properties.py,sha256=xbQfx256FZwNPPn9ryWWOMKliyCChq4ZaTH9NDQVR_8,17968
8
+ openevsehttp/properties.py,sha256=yFzpZQAzSNwiz2gAcqC3Spe9gQHa_orLyfgs7eZwL14,19093
9
9
  openevsehttp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  openevsehttp/sensors.py,sha256=zhQSToY_Zp2rOhPnUXT40xS5ZnUavqNGLm4zAFiUUrY,5717
11
11
  openevsehttp/utils.py,sha256=fPUWL64gFTRw4TtV4WITDAFMJ75VjNhPBaU6diwVlv0,2019
12
12
  openevsehttp/websocket.py,sha256=yMuLNsB2qYZLtfYRW7-D7NH7rv-qaSTp7F79OajKm_0,10790
13
- python_openevse_http-1.4.0.dist-info/licenses/LICENSE,sha256=hSB6TOQ7rmwSGb6XzqRjDGMvmUj5_GlacqQin3tegtA,11341
14
- python_openevse_http-1.4.0.dist-info/METADATA,sha256=uQBdZBdNAnWvyuwoWpgJYjBDlxUAoDxot4LANe6TZTY,5411
15
- python_openevse_http-1.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
- python_openevse_http-1.4.0.dist-info/top_level.txt,sha256=u8RUkoEIE33Cjn6gmqiEoVpZ0VZ59WJ3FXBwwOg0CPE,13
17
- python_openevse_http-1.4.0.dist-info/RECORD,,
13
+ python_openevse_http-1.6.0.dist-info/licenses/LICENSE,sha256=hSB6TOQ7rmwSGb6XzqRjDGMvmUj5_GlacqQin3tegtA,11341
14
+ python_openevse_http-1.6.0.dist-info/METADATA,sha256=t-5bsWNGUFccdXWSE67jI_68jQSoak7CzQXyIH8zhqk,5413
15
+ python_openevse_http-1.6.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
16
+ python_openevse_http-1.6.0.dist-info/top_level.txt,sha256=u8RUkoEIE33Cjn6gmqiEoVpZ0VZ59WJ3FXBwwOg0CPE,13
17
+ python_openevse_http-1.6.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5